Merge branch 'trunk' into issue-37839
This commit is contained in:
commit
4af01af796
|
@ -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.
|
||||
|
||||
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.
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add Inventory item to the global Quick Update dropdown
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add Shipping item to the global Quick Update dropdown
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add woocommerce/taxonomy-field block
|
|
@ -26,3 +26,4 @@ export { init as initRequirePassword } from './password';
|
|||
export { init as initVariationItems } from './variation-items';
|
||||
export { init as initVariationOptions } from './variation-options';
|
||||
export { init as initNotice } from './notice';
|
||||
export { init as initTaxonomy } from './taxonomy';
|
||||
|
|
|
@ -18,3 +18,4 @@
|
|||
@import 'password/editor.scss';
|
||||
@import 'variation-items/editor.scss';
|
||||
@import 'variation-options/editor.scss';
|
||||
@import 'taxonomy/editor.scss';
|
||||
|
|
|
@ -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 );
|
||||
```
|
|
@ -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"
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
} );
|
|
@ -0,0 +1,9 @@
|
|||
export interface Taxonomy {
|
||||
id: number;
|
||||
name: string;
|
||||
parent: number;
|
||||
}
|
||||
|
||||
export interface TaxonomyMetadata {
|
||||
hierarchical: boolean;
|
||||
}
|
|
@ -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;
|
|
@ -2,7 +2,6 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { ProductVariation } from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { Dropdown, MenuGroup, MenuItem } from '@wordpress/components';
|
||||
import { createElement } from '@wordpress/element';
|
||||
|
@ -14,23 +13,18 @@ import { chevronRight } from '@wordpress/icons';
|
|||
import { TRACKS_SOURCE } from '../../../constants';
|
||||
import { PRODUCT_STOCK_STATUS_KEYS } from '../../../utils/get-product-stock-status';
|
||||
import { UpdateStockMenuItem } from '../update-stock-menu-item';
|
||||
|
||||
export type InventoryMenuItemProps = {
|
||||
variation: ProductVariation;
|
||||
handlePrompt(
|
||||
label?: string,
|
||||
parser?: ( value: string ) => Partial< ProductVariation > | null
|
||||
): void;
|
||||
onChange( values: Partial< ProductVariation > ): void;
|
||||
onClose(): void;
|
||||
};
|
||||
import { VariationActionsMenuItemProps } from '../types';
|
||||
import { handlePrompt } from '../../../utils/handle-prompt';
|
||||
|
||||
export function InventoryMenuItem( {
|
||||
variation,
|
||||
handlePrompt,
|
||||
selection,
|
||||
onChange,
|
||||
onClose,
|
||||
}: InventoryMenuItemProps ) {
|
||||
}: VariationActionsMenuItemProps ) {
|
||||
const ids = Array.isArray( selection )
|
||||
? selection.map( ( { id } ) => id )
|
||||
: selection.id;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
position="middle right"
|
||||
|
@ -41,7 +35,7 @@ export function InventoryMenuItem( {
|
|||
'product_variations_menu_inventory_click',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
onToggle();
|
||||
|
@ -57,7 +51,7 @@ export function InventoryMenuItem( {
|
|||
<div className="components-dropdown-menu__menu">
|
||||
<MenuGroup>
|
||||
<UpdateStockMenuItem
|
||||
selection={ variation }
|
||||
selection={ selection }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
|
@ -68,12 +62,23 @@ export function InventoryMenuItem( {
|
|||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'manage_stock_toggle',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
onChange( {
|
||||
manage_stock: ! variation.manage_stock,
|
||||
} );
|
||||
if ( Array.isArray( selection ) ) {
|
||||
onChange(
|
||||
selection.map(
|
||||
( { id, manage_stock } ) => ( {
|
||||
id,
|
||||
manage_stock: ! manage_stock,
|
||||
} )
|
||||
)
|
||||
);
|
||||
} else {
|
||||
onChange( {
|
||||
manage_stock: ! selection.manage_stock,
|
||||
} );
|
||||
}
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
|
@ -86,14 +91,25 @@ export function InventoryMenuItem( {
|
|||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'set_status_in_stock',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
onChange( {
|
||||
stock_status:
|
||||
PRODUCT_STOCK_STATUS_KEYS.instock,
|
||||
manage_stock: false,
|
||||
} );
|
||||
if ( Array.isArray( selection ) ) {
|
||||
onChange(
|
||||
selection.map( ( { id } ) => ( {
|
||||
id,
|
||||
stock_status:
|
||||
PRODUCT_STOCK_STATUS_KEYS.instock,
|
||||
manage_stock: false,
|
||||
} ) )
|
||||
);
|
||||
} else {
|
||||
onChange( {
|
||||
stock_status:
|
||||
PRODUCT_STOCK_STATUS_KEYS.instock,
|
||||
manage_stock: false,
|
||||
} );
|
||||
}
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
|
@ -106,14 +122,25 @@ export function InventoryMenuItem( {
|
|||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'set_status_out_of_stock',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
onChange( {
|
||||
stock_status:
|
||||
PRODUCT_STOCK_STATUS_KEYS.outofstock,
|
||||
manage_stock: false,
|
||||
} );
|
||||
if ( Array.isArray( selection ) ) {
|
||||
onChange(
|
||||
selection.map( ( { id } ) => ( {
|
||||
id,
|
||||
stock_status:
|
||||
PRODUCT_STOCK_STATUS_KEYS.outofstock,
|
||||
manage_stock: false,
|
||||
} ) )
|
||||
);
|
||||
} else {
|
||||
onChange( {
|
||||
stock_status:
|
||||
PRODUCT_STOCK_STATUS_KEYS.outofstock,
|
||||
manage_stock: false,
|
||||
} );
|
||||
}
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
|
@ -129,14 +156,25 @@ export function InventoryMenuItem( {
|
|||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'set_status_on_back_order',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
onChange( {
|
||||
stock_status:
|
||||
PRODUCT_STOCK_STATUS_KEYS.onbackorder,
|
||||
manage_stock: false,
|
||||
} );
|
||||
if ( Array.isArray( selection ) ) {
|
||||
onChange(
|
||||
selection.map( ( { id } ) => ( {
|
||||
id,
|
||||
stock_status:
|
||||
PRODUCT_STOCK_STATUS_KEYS.onbackorder,
|
||||
manage_stock: false,
|
||||
} ) )
|
||||
);
|
||||
} else {
|
||||
onChange( {
|
||||
stock_status:
|
||||
PRODUCT_STOCK_STATUS_KEYS.onbackorder,
|
||||
manage_stock: false,
|
||||
} );
|
||||
}
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
|
@ -152,26 +190,40 @@ export function InventoryMenuItem( {
|
|||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'low_stock_amount_set',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
handlePrompt( undefined, ( value ) => {
|
||||
recordEvent(
|
||||
'product_variations_menu_inventory_select',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'low_stock_amount_set',
|
||||
variation_id: variation.id,
|
||||
handlePrompt( {
|
||||
onOk( value ) {
|
||||
recordEvent(
|
||||
'product_variations_menu_inventory_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'low_stock_amount_set',
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
const lowStockAmount = Number( value );
|
||||
if ( Number.isNaN( lowStockAmount ) ) {
|
||||
return null;
|
||||
}
|
||||
);
|
||||
const lowStockAmount = Number( value );
|
||||
if ( Number.isNaN( lowStockAmount ) ) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
low_stock_amount: lowStockAmount,
|
||||
manage_stock: true,
|
||||
};
|
||||
if ( Array.isArray( selection ) ) {
|
||||
onChange(
|
||||
selection.map( ( { id } ) => ( {
|
||||
id,
|
||||
low_stock_amount:
|
||||
lowStockAmount,
|
||||
manage_stock: true,
|
||||
} ) )
|
||||
);
|
||||
} else {
|
||||
onChange( {
|
||||
low_stock_amount:
|
||||
lowStockAmount,
|
||||
manage_stock: true,
|
||||
} );
|
||||
}
|
||||
},
|
||||
} );
|
||||
onClose();
|
||||
} }
|
||||
|
|
|
@ -5,19 +5,48 @@ import { Dropdown, MenuItem } from '@wordpress/components';
|
|||
import { createElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { chevronRight } from '@wordpress/icons';
|
||||
import { ProductVariation } from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ShippingMenuItemProps } from './types';
|
||||
import { TRACKS_SOURCE } from '../../../constants';
|
||||
import { VariationActionsMenuItemProps } from '../types';
|
||||
import { handlePrompt } from '../../../utils/handle-prompt';
|
||||
|
||||
export function ShippingMenuItem( {
|
||||
variation,
|
||||
handlePrompt,
|
||||
selection,
|
||||
onChange,
|
||||
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 (
|
||||
<Dropdown
|
||||
position="middle right"
|
||||
|
@ -26,7 +55,7 @@ export function ShippingMenuItem( {
|
|||
onClick={ () => {
|
||||
recordEvent( 'product_variations_menu_shipping_click', {
|
||||
source: TRACKS_SOURCE,
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
} );
|
||||
onToggle();
|
||||
} }
|
||||
|
@ -46,24 +75,23 @@ export function ShippingMenuItem( {
|
|||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'dimensions_length_set',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
handlePrompt( undefined, ( value ) => {
|
||||
recordEvent(
|
||||
'product_variations_menu_shipping_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'dimensions_length_set',
|
||||
variation_id: variation.id,
|
||||
}
|
||||
);
|
||||
return {
|
||||
dimensions: {
|
||||
...variation.dimensions,
|
||||
handlePrompt( {
|
||||
onOk( value ) {
|
||||
recordEvent(
|
||||
'product_variations_menu_shipping_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'dimensions_length_set',
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
handleDimensionsChange( {
|
||||
length: value,
|
||||
},
|
||||
};
|
||||
} );
|
||||
},
|
||||
} );
|
||||
onClose();
|
||||
} }
|
||||
|
@ -77,24 +105,23 @@ export function ShippingMenuItem( {
|
|||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'dimensions_width_set',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
handlePrompt( undefined, ( value ) => {
|
||||
recordEvent(
|
||||
'product_variations_menu_shipping_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'dimensions_width_set',
|
||||
variation_id: variation.id,
|
||||
}
|
||||
);
|
||||
return {
|
||||
dimensions: {
|
||||
...variation.dimensions,
|
||||
handlePrompt( {
|
||||
onOk( value ) {
|
||||
recordEvent(
|
||||
'product_variations_menu_shipping_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'dimensions_width_set',
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
handleDimensionsChange( {
|
||||
width: value,
|
||||
},
|
||||
};
|
||||
} );
|
||||
},
|
||||
} );
|
||||
onClose();
|
||||
} }
|
||||
|
@ -108,24 +135,23 @@ export function ShippingMenuItem( {
|
|||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'dimensions_height_set',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
handlePrompt( undefined, ( value ) => {
|
||||
recordEvent(
|
||||
'product_variations_menu_shipping_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'dimensions_height_set',
|
||||
variation_id: variation.id,
|
||||
}
|
||||
);
|
||||
return {
|
||||
dimensions: {
|
||||
...variation.dimensions,
|
||||
handlePrompt( {
|
||||
onOk( value ) {
|
||||
recordEvent(
|
||||
'product_variations_menu_shipping_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'dimensions_height_set',
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
handleDimensionsChange( {
|
||||
height: value,
|
||||
},
|
||||
};
|
||||
} );
|
||||
},
|
||||
} );
|
||||
onClose();
|
||||
} }
|
||||
|
@ -139,19 +165,30 @@ export function ShippingMenuItem( {
|
|||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'weight_set',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
handlePrompt( undefined, ( value ) => {
|
||||
recordEvent(
|
||||
'product_variations_menu_shipping_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'weight_set',
|
||||
variation_id: variation.id,
|
||||
handlePrompt( {
|
||||
onOk( value ) {
|
||||
recordEvent(
|
||||
'product_variations_menu_shipping_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
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();
|
||||
} }
|
||||
|
|
|
@ -5,7 +5,6 @@ import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components';
|
|||
import { createElement, Fragment } from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { moreVertical } from '@wordpress/icons';
|
||||
import { ProductVariation } from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
|
@ -22,21 +21,6 @@ export function VariationActionsMenu( {
|
|||
onChange,
|
||||
onDelete,
|
||||
}: 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 (
|
||||
<DropdownMenu
|
||||
icon={ moreVertical }
|
||||
|
@ -78,14 +62,13 @@ export function VariationActionsMenu( {
|
|||
onClose={ onClose }
|
||||
/>
|
||||
<InventoryMenuItem
|
||||
variation={ selection }
|
||||
handlePrompt={ handlePrompt }
|
||||
selection={ selection }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
<ShippingMenuItem
|
||||
variation={ selection }
|
||||
handlePrompt={ handlePrompt }
|
||||
selection={ selection }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
</MenuGroup>
|
||||
|
|
|
@ -13,6 +13,8 @@ import { VariationsActionsMenuProps } from './types';
|
|||
import { UpdateStockMenuItem } from '../update-stock-menu-item';
|
||||
import { PricingMenuItem } from '../pricing-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( {
|
||||
selection,
|
||||
|
@ -22,7 +24,7 @@ export function VariationsActionsMenu( {
|
|||
}: VariationsActionsMenuProps ) {
|
||||
return (
|
||||
<Dropdown
|
||||
position="bottom right"
|
||||
position="bottom left"
|
||||
renderToggle={ ( { isOpen, onToggle } ) => (
|
||||
<Button
|
||||
disabled={ disabled }
|
||||
|
@ -55,6 +57,16 @@ export function VariationsActionsMenu( {
|
|||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
<InventoryMenuItem
|
||||
selection={ selection }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
<ShippingMenuItem
|
||||
selection={ selection }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<MenuItem
|
||||
|
|
|
@ -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).
|
||||
|
||||
### SqlQuery Class
|
||||
## SqlQuery Class
|
||||
|
||||
The `SqlQuery` class is a SQL Query statement object. Its properties consist of
|
||||
|
||||
- A `context` string identifying the context of the query.
|
||||
- SQL clause (`type`) string arrays used to construct the SQL statement:
|
||||
- `select`
|
||||
- `from`
|
||||
- `right_join`
|
||||
- `join`
|
||||
- `left_join`
|
||||
- `where`
|
||||
- `where_time`
|
||||
- `group_by`
|
||||
- `having`
|
||||
- `order_by`
|
||||
- `limit`
|
||||
- `select`
|
||||
- `from`
|
||||
- `right_join`
|
||||
- `join`
|
||||
- `left_join`
|
||||
- `where`
|
||||
- `where_time`
|
||||
- `group_by`
|
||||
- `having`
|
||||
- `order_by`
|
||||
- `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:
|
||||
|
||||
|
@ -49,7 +48,7 @@ Query contexts are named as follows:
|
|||
- Interval Query = Class Context + `_interval`
|
||||
- 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:
|
||||
|
||||
|
@ -64,13 +63,13 @@ When getting the full statement the clause arrays are passed through two filters
|
|||
|
||||
The filters are:
|
||||
|
||||
- `apply_filters( "wc_admin_clauses_{$type}", $clauses, $context );`
|
||||
- `apply_filters( "wc_admin_clauses_{$type}_{$context}", $clauses );`
|
||||
- `apply_filters( "woocommerce_analytics_clauses_{$type}", $clauses, $context );`
|
||||
- `apply_filters( "woocommerce_analytics_clauses_{$type}_{$context}", $clauses );`
|
||||
|
||||
Example usage
|
||||
|
||||
```
|
||||
add_filter( 'wc_admin_clauses_product_stats_select_total', 'my_custom_product_stats' );
|
||||
```php
|
||||
add_filter( 'woocommerce_analytics_clauses_product_stats_select_total', 'my_custom_product_stats' );
|
||||
/**
|
||||
* Add sample data to product stats totals.
|
||||
*
|
||||
|
|
|
@ -217,6 +217,12 @@ const webpackConfig = {
|
|||
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' ) ) {
|
||||
// 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.
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
FIx WC Admin pages are empty for WP 6.2 and below.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
update the SqlQuery filter prefix in data.md
|
Loading…
Reference in New Issue