Merge branch 'trunk' into issue-37835
This commit is contained in:
commit
16819d18ac
|
@ -1,5 +1,11 @@
|
|||
== Changelog ==
|
||||
|
||||
= 8.0.3 2023-08-29 =
|
||||
|
||||
* Update - Bump WooCommerce Blocks to 10.6.6. [#39853](https://github.com/woocommerce/woocommerce/pull/39853)
|
||||
* Fix - Avoid extra queries when a WooPayments incentive has been dismissed. [#39882](https://github.com/woocommerce/woocommerce/pull/39882)
|
||||
|
||||
|
||||
= 8.0.2 2023-08-15 =
|
||||
|
||||
* Fix - Fix an issue which was causing some attributes to default to a minimum length of 3. [#39686](https://github.com/woocommerce/woocommerce/pull/39686)
|
||||
|
|
|
@ -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
|
||||
|
||||
Refactor Pagination component and split out into multiple re-usable components. Also added a `usePagination` hook.
|
|
@ -27,7 +27,7 @@ export { MediaUploader } from './media-uploader';
|
|||
export { default as MenuItem } from './ellipsis-menu/menu-item';
|
||||
export { default as MenuTitle } from './ellipsis-menu/menu-title';
|
||||
export { default as OrderStatus } from './order-status';
|
||||
export { default as Pagination } from './pagination';
|
||||
export * from './pagination';
|
||||
export { default as Pill } from './pill';
|
||||
export { default as Plugins } from './plugins';
|
||||
export { default as ProductImage } from './product-image';
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
Pagination
|
||||
===
|
||||
# Pagination
|
||||
|
||||
Use `Pagination` to allow navigation between pages that represent a collection of items.
|
||||
The component allows for selecting a new page and items per page options.
|
||||
|
||||
You can also make use of the `usePagination` hook and the custom components:
|
||||
|
||||
- `PaginationPageArrowsWithPicker`
|
||||
- `PaginationPageArrows`
|
||||
- `PaginationPagePicker`
|
||||
- `PaginationPageSizePicker`
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
|
@ -16,16 +22,41 @@ The component allows for selecting a new page and items per page options.
|
|||
/>
|
||||
```
|
||||
|
||||
## Custom Example
|
||||
|
||||
```jsx
|
||||
const paginationProps = usePagination( {
|
||||
totalCount: 200,
|
||||
defaultPerPage: 25,
|
||||
} );
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
Viewing { paginationProps.start }-{ paginationProps.end } of 200
|
||||
items
|
||||
</div>
|
||||
<PaginationPageArrowsWithPicker { ...paginationProps } />
|
||||
<PaginationPageSizePicker
|
||||
{ ...paginationProps }
|
||||
total={ 200 }
|
||||
perPageOptions={ [ 5, 10, 25 ] }
|
||||
label=""
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
Name | Type | Default | Description
|
||||
--- | --- | --- | ---
|
||||
`page` | Number | `null` | (required) The current page of the collection
|
||||
`onPageChange` | Function | `noop` | A function to execute when the page is changed
|
||||
`perPage` | Number | `null` | (required) The amount of results that are being displayed per page
|
||||
`onPerPageChange` | Function | `noop` | A function to execute when the per page option is changed
|
||||
`total` | Number | `null` | (required) The total number of results
|
||||
`className` | String | `null` | Additional classNames
|
||||
`showPagePicker` | Boolean | `true` | Whether the page picker should be shown.
|
||||
`showPerPagePicker` | Boolean | `true` | Whether the per page picker should shown.
|
||||
`showPageArrowsLabel` | Boolean | `true` | Whether the page arrows label should be shown.
|
||||
| Name | Type | Default | Description |
|
||||
| --------------------- | -------- | ------- | ------------------------------------------------------------------ |
|
||||
| `page` | Number | `null` | (required) The current page of the collection |
|
||||
| `onPageChange` | Function | `noop` | A function to execute when the page is changed |
|
||||
| `perPage` | Number | `null` | (required) The amount of results that are being displayed per page |
|
||||
| `onPerPageChange` | Function | `noop` | A function to execute when the per page option is changed |
|
||||
| `total` | Number | `null` | (required) The total number of results |
|
||||
| `className` | String | `null` | Additional classNames |
|
||||
| `showPagePicker` | Boolean | `true` | Whether the page picker should be shown. |
|
||||
| `showPerPagePicker` | Boolean | `true` | Whether the per page picker should shown. |
|
||||
| `showPageArrowsLabel` | Boolean | `true` | Whether the page arrows label should be shown. |
|
||||
|
|
|
@ -1,269 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { createElement, Component } from '@wordpress/element';
|
||||
import { Button, SelectControl } from '@wordpress/components';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { noop, uniqueId } from 'lodash';
|
||||
import { Icon, chevronLeft, chevronRight } from '@wordpress/icons';
|
||||
|
||||
const PER_PAGE_OPTIONS = [ 25, 50, 75, 100 ];
|
||||
|
||||
/**
|
||||
* Use `Pagination` to allow navigation between pages that represent a collection of items.
|
||||
* The component allows for selecting a new page and items per page options.
|
||||
*/
|
||||
class Pagination extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
|
||||
this.state = {
|
||||
inputValue: this.props.page,
|
||||
};
|
||||
|
||||
this.previousPage = this.previousPage.bind( this );
|
||||
this.nextPage = this.nextPage.bind( this );
|
||||
this.onInputChange = this.onInputChange.bind( this );
|
||||
this.onInputBlur = this.onInputBlur.bind( this );
|
||||
this.perPageChange = this.perPageChange.bind( this );
|
||||
this.selectInputValue = this.selectInputValue.bind( this );
|
||||
}
|
||||
|
||||
previousPage( event ) {
|
||||
event.stopPropagation();
|
||||
const { page, onPageChange } = this.props;
|
||||
if ( page - 1 < 1 ) {
|
||||
return;
|
||||
}
|
||||
onPageChange( page - 1, 'previous' );
|
||||
}
|
||||
|
||||
nextPage( event ) {
|
||||
event.stopPropagation();
|
||||
const { page, onPageChange } = this.props;
|
||||
if ( page + 1 > this.pageCount ) {
|
||||
return;
|
||||
}
|
||||
onPageChange( page + 1, 'next' );
|
||||
}
|
||||
|
||||
perPageChange( perPage ) {
|
||||
const { onPerPageChange, onPageChange, total, page } = this.props;
|
||||
|
||||
onPerPageChange( parseInt( perPage, 10 ) );
|
||||
const newMaxPage = Math.ceil( total / parseInt( perPage, 10 ) );
|
||||
if ( page > newMaxPage ) {
|
||||
onPageChange( newMaxPage );
|
||||
}
|
||||
}
|
||||
|
||||
onInputChange( event ) {
|
||||
this.setState( {
|
||||
inputValue: event.target.value,
|
||||
} );
|
||||
}
|
||||
|
||||
onInputBlur( event ) {
|
||||
const { onPageChange, page } = this.props;
|
||||
const newPage = parseInt( event.target.value, 10 );
|
||||
|
||||
if (
|
||||
newPage !== page &&
|
||||
Number.isFinite( newPage ) &&
|
||||
newPage > 0 &&
|
||||
this.pageCount &&
|
||||
this.pageCount >= newPage
|
||||
) {
|
||||
onPageChange( newPage, 'goto' );
|
||||
}
|
||||
}
|
||||
|
||||
selectInputValue( event ) {
|
||||
event.target.select();
|
||||
}
|
||||
|
||||
renderPageArrows() {
|
||||
const { page, showPageArrowsLabel } = this.props;
|
||||
|
||||
if ( this.pageCount <= 1 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previousLinkClass = classNames( 'woocommerce-pagination__link', {
|
||||
'is-active': page > 1,
|
||||
} );
|
||||
|
||||
const nextLinkClass = classNames( 'woocommerce-pagination__link', {
|
||||
'is-active': page < this.pageCount,
|
||||
} );
|
||||
|
||||
return (
|
||||
<div className="woocommerce-pagination__page-arrows">
|
||||
{ showPageArrowsLabel && (
|
||||
<span
|
||||
className="woocommerce-pagination__page-arrows-label"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
{ sprintf(
|
||||
__( 'Page %d of %d', 'woocommerce' ),
|
||||
page,
|
||||
this.pageCount
|
||||
) }
|
||||
</span>
|
||||
) }
|
||||
<div className="woocommerce-pagination__page-arrows-buttons">
|
||||
<Button
|
||||
className={ previousLinkClass }
|
||||
disabled={ ! ( page > 1 ) }
|
||||
onClick={ this.previousPage }
|
||||
label={ __( 'Previous Page', 'woocommerce' ) }
|
||||
>
|
||||
<Icon icon={ chevronLeft } />
|
||||
</Button>
|
||||
<Button
|
||||
className={ nextLinkClass }
|
||||
disabled={ ! ( page < this.pageCount ) }
|
||||
onClick={ this.nextPage }
|
||||
label={ __( 'Next Page', 'woocommerce' ) }
|
||||
>
|
||||
<Icon icon={ chevronRight } />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderPagePicker() {
|
||||
const { page } = this.props;
|
||||
const { inputValue } = this.state;
|
||||
const isError = page < 1 || page > this.pageCount;
|
||||
const inputClass = classNames(
|
||||
'woocommerce-pagination__page-picker-input',
|
||||
{
|
||||
'has-error': isError,
|
||||
}
|
||||
);
|
||||
|
||||
const instanceId = uniqueId( 'woocommerce-pagination-page-picker-' );
|
||||
return (
|
||||
<div className="woocommerce-pagination__page-picker">
|
||||
<label
|
||||
htmlFor={ instanceId }
|
||||
className="woocommerce-pagination__page-picker-label"
|
||||
>
|
||||
{ __( 'Go to page', 'woocommerce' ) }
|
||||
<input
|
||||
id={ instanceId }
|
||||
className={ inputClass }
|
||||
aria-invalid={ isError }
|
||||
type="number"
|
||||
onClick={ this.selectInputValue }
|
||||
onChange={ this.onInputChange }
|
||||
onBlur={ this.onInputBlur }
|
||||
value={ inputValue }
|
||||
min={ 1 }
|
||||
max={ this.pageCount }
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderPerPagePicker() {
|
||||
// @todo Replace this with a styleized Select drop-down/control?
|
||||
const pickerOptions = PER_PAGE_OPTIONS.map( ( option ) => {
|
||||
return { value: option, label: option };
|
||||
} );
|
||||
|
||||
return (
|
||||
<div className="woocommerce-pagination__per-page-picker">
|
||||
<SelectControl
|
||||
label={ __( 'Rows per page', 'woocommerce' ) }
|
||||
labelPosition="side"
|
||||
value={ this.props.perPage }
|
||||
onChange={ this.perPageChange }
|
||||
options={ pickerOptions }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { total, perPage, className, showPagePicker, showPerPagePicker } =
|
||||
this.props;
|
||||
this.pageCount = Math.ceil( total / perPage );
|
||||
|
||||
const classes = classNames( 'woocommerce-pagination', className );
|
||||
|
||||
if ( this.pageCount <= 1 ) {
|
||||
return (
|
||||
( total > PER_PAGE_OPTIONS[ 0 ] && (
|
||||
<div className={ classes }>
|
||||
{ this.renderPerPagePicker() }
|
||||
</div>
|
||||
) ) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ classes }>
|
||||
{ this.renderPageArrows() }
|
||||
{ showPagePicker && this.renderPagePicker() }
|
||||
{ showPerPagePicker && this.renderPerPagePicker() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Pagination.propTypes = {
|
||||
/**
|
||||
* The current page of the collection.
|
||||
*/
|
||||
page: PropTypes.number.isRequired,
|
||||
/**
|
||||
* A function to execute when the page is changed.
|
||||
*/
|
||||
onPageChange: PropTypes.func,
|
||||
/**
|
||||
* The amount of results that are being displayed per page.
|
||||
*/
|
||||
perPage: PropTypes.number.isRequired,
|
||||
/**
|
||||
* A function to execute when the per page option is changed.
|
||||
*/
|
||||
onPerPageChange: PropTypes.func,
|
||||
/**
|
||||
* The total number of results.
|
||||
*/
|
||||
total: PropTypes.number.isRequired,
|
||||
/**
|
||||
* Additional classNames.
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
/**
|
||||
* Whether the page picker should be rendered.
|
||||
*/
|
||||
showPagePicker: PropTypes.bool,
|
||||
/**
|
||||
* Whether the perPage picker should be rendered.
|
||||
*/
|
||||
showPerPagePicker: PropTypes.bool,
|
||||
/**
|
||||
* Whether the page arrows label should be rendered.
|
||||
*/
|
||||
showPageArrowsLabel: PropTypes.bool,
|
||||
};
|
||||
|
||||
Pagination.defaultProps = {
|
||||
onPageChange: noop,
|
||||
onPerPageChange: noop,
|
||||
showPagePicker: true,
|
||||
showPerPagePicker: true,
|
||||
showPageArrowsLabel: true,
|
||||
};
|
||||
|
||||
export default Pagination;
|
|
@ -0,0 +1,6 @@
|
|||
export * from './pagination';
|
||||
export { PageArrows as PaginationPageArrows } from './page-arrows';
|
||||
export { PagePicker as PaginationPagePicker } from './page-picker';
|
||||
export { PageSizePicker as PaginationPageSizePicker } from './page-size-picker';
|
||||
export { PageArrowsWithPicker as PaginationPageArrowsWithPicker } from './page-arrows-with-picker';
|
||||
export * from './use-pagination';
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Button } from '@wordpress/components';
|
||||
import { createElement, useEffect, useState } from '@wordpress/element';
|
||||
import { chevronLeft, chevronRight } from '@wordpress/icons';
|
||||
import { sprintf, __ } from '@wordpress/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { uniqueId } from 'lodash';
|
||||
|
||||
type PageArrowsWithPickerProps = {
|
||||
currentPage: number;
|
||||
pageCount: number;
|
||||
setCurrentPage: (
|
||||
page: number,
|
||||
action?: 'previous' | 'next' | 'goto'
|
||||
) => void;
|
||||
};
|
||||
|
||||
export function PageArrowsWithPicker( {
|
||||
pageCount,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
}: PageArrowsWithPickerProps ) {
|
||||
const [ inputValue, setInputValue ] = useState( currentPage );
|
||||
|
||||
useEffect( () => {
|
||||
if ( currentPage !== inputValue ) {
|
||||
setInputValue( currentPage );
|
||||
}
|
||||
}, [ currentPage ] );
|
||||
|
||||
function onInputChange( event: React.FormEvent< HTMLInputElement > ) {
|
||||
setInputValue( parseInt( event.currentTarget.value, 10 ) );
|
||||
}
|
||||
|
||||
function onInputBlur( event: React.FocusEvent< HTMLInputElement > ) {
|
||||
const newPage = parseInt( event.target.value, 10 );
|
||||
|
||||
if (
|
||||
newPage !== currentPage &&
|
||||
Number.isFinite( newPage ) &&
|
||||
newPage > 0 &&
|
||||
pageCount &&
|
||||
pageCount >= newPage
|
||||
) {
|
||||
setCurrentPage( newPage, 'goto' );
|
||||
} else {
|
||||
setInputValue( currentPage );
|
||||
}
|
||||
}
|
||||
|
||||
function previousPage( event: React.MouseEvent ) {
|
||||
event.stopPropagation();
|
||||
if ( currentPage - 1 < 1 ) {
|
||||
return;
|
||||
}
|
||||
setInputValue( currentPage - 1 );
|
||||
setCurrentPage( currentPage - 1, 'previous' );
|
||||
}
|
||||
|
||||
function nextPage( event: React.MouseEvent ) {
|
||||
event.stopPropagation();
|
||||
if ( currentPage + 1 > pageCount ) {
|
||||
return;
|
||||
}
|
||||
setInputValue( currentPage + 1 );
|
||||
setCurrentPage( currentPage + 1, 'next' );
|
||||
}
|
||||
|
||||
if ( pageCount <= 1 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previousLinkClass = classNames( 'woocommerce-pagination__link', {
|
||||
'is-active': currentPage > 1,
|
||||
} );
|
||||
|
||||
const nextLinkClass = classNames( 'woocommerce-pagination__link', {
|
||||
'is-active': currentPage < pageCount,
|
||||
} );
|
||||
const isError = currentPage < 1 || currentPage > pageCount;
|
||||
const inputClass = classNames(
|
||||
'woocommerce-pagination__page-arrow-picker-input',
|
||||
{
|
||||
'has-error': isError,
|
||||
}
|
||||
);
|
||||
|
||||
const instanceId = uniqueId( 'woocommerce-pagination-page-picker-' );
|
||||
return (
|
||||
<div className="woocommerce-pagination__page-arrows">
|
||||
<Button
|
||||
className={ previousLinkClass }
|
||||
icon={ chevronLeft }
|
||||
disabled={ ! ( currentPage > 1 ) }
|
||||
onClick={ previousPage }
|
||||
label={ __( 'Previous Page', 'woocommerce' ) }
|
||||
/>
|
||||
<input
|
||||
id={ instanceId }
|
||||
className={ inputClass }
|
||||
aria-invalid={ isError }
|
||||
type="number"
|
||||
onChange={ onInputChange }
|
||||
onBlur={ onInputBlur }
|
||||
value={ inputValue }
|
||||
min={ 1 }
|
||||
max={ pageCount }
|
||||
/>
|
||||
{ sprintf( __( 'of %d', 'woocommerce' ), pageCount ) }
|
||||
<Button
|
||||
className={ nextLinkClass }
|
||||
icon={ chevronRight }
|
||||
disabled={ ! ( currentPage < pageCount ) }
|
||||
onClick={ nextPage }
|
||||
label={ __( 'Next Page', 'woocommerce' ) }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Button, Icon } from '@wordpress/components';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { chevronLeft, chevronRight } from '@wordpress/icons';
|
||||
import { sprintf, __ } from '@wordpress/i18n';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type PageArrowsProps = {
|
||||
currentPage: number;
|
||||
pageCount: number;
|
||||
showPageArrowsLabel?: boolean;
|
||||
setCurrentPage: (
|
||||
page: number,
|
||||
action?: 'previous' | 'next' | 'goto'
|
||||
) => void;
|
||||
};
|
||||
|
||||
export function PageArrows( {
|
||||
pageCount,
|
||||
currentPage,
|
||||
showPageArrowsLabel = true,
|
||||
setCurrentPage,
|
||||
}: PageArrowsProps ) {
|
||||
function previousPage( event: React.MouseEvent ) {
|
||||
event.stopPropagation();
|
||||
if ( currentPage - 1 < 1 ) {
|
||||
return;
|
||||
}
|
||||
setCurrentPage( currentPage - 1, 'previous' );
|
||||
}
|
||||
|
||||
function nextPage( event: React.MouseEvent ) {
|
||||
event.stopPropagation();
|
||||
if ( currentPage + 1 > pageCount ) {
|
||||
return;
|
||||
}
|
||||
setCurrentPage( currentPage + 1, 'next' );
|
||||
}
|
||||
|
||||
if ( pageCount <= 1 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previousLinkClass = classNames( 'woocommerce-pagination__link', {
|
||||
'is-active': currentPage > 1,
|
||||
} );
|
||||
|
||||
const nextLinkClass = classNames( 'woocommerce-pagination__link', {
|
||||
'is-active': currentPage < pageCount,
|
||||
} );
|
||||
|
||||
return (
|
||||
<div className="woocommerce-pagination__page-arrows">
|
||||
{ showPageArrowsLabel && (
|
||||
<span
|
||||
className="woocommerce-pagination__page-arrows-label"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
{ sprintf(
|
||||
__( 'Page %d of %d', 'woocommerce' ),
|
||||
currentPage,
|
||||
pageCount
|
||||
) }
|
||||
</span>
|
||||
) }
|
||||
<div className="woocommerce-pagination__page-arrows-buttons">
|
||||
<Button
|
||||
className={ previousLinkClass }
|
||||
disabled={ ! ( currentPage > 1 ) }
|
||||
onClick={ previousPage }
|
||||
label={ __( 'Previous Page', 'woocommerce' ) }
|
||||
>
|
||||
<Icon icon={ chevronLeft } />
|
||||
</Button>
|
||||
<Button
|
||||
className={ nextLinkClass }
|
||||
disabled={ ! ( currentPage < pageCount ) }
|
||||
onClick={ nextPage }
|
||||
label={ __( 'Next Page', 'woocommerce' ) }
|
||||
>
|
||||
<Icon icon={ chevronRight } />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement, useState } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { uniqueId } from 'lodash';
|
||||
|
||||
type PagePickerProps = {
|
||||
currentPage: number;
|
||||
pageCount: number;
|
||||
setCurrentPage: (
|
||||
page: number,
|
||||
action?: 'previous' | 'next' | 'goto'
|
||||
) => void;
|
||||
};
|
||||
|
||||
export function PagePicker( {
|
||||
pageCount,
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
}: PagePickerProps ) {
|
||||
const [ inputValue, setInputValue ] = useState( currentPage );
|
||||
|
||||
function onInputChange( event: React.FormEvent< HTMLInputElement > ) {
|
||||
setInputValue( parseInt( event.currentTarget.value, 10 ) );
|
||||
}
|
||||
|
||||
function onInputBlur( event: React.FocusEvent< HTMLInputElement > ) {
|
||||
const newPage = parseInt( event.target.value, 10 );
|
||||
|
||||
if (
|
||||
newPage !== currentPage &&
|
||||
Number.isFinite( newPage ) &&
|
||||
newPage > 0 &&
|
||||
pageCount &&
|
||||
pageCount >= newPage
|
||||
) {
|
||||
setCurrentPage( newPage, 'goto' );
|
||||
}
|
||||
}
|
||||
|
||||
function selectInputValue( event: React.MouseEvent< HTMLInputElement > ) {
|
||||
event.currentTarget.select();
|
||||
}
|
||||
|
||||
const isError = currentPage < 1 || currentPage > pageCount;
|
||||
const inputClass = classNames(
|
||||
'woocommerce-pagination__page-picker-input',
|
||||
{
|
||||
'has-error': isError,
|
||||
}
|
||||
);
|
||||
|
||||
const instanceId = uniqueId( 'woocommerce-pagination-page-picker-' );
|
||||
return (
|
||||
<div className="woocommerce-pagination__page-picker">
|
||||
<label
|
||||
htmlFor={ instanceId }
|
||||
className="woocommerce-pagination__page-picker-label"
|
||||
>
|
||||
{ __( 'Go to page', 'woocommerce' ) }
|
||||
<input
|
||||
id={ instanceId }
|
||||
className={ inputClass }
|
||||
aria-invalid={ isError }
|
||||
type="number"
|
||||
onClick={ selectInputValue }
|
||||
onChange={ onInputChange }
|
||||
onBlur={ onInputBlur }
|
||||
value={ inputValue }
|
||||
min={ 1 }
|
||||
max={ pageCount }
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { SelectControl } from '@wordpress/components';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export const DEFAULT_PER_PAGE_OPTIONS = [ 25, 50, 75, 100 ];
|
||||
|
||||
type PageSizePickerProps = {
|
||||
currentPage: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
setCurrentPage: (
|
||||
page: number,
|
||||
action?: 'previous' | 'next' | 'goto'
|
||||
) => void;
|
||||
setPerPageChange?: ( perPage: number ) => void;
|
||||
perPageOptions?: number[];
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export function PageSizePicker( {
|
||||
perPage,
|
||||
currentPage,
|
||||
total,
|
||||
setCurrentPage,
|
||||
setPerPageChange = () => {},
|
||||
perPageOptions = DEFAULT_PER_PAGE_OPTIONS,
|
||||
label = __( 'Rows per page', 'woocommerce' ),
|
||||
}: PageSizePickerProps ) {
|
||||
function perPageChange( newPerPage: string ) {
|
||||
setPerPageChange( parseInt( newPerPage, 10 ) );
|
||||
const newMaxPage = Math.ceil( total / parseInt( newPerPage, 10 ) );
|
||||
if ( currentPage > newMaxPage ) {
|
||||
setCurrentPage( newMaxPage );
|
||||
}
|
||||
}
|
||||
|
||||
// @todo Replace this with a styleized Select drop-down/control?
|
||||
const pickerOptions = perPageOptions.map( ( option ) => {
|
||||
return { value: option.toString(), label: option.toString() };
|
||||
} );
|
||||
|
||||
return (
|
||||
<div className="woocommerce-pagination__per-page-picker">
|
||||
<SelectControl
|
||||
label={ label }
|
||||
// @ts-expect-error outdated types file.
|
||||
labelPosition="side"
|
||||
value={ perPage.toString() }
|
||||
onChange={ perPageChange }
|
||||
options={ pickerOptions }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement } from '@wordpress/element';
|
||||
import classNames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { PageArrows } from './page-arrows';
|
||||
import { PagePicker } from './page-picker';
|
||||
import { DEFAULT_PER_PAGE_OPTIONS, PageSizePicker } from './page-size-picker';
|
||||
|
||||
export type PaginationProps = {
|
||||
page: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
onPageChange?: (
|
||||
page: number,
|
||||
action?: 'previous' | 'next' | 'goto'
|
||||
) => void;
|
||||
onPerPageChange?: ( perPage: number ) => void;
|
||||
className?: string;
|
||||
showPagePicker?: boolean;
|
||||
showPerPagePicker?: boolean;
|
||||
showPageArrowsLabel?: boolean;
|
||||
perPageOptions?: number[];
|
||||
children?: ( props: { pageCount: number } ) => JSX.Element;
|
||||
};
|
||||
|
||||
export function Pagination( {
|
||||
page,
|
||||
onPageChange = () => {},
|
||||
total,
|
||||
perPage,
|
||||
onPerPageChange = () => {},
|
||||
showPagePicker = true,
|
||||
showPerPagePicker = true,
|
||||
showPageArrowsLabel = true,
|
||||
className,
|
||||
perPageOptions = DEFAULT_PER_PAGE_OPTIONS,
|
||||
children,
|
||||
}: PaginationProps ): JSX.Element | null {
|
||||
const pageCount = Math.ceil( total / perPage );
|
||||
|
||||
if ( children && typeof children === 'function' ) {
|
||||
return children( {
|
||||
pageCount,
|
||||
} );
|
||||
}
|
||||
|
||||
const classes = classNames( 'woocommerce-pagination', className );
|
||||
|
||||
if ( pageCount <= 1 ) {
|
||||
return (
|
||||
( total > perPageOptions[ 0 ] && (
|
||||
<div className={ classes }>
|
||||
<PageSizePicker
|
||||
currentPage={ page }
|
||||
perPage={ perPage }
|
||||
setCurrentPage={ onPageChange }
|
||||
total={ total }
|
||||
setPerPageChange={ onPerPageChange }
|
||||
perPageOptions={ perPageOptions }
|
||||
/>
|
||||
</div>
|
||||
) ) ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ classes }>
|
||||
<PageArrows
|
||||
currentPage={ page }
|
||||
pageCount={ pageCount }
|
||||
showPageArrowsLabel={ showPageArrowsLabel }
|
||||
setCurrentPage={ onPageChange }
|
||||
/>
|
||||
{ showPagePicker && (
|
||||
<PagePicker
|
||||
currentPage={ page }
|
||||
pageCount={ pageCount }
|
||||
setCurrentPage={ onPageChange }
|
||||
/>
|
||||
) }
|
||||
{ showPerPagePicker && (
|
||||
<PageSizePicker
|
||||
currentPage={ page }
|
||||
perPage={ perPage }
|
||||
setCurrentPage={ onPageChange }
|
||||
total={ total }
|
||||
setPerPageChange={ onPerPageChange }
|
||||
perPageOptions={ perPageOptions }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -7,7 +7,12 @@ import { createElement, useState } from '@wordpress/element';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Pagination from '../';
|
||||
import {
|
||||
Pagination,
|
||||
PaginationPageArrowsWithPicker,
|
||||
usePagination,
|
||||
PaginationPageSizePicker,
|
||||
} from '../';
|
||||
|
||||
export default {
|
||||
title: 'WooCommerce Admin/components/Pagination',
|
||||
|
@ -18,6 +23,10 @@ export default {
|
|||
showPerPagePicker: true,
|
||||
showPageArrowsLabel: true,
|
||||
},
|
||||
argTypes: {
|
||||
onPageChange: { action: 'onPageChange' },
|
||||
onPerPageChange: { action: 'onPerPageChange' },
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = ( args ) => {
|
||||
|
@ -34,3 +43,28 @@ export const Default = ( args ) => {
|
|||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomWithHook = ( args ) => {
|
||||
const paginationProps = usePagination( {
|
||||
totalCount: args.total,
|
||||
defaultPerPage: 25,
|
||||
onPageChange: args.onPageChange,
|
||||
onPerPageChange: args.onPerPageChange,
|
||||
} );
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
Viewing { paginationProps.start }-{ paginationProps.end } of{ ' ' }
|
||||
{ args.total } items
|
||||
</div>
|
||||
<PaginationPageArrowsWithPicker { ...paginationProps } />
|
||||
<PaginationPageSizePicker
|
||||
{ ...paginationProps }
|
||||
total={ args.total }
|
||||
perPageOptions={ [ 5, 10, 25 ] }
|
||||
label=""
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@include breakpoint( '<782px' ) {
|
||||
@include breakpoint("<782px") {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,8 @@
|
|||
.woocommerce-pagination__page-arrows {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: $gap-smaller;
|
||||
}
|
||||
|
||||
.woocommerce-pagination__page-arrows-buttons {
|
||||
|
@ -33,7 +35,7 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.components-button:not(:disabled):not([aria-disabled='true']):hover {
|
||||
.components-button:not(:disabled):not([aria-disabled="true"]):hover {
|
||||
color: $gray-700;
|
||||
}
|
||||
|
||||
|
@ -61,7 +63,7 @@
|
|||
.woocommerce-pagination__page-picker {
|
||||
margin-left: $spacing;
|
||||
|
||||
@include breakpoint( '<782px' ) {
|
||||
@include breakpoint("<782px") {
|
||||
margin-top: $gap;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
@ -76,7 +78,7 @@
|
|||
.woocommerce-pagination__per-page-picker {
|
||||
margin-left: $spacing;
|
||||
|
||||
@include breakpoint( '<782px' ) {
|
||||
@include breakpoint("<782px") {
|
||||
margin-top: $gap;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
@ -110,7 +112,23 @@
|
|||
}
|
||||
|
||||
.woocommerce-pagination__page-picker-input.has-error,
|
||||
.woocommerce-pagination__page-picker-input.has-error:focus {
|
||||
.woocommerce-pagination__page-picker-input.has-error:focus,
|
||||
.woocommerce-pagination__page-arrow-picker-input.has-error,
|
||||
.woocommerce-pagination__page-arrow-picker-input.has-error:focus {
|
||||
border-color: $error-red;
|
||||
box-shadow: 0 0 2px $error-red;
|
||||
}
|
||||
|
||||
.woocommerce-pagination__page-arrow-picker-input {
|
||||
/* Chrome, Safari, Edge, Opera */
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
&[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useState } from '@wordpress/element';
|
||||
|
||||
export type usePaginationProps = {
|
||||
totalCount: number;
|
||||
defaultPerPage?: number;
|
||||
onPageChange?: ( page: number ) => void;
|
||||
onPerPageChange?: ( page: number ) => void;
|
||||
};
|
||||
|
||||
export function usePagination( {
|
||||
totalCount,
|
||||
defaultPerPage = 25,
|
||||
onPageChange,
|
||||
onPerPageChange,
|
||||
}: usePaginationProps ) {
|
||||
const [ currentPage, setCurrentPage ] = useState( 1 );
|
||||
const [ perPage, setPerPage ] = useState( defaultPerPage );
|
||||
|
||||
const pageCount = Math.ceil( totalCount / perPage );
|
||||
const start = perPage * ( currentPage - 1 ) + 1;
|
||||
const end = Math.min( perPage * currentPage, totalCount );
|
||||
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
currentPage,
|
||||
perPage,
|
||||
pageCount,
|
||||
setCurrentPage: ( newPage: number ) => {
|
||||
setCurrentPage( newPage );
|
||||
if ( onPageChange ) {
|
||||
onPageChange( newPage );
|
||||
}
|
||||
},
|
||||
setPerPageChange: ( newPerPage: number ) => {
|
||||
setPerPage( newPerPage );
|
||||
if ( onPerPageChange ) {
|
||||
onPerPageChange( newPerPage );
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -39,6 +39,7 @@ export type SortableProps = {
|
|||
onDragOver?: DragEventHandler< HTMLLIElement >;
|
||||
onDragStart?: DragEventHandler< HTMLDivElement >;
|
||||
onOrderChange?: ( items: SortableChild[] ) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const THROTTLE_TIME = 16;
|
||||
|
@ -52,6 +53,7 @@ export const Sortable = ( {
|
|||
onDragOver = () => null,
|
||||
onDragStart = () => null,
|
||||
onOrderChange = () => null,
|
||||
className,
|
||||
}: SortableProps ) => {
|
||||
const ref = useRef< HTMLOListElement >( null );
|
||||
const [ items, setItems ] = useState< SortableChild[] >( [] );
|
||||
|
@ -227,7 +229,7 @@ export const Sortable = ( {
|
|||
return (
|
||||
<SortableContext.Provider value={ {} }>
|
||||
<ol
|
||||
className={ classnames( 'woocommerce-sortable', {
|
||||
className={ classnames( 'woocommerce-sortable', className, {
|
||||
'is-dragging': dragIndex !== null,
|
||||
'is-horizontal': isHorizontal,
|
||||
} ) }
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
import EllipsisMenu from '../ellipsis-menu';
|
||||
import MenuItem from '../ellipsis-menu/menu-item';
|
||||
import MenuTitle from '../ellipsis-menu/menu-title';
|
||||
import Pagination from '../pagination';
|
||||
import { Pagination } from '../pagination';
|
||||
import Table from './table';
|
||||
import TablePlaceholder from './placeholder';
|
||||
import TableSummary, { TableSummaryPlaceholder } from './summary';
|
||||
|
@ -105,14 +105,14 @@ const TableCard: React.VFC< TableCardProps > = ( {
|
|||
};
|
||||
|
||||
const onPageChange = (
|
||||
newPage: string,
|
||||
direction?: 'previous' | 'next'
|
||||
newPage: number,
|
||||
direction?: 'previous' | 'next' | 'goto'
|
||||
) => {
|
||||
if ( props.onPageChange ) {
|
||||
props.onPageChange( parseInt( newPage, 10 ), direction );
|
||||
props.onPageChange( newPage, direction );
|
||||
}
|
||||
if ( onQueryChange ) {
|
||||
onQueryChange( 'paged' )( newPage, direction );
|
||||
onQueryChange( 'paged' )( newPage.toString(), direction );
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -233,7 +233,11 @@ const TableCard: React.VFC< TableCardProps > = ( {
|
|||
perPage={ rowsPerPage }
|
||||
total={ totalRows }
|
||||
onPageChange={ onPageChange }
|
||||
onPerPageChange={ onQueryChange( 'per_page' ) }
|
||||
onPerPageChange={ ( perPage ) =>
|
||||
onQueryChange( 'per_page' )(
|
||||
perPage.toString()
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{ summary && <TableSummary data={ summary } /> }
|
||||
|
|
|
@ -164,7 +164,10 @@ export type TableCardProps = CommonTableProps & {
|
|||
/**
|
||||
* A callback function that is invoked when the current page is changed.
|
||||
*/
|
||||
onPageChange?: ( newPage: number, direction?: 'previous' | 'next' ) => void;
|
||||
onPageChange?: (
|
||||
newPage: number,
|
||||
direction?: 'previous' | 'next' | 'goto'
|
||||
) => void;
|
||||
/**
|
||||
* The total number of rows to display per page.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Update generateProductVariations action to add support for default_attributes and allowing to disable the product saving.
|
|
@ -17,7 +17,7 @@ import type {
|
|||
GenerateRequest,
|
||||
} from './types';
|
||||
import CRUD_ACTIONS from './crud-actions';
|
||||
import { ProductAttribute } from '../products/types';
|
||||
import { ProductAttribute, ProductDefaultAttribute } from '../products/types';
|
||||
|
||||
export function generateProductVariationsError( key: IdType, error: unknown ) {
|
||||
return {
|
||||
|
@ -47,8 +47,10 @@ export const generateProductVariations = function* (
|
|||
productData: {
|
||||
type?: string;
|
||||
attributes: ProductAttribute[];
|
||||
default_attributes?: ProductDefaultAttribute[];
|
||||
},
|
||||
data: GenerateRequest
|
||||
data: GenerateRequest,
|
||||
saveAttributes = true
|
||||
) {
|
||||
const urlParameters = getUrlParameters(
|
||||
WC_PRODUCT_VARIATIONS_NAMESPACE,
|
||||
|
@ -57,20 +59,22 @@ export const generateProductVariations = function* (
|
|||
const { key } = parseId( idQuery, urlParameters );
|
||||
yield generateProductVariationsRequest( key );
|
||||
|
||||
try {
|
||||
yield controls.dispatch(
|
||||
'core',
|
||||
'saveEntityRecord',
|
||||
'postType',
|
||||
'product',
|
||||
{
|
||||
id: urlParameters[ 0 ],
|
||||
...productData,
|
||||
}
|
||||
);
|
||||
} catch ( error ) {
|
||||
yield generateProductVariationsError( key, error );
|
||||
throw error;
|
||||
if ( saveAttributes ) {
|
||||
try {
|
||||
yield controls.dispatch(
|
||||
'core',
|
||||
'saveEntityRecord',
|
||||
'postType',
|
||||
'product',
|
||||
{
|
||||
id: urlParameters[ 0 ],
|
||||
...productData,
|
||||
}
|
||||
);
|
||||
} catch ( error ) {
|
||||
yield generateProductVariationsError( key, error );
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -83,7 +87,6 @@ export const generateProductVariations = function* (
|
|||
method: 'POST',
|
||||
data,
|
||||
} );
|
||||
|
||||
yield generateProductVariationsSuccess( key );
|
||||
return result;
|
||||
} catch ( error ) {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add global quick actions dropdown menu to the Variations table header
|
|
@ -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 tracking events to add edit and update attribute
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add woocommerce/taxonomy-field block
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Fix bug where the form was dirty still after adding product variations for the first time.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Update pagination look of the Pagination table.
|
|
@ -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;
|
|
@ -1,7 +1,13 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement, useEffect, useRef, useState } from '@wordpress/element';
|
||||
import {
|
||||
createElement,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { TourKit, TourKitTypes } from '@woocommerce/components';
|
||||
import {
|
||||
|
@ -18,25 +24,28 @@ import { useEntityId } from '@wordpress/core-data';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { DEFAULT_PER_PAGE_OPTION } from '../../constants';
|
||||
import { DEFAULT_VARIATION_PER_PAGE_OPTION } from '../../constants';
|
||||
|
||||
export const VariableProductTour: React.FC = () => {
|
||||
const [ isTourOpen, setIsTourOpen ] = useState( false );
|
||||
const productId = useEntityId( 'postType', 'product' );
|
||||
const prevTotalCount = useRef< undefined | number >();
|
||||
const requestParams = useMemo(
|
||||
() => ( {
|
||||
product_id: productId,
|
||||
page: 1,
|
||||
per_page: DEFAULT_VARIATION_PER_PAGE_OPTION,
|
||||
order: 'asc',
|
||||
orderby: 'menu_order',
|
||||
} ),
|
||||
[ productId ]
|
||||
);
|
||||
|
||||
const { totalCount } = useSelect(
|
||||
( select ) => {
|
||||
const { getProductVariationsTotalCount } = select(
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
|
||||
);
|
||||
const requestParams = {
|
||||
product_id: productId,
|
||||
page: 1,
|
||||
per_page: DEFAULT_PER_PAGE_OPTION,
|
||||
order: 'asc',
|
||||
orderby: 'menu_order',
|
||||
};
|
||||
return {
|
||||
totalCount:
|
||||
getProductVariationsTotalCount< number >( requestParams ),
|
||||
|
|
|
@ -22,32 +22,10 @@ import { useEntityProp, useEntityId } from '@wordpress/core-data';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
EnhancedProductAttribute,
|
||||
useProductAttributes,
|
||||
} from '../../hooks/use-product-attributes';
|
||||
import { useProductAttributes } from '../../hooks/use-product-attributes';
|
||||
import { AttributeControl } from '../../components/attribute-control';
|
||||
import { useProductVariationsHelper } from '../../hooks/use-product-variations-helper';
|
||||
|
||||
function manageDefaultAttributes( values: EnhancedProductAttribute[] ) {
|
||||
return values.reduce< Product[ 'default_attributes' ] >(
|
||||
( prevDefaultAttributes, currentAttribute ) => {
|
||||
if ( currentAttribute.isDefault ) {
|
||||
return [
|
||||
...prevDefaultAttributes,
|
||||
{
|
||||
id: currentAttribute.id,
|
||||
name: currentAttribute.name,
|
||||
option: currentAttribute.options[ 0 ],
|
||||
},
|
||||
];
|
||||
}
|
||||
return prevDefaultAttributes;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
export function Edit() {
|
||||
const blockProps = useBlockProps();
|
||||
const { generateProductVariations } = useProductVariationsHelper();
|
||||
|
@ -72,10 +50,10 @@ export function Edit() {
|
|||
allAttributes: entityAttributes,
|
||||
isVariationAttributes: true,
|
||||
productId: useEntityId( 'postType', 'product' ),
|
||||
onChange( values ) {
|
||||
onChange( values, defaultAttributes ) {
|
||||
setEntityAttributes( values );
|
||||
setEntityDefaultAttributes( manageDefaultAttributes( values ) );
|
||||
generateProductVariations( values );
|
||||
setEntityDefaultAttributes( defaultAttributes );
|
||||
generateProductVariations( values, defaultAttributes );
|
||||
},
|
||||
} );
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import type { BlockEditProps } from '@wordpress/blocks';
|
|||
import { Button } from '@wordpress/components';
|
||||
import { Link } from '@woocommerce/components';
|
||||
import { Product, ProductAttribute } from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import {
|
||||
createElement,
|
||||
useState,
|
||||
|
@ -36,16 +37,7 @@ import {
|
|||
import { getAttributeId } from '../../components/attribute-control/utils';
|
||||
import { useProductVariationsHelper } from '../../hooks/use-product-variations-helper';
|
||||
import { hasAttributesUsedForVariations } from '../../utils';
|
||||
|
||||
function getFirstOptionFromEachAttribute(
|
||||
attributes: Product[ 'attributes' ]
|
||||
): Product[ 'default_attributes' ] {
|
||||
return attributes.map( ( attribute ) => ( {
|
||||
id: attribute.id,
|
||||
name: attribute.name,
|
||||
option: attribute.options[ 0 ],
|
||||
} ) );
|
||||
}
|
||||
import { TRACKS_SOURCE } from '../../constants';
|
||||
|
||||
export function Edit( {
|
||||
attributes,
|
||||
|
@ -66,12 +58,10 @@ export function Edit( {
|
|||
allAttributes: productAttributes,
|
||||
isVariationAttributes: true,
|
||||
productId: useEntityId( 'postType', 'product' ),
|
||||
onChange( values ) {
|
||||
onChange( values, defaultAttributes ) {
|
||||
setProductAttributes( values );
|
||||
setDefaultProductAttributes(
|
||||
getFirstOptionFromEachAttribute( values )
|
||||
);
|
||||
generateProductVariations( values );
|
||||
setDefaultProductAttributes( defaultAttributes );
|
||||
generateProductVariations( values, defaultAttributes );
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -101,15 +91,22 @@ export function Edit( {
|
|||
};
|
||||
|
||||
const handleAdd = ( newOptions: EnhancedProductAttribute[] ) => {
|
||||
handleChange( [
|
||||
...newOptions.filter(
|
||||
( newAttr ) =>
|
||||
! variationOptions.find(
|
||||
( attr: ProductAttribute ) =>
|
||||
getAttributeId( newAttr ) === getAttributeId( attr )
|
||||
)
|
||||
),
|
||||
] );
|
||||
const addedAttributesOnly = newOptions.filter(
|
||||
( newAttr ) =>
|
||||
! variationOptions.some(
|
||||
( attr: ProductAttribute ) =>
|
||||
getAttributeId( newAttr ) === getAttributeId( attr )
|
||||
)
|
||||
);
|
||||
recordEvent( 'product_options_add', {
|
||||
source: TRACKS_SOURCE,
|
||||
options: addedAttributesOnly.map( ( attribute ) => ( {
|
||||
attribute: attribute.name,
|
||||
values: attribute.options,
|
||||
} ) ),
|
||||
} );
|
||||
|
||||
handleChange( addedAttributesOnly );
|
||||
closeNewModal();
|
||||
};
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
import { AttributeListItem } from '../attribute-list-item';
|
||||
import { NewAttributeModal } from './new-attribute-modal';
|
||||
import { RemoveConfirmationModal } from './remove-confirmation-modal';
|
||||
import { TRACKS_SOURCE } from '../../constants';
|
||||
|
||||
type AttributeControlProps = {
|
||||
value: EnhancedProductAttribute[];
|
||||
|
@ -157,6 +158,10 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
|
|||
};
|
||||
|
||||
const openEditModal = ( attribute: ProductAttribute ) => {
|
||||
recordEvent( 'product_options_edit', {
|
||||
source: TRACKS_SOURCE,
|
||||
attribute: attribute.name,
|
||||
} );
|
||||
setCurrentAttributeId( getAttributeId( attribute ) );
|
||||
onEditModalOpen( attribute );
|
||||
};
|
||||
|
@ -167,21 +172,35 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
|
|||
};
|
||||
|
||||
const handleAdd = ( newAttributes: EnhancedProductAttribute[] ) => {
|
||||
handleChange( [
|
||||
...value,
|
||||
...newAttributes.filter(
|
||||
( newAttr ) =>
|
||||
! value.find(
|
||||
( attr ) =>
|
||||
getAttributeId( newAttr ) === getAttributeId( attr )
|
||||
)
|
||||
),
|
||||
] );
|
||||
const addedAttributesOnly = newAttributes.filter(
|
||||
( newAttr ) =>
|
||||
! value.some(
|
||||
( current: ProductAttribute ) =>
|
||||
getAttributeId( newAttr ) === getAttributeId( current )
|
||||
)
|
||||
);
|
||||
recordEvent( 'product_options_add', {
|
||||
source: TRACKS_SOURCE,
|
||||
options: addedAttributesOnly.map( ( attribute ) => ( {
|
||||
attribute: attribute.name,
|
||||
values: attribute.options,
|
||||
} ) ),
|
||||
} );
|
||||
handleChange( [ ...value, ...addedAttributesOnly ] );
|
||||
onAdd( newAttributes );
|
||||
closeNewModal();
|
||||
};
|
||||
|
||||
const handleEdit = ( updatedAttribute: ProductAttribute ) => {
|
||||
const handleEdit = ( updatedAttribute: EnhancedProductAttribute ) => {
|
||||
recordEvent( 'product_options_update', {
|
||||
source: TRACKS_SOURCE,
|
||||
attribute: updatedAttribute.name,
|
||||
values: updatedAttribute.terms?.map( ( term ) => term.name ),
|
||||
default: updatedAttribute.isDefault,
|
||||
visible: updatedAttribute.visible,
|
||||
filter: true, // default true until attribute filter gets implemented
|
||||
} );
|
||||
|
||||
const updatedAttributes = value.map( ( attr ) => {
|
||||
if (
|
||||
getAttributeId( attr ) === getAttributeId( updatedAttribute )
|
||||
|
@ -221,7 +240,9 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
|
|||
className="woocommerce-add-attribute-list-item__add-button"
|
||||
onClick={ () => {
|
||||
openNewModal();
|
||||
recordEvent( 'product_add_attributes_click' );
|
||||
recordEvent( 'product_options_add_button_click', {
|
||||
source: TRACKS_SOURCE,
|
||||
} );
|
||||
} }
|
||||
>
|
||||
{ uiStrings.newAttributeListItemLabel }
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export * from './variations-table';
|
||||
export * from './types';
|
||||
|
|
|
@ -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';
|
||||
|
@ -13,23 +12,19 @@ import { chevronRight } from '@wordpress/icons';
|
|||
*/
|
||||
import { TRACKS_SOURCE } from '../../../constants';
|
||||
import { PRODUCT_STOCK_STATUS_KEYS } from '../../../utils/get-product-stock-status';
|
||||
|
||||
export type InventoryMenuItemProps = {
|
||||
variation: ProductVariation;
|
||||
handlePrompt(
|
||||
label?: string,
|
||||
parser?: ( value: string ) => Partial< ProductVariation > | null
|
||||
): void;
|
||||
onChange( values: Partial< ProductVariation > ): void;
|
||||
onClose(): void;
|
||||
};
|
||||
import { UpdateStockMenuItem } from '../update-stock-menu-item';
|
||||
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"
|
||||
|
@ -40,7 +35,7 @@ export function InventoryMenuItem( {
|
|||
'product_variations_menu_inventory_click',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
onToggle();
|
||||
|
@ -55,39 +50,11 @@ export function InventoryMenuItem( {
|
|||
renderContent={ () => (
|
||||
<div className="components-dropdown-menu__menu">
|
||||
<MenuGroup>
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_variations_menu_inventory_select',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'stock_quantity_set',
|
||||
variation_id: variation.id,
|
||||
}
|
||||
);
|
||||
handlePrompt( undefined, ( value ) => {
|
||||
const stockQuantity = Number( value );
|
||||
if ( Number.isNaN( stockQuantity ) ) {
|
||||
return {};
|
||||
}
|
||||
recordEvent(
|
||||
'product_variations_menu_inventory_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'stock_quantity_set',
|
||||
variation_id: variation.id,
|
||||
}
|
||||
);
|
||||
return {
|
||||
stock_quantity: stockQuantity,
|
||||
manage_stock: true,
|
||||
};
|
||||
} );
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
{ __( 'Update stock', 'woocommerce' ) }
|
||||
</MenuItem>
|
||||
<UpdateStockMenuItem
|
||||
selection={ selection }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
|
@ -95,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();
|
||||
} }
|
||||
>
|
||||
|
@ -113,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();
|
||||
} }
|
||||
>
|
||||
|
@ -133,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();
|
||||
} }
|
||||
>
|
||||
|
@ -156,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();
|
||||
} }
|
||||
>
|
||||
|
@ -179,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();
|
||||
} }
|
||||
|
|
|
@ -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';
|
||||
|
@ -12,6 +11,9 @@ import { chevronRight } from '@wordpress/icons';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { TRACKS_SOURCE } from '../../../constants';
|
||||
import { handlePrompt } from '../../../utils/handle-prompt';
|
||||
import { VariationActionsMenuItemProps } from '../types';
|
||||
import { SetListPriceMenuItem } from '../set-list-price-menu-item';
|
||||
|
||||
function isPercentage( value: string ) {
|
||||
return value.endsWith( '%' );
|
||||
|
@ -52,20 +54,15 @@ function addFixedOrPercentage(
|
|||
return Number( value ) + Number( fixedOrPercentage ) * increaseOrDecrease;
|
||||
}
|
||||
|
||||
export type PricingMenuItemProps = {
|
||||
variation: ProductVariation;
|
||||
handlePrompt(
|
||||
label?: string,
|
||||
parser?: ( value: string ) => Partial< ProductVariation >
|
||||
): void;
|
||||
onClose(): void;
|
||||
};
|
||||
|
||||
export function PricingMenuItem( {
|
||||
variation,
|
||||
handlePrompt,
|
||||
selection,
|
||||
onChange,
|
||||
onClose,
|
||||
}: PricingMenuItemProps ) {
|
||||
}: VariationActionsMenuItemProps ) {
|
||||
const ids = Array.isArray( selection )
|
||||
? selection.map( ( { id } ) => id )
|
||||
: selection.id;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
position="middle right"
|
||||
|
@ -74,7 +71,7 @@ export function PricingMenuItem( {
|
|||
onClick={ () => {
|
||||
recordEvent( 'product_variations_menu_pricing_click', {
|
||||
source: TRACKS_SOURCE,
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
} );
|
||||
onToggle();
|
||||
} }
|
||||
|
@ -88,34 +85,11 @@ export function PricingMenuItem( {
|
|||
renderContent={ () => (
|
||||
<div className="components-dropdown-menu__menu">
|
||||
<MenuGroup label={ __( 'List price', 'woocommerce' ) }>
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_select',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'list_price_set',
|
||||
variation_id: variation.id,
|
||||
}
|
||||
);
|
||||
handlePrompt( undefined, ( value ) => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'list_price_set',
|
||||
variation_id: variation.id,
|
||||
}
|
||||
);
|
||||
return {
|
||||
regular_price: value,
|
||||
};
|
||||
} );
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
{ __( 'Set list price', 'woocommerce' ) }
|
||||
</MenuItem>
|
||||
<SetListPriceMenuItem
|
||||
selection={ selection }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
|
@ -123,31 +97,50 @@ export function PricingMenuItem( {
|
|||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'list_price_increase',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
handlePrompt(
|
||||
__(
|
||||
handlePrompt( {
|
||||
message: __(
|
||||
'Enter a value (fixed or %)',
|
||||
'woocommerce'
|
||||
),
|
||||
( value ) => {
|
||||
onOk( value ) {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'list_price_increase',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
return {
|
||||
regular_price: addFixedOrPercentage(
|
||||
variation.regular_price,
|
||||
value
|
||||
)?.toFixed( 2 ),
|
||||
};
|
||||
}
|
||||
);
|
||||
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 ),
|
||||
} );
|
||||
}
|
||||
},
|
||||
} );
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
|
@ -160,32 +153,52 @@ export function PricingMenuItem( {
|
|||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'list_price_decrease',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
handlePrompt(
|
||||
__(
|
||||
handlePrompt( {
|
||||
message: __(
|
||||
'Enter a value (fixed or %)',
|
||||
'woocommerce'
|
||||
),
|
||||
( value ) => {
|
||||
onOk( value ) {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'list_price_increase',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
return {
|
||||
regular_price: addFixedOrPercentage(
|
||||
variation.regular_price,
|
||||
value,
|
||||
-1
|
||||
)?.toFixed( 2 ),
|
||||
};
|
||||
}
|
||||
);
|
||||
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 ),
|
||||
} );
|
||||
}
|
||||
},
|
||||
} );
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
|
@ -200,21 +213,32 @@ export function PricingMenuItem( {
|
|||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_set',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
handlePrompt( undefined, ( value ) => {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_set',
|
||||
variation_id: variation.id,
|
||||
handlePrompt( {
|
||||
onOk( value ) {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_set',
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
if ( Array.isArray( selection ) ) {
|
||||
onChange(
|
||||
selection.map( ( { id } ) => ( {
|
||||
id,
|
||||
sale_price: value,
|
||||
} ) )
|
||||
);
|
||||
} else {
|
||||
onChange( {
|
||||
sale_price: value,
|
||||
} );
|
||||
}
|
||||
);
|
||||
return {
|
||||
sale_price: value,
|
||||
};
|
||||
},
|
||||
} );
|
||||
onClose();
|
||||
} }
|
||||
|
@ -228,31 +252,50 @@ export function PricingMenuItem( {
|
|||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_increase',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
handlePrompt(
|
||||
__(
|
||||
handlePrompt( {
|
||||
message: __(
|
||||
'Enter a value (fixed or %)',
|
||||
'woocommerce'
|
||||
),
|
||||
( value ) => {
|
||||
onOk( value ) {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_increase',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
return {
|
||||
sale_price: addFixedOrPercentage(
|
||||
variation.sale_price,
|
||||
value
|
||||
)?.toFixed( 2 ),
|
||||
};
|
||||
}
|
||||
);
|
||||
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 ),
|
||||
} );
|
||||
}
|
||||
},
|
||||
} );
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
|
@ -265,32 +308,52 @@ export function PricingMenuItem( {
|
|||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_decrease',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
handlePrompt(
|
||||
__(
|
||||
handlePrompt( {
|
||||
message: __(
|
||||
'Enter a value (fixed or %)',
|
||||
'woocommerce'
|
||||
),
|
||||
( value ) => {
|
||||
onOk( value ) {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_decrease',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
return {
|
||||
sale_price: addFixedOrPercentage(
|
||||
variation.sale_price,
|
||||
value,
|
||||
-1
|
||||
)?.toFixed( 2 ),
|
||||
};
|
||||
}
|
||||
);
|
||||
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 ),
|
||||
} );
|
||||
}
|
||||
},
|
||||
} );
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
|
@ -303,47 +366,66 @@ export function PricingMenuItem( {
|
|||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_schedule',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
handlePrompt(
|
||||
__(
|
||||
handlePrompt( {
|
||||
message: __(
|
||||
'Sale start date (YYYY-MM-DD format or leave blank)',
|
||||
'woocommerce'
|
||||
),
|
||||
( value ) => {
|
||||
onOk( value ) {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_schedule',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
return {
|
||||
date_on_sale_from_gmt: value,
|
||||
};
|
||||
}
|
||||
);
|
||||
handlePrompt(
|
||||
__(
|
||||
if ( Array.isArray( selection ) ) {
|
||||
onChange(
|
||||
selection.map( ( { id } ) => ( {
|
||||
id,
|
||||
date_on_sale_from_gmt:
|
||||
value,
|
||||
} ) )
|
||||
);
|
||||
} else {
|
||||
onChange( {
|
||||
date_on_sale_from_gmt: value,
|
||||
} );
|
||||
}
|
||||
},
|
||||
} );
|
||||
handlePrompt( {
|
||||
message: __(
|
||||
'Sale end date (YYYY-MM-DD format or leave blank)',
|
||||
'woocommerce'
|
||||
),
|
||||
( value ) => {
|
||||
onOk( value ) {
|
||||
recordEvent(
|
||||
'product_variations_menu_pricing_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'sale_price_schedule',
|
||||
variation_id: variation.id,
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
return {
|
||||
date_on_sale_to_gmt: value,
|
||||
};
|
||||
}
|
||||
);
|
||||
if ( Array.isArray( selection ) ) {
|
||||
onChange(
|
||||
selection.map( ( { id } ) => ( {
|
||||
id,
|
||||
date_on_sale_to_gmt: value,
|
||||
} ) )
|
||||
);
|
||||
} else {
|
||||
onChange( {
|
||||
date_on_sale_to_gmt: value,
|
||||
} );
|
||||
}
|
||||
},
|
||||
} );
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './set-list-price-menu-item';
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { MenuItem } from '@wordpress/components';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { TRACKS_SOURCE } from '../../../constants';
|
||||
import { handlePrompt } from '../../../utils/handle-prompt';
|
||||
import { VariationActionsMenuItemProps } from '../types';
|
||||
|
||||
export function SetListPriceMenuItem( {
|
||||
selection,
|
||||
onChange,
|
||||
onClose,
|
||||
}: VariationActionsMenuItemProps ) {
|
||||
return (
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
const ids = Array.isArray( selection )
|
||||
? selection.map( ( { id } ) => id )
|
||||
: selection.id;
|
||||
|
||||
recordEvent( 'product_variations_menu_pricing_select', {
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'list_price_set',
|
||||
variation_id: ids,
|
||||
} );
|
||||
handlePrompt( {
|
||||
onOk( value ) {
|
||||
recordEvent( 'product_variations_menu_pricing_update', {
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'list_price_set',
|
||||
variation_id: ids,
|
||||
} );
|
||||
if ( Array.isArray( selection ) ) {
|
||||
onChange(
|
||||
selection.map( ( { id } ) => ( {
|
||||
id,
|
||||
regular_price: value,
|
||||
} ) )
|
||||
);
|
||||
} else {
|
||||
onChange( {
|
||||
regular_price: value,
|
||||
} );
|
||||
}
|
||||
},
|
||||
} );
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
{ __( 'Set list price', 'woocommerce' ) }
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
|
@ -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();
|
||||
} }
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
@import "./variations-actions-menu/styles.scss";
|
||||
|
||||
$table-row-height: calc($grid-unit * 9);
|
||||
|
||||
.woocommerce-product-variations {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -10,6 +14,11 @@
|
|||
border-bottom: 1px solid $gray-200;
|
||||
}
|
||||
|
||||
&__table {
|
||||
height: $table-row-height * 5;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&__selection {
|
||||
.components-checkbox-control__input[type="checkbox"] {
|
||||
&:not(:checked):not(:focus) {
|
||||
|
@ -18,6 +27,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__filters {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
&__loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -101,7 +114,7 @@
|
|||
display: grid;
|
||||
grid-template-columns: 44px auto 25% 25% 88px;
|
||||
padding: 0;
|
||||
min-height: calc($grid-unit * 9);
|
||||
min-height: $table-row-height;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
@ -119,6 +132,7 @@
|
|||
}
|
||||
|
||||
&__footer {
|
||||
padding: $gap;
|
||||
padding: $gap 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { ProductVariation } from '@woocommerce/data';
|
||||
|
||||
export type VariationActionsMenuItemProps = {
|
||||
selection: ProductVariation | ProductVariation[];
|
||||
onChange(
|
||||
variation: Partial< ProductVariation > | Partial< ProductVariation >[]
|
||||
): void;
|
||||
onClose(): void;
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './update-stock-menu-item';
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { MenuItem } from '@wordpress/components';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { TRACKS_SOURCE } from '../../../constants';
|
||||
import { handlePrompt } from '../../../utils/handle-prompt';
|
||||
import { VariationActionsMenuItemProps } from '../types';
|
||||
|
||||
export function UpdateStockMenuItem( {
|
||||
selection,
|
||||
onChange,
|
||||
onClose,
|
||||
}: VariationActionsMenuItemProps ) {
|
||||
return (
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
const ids = Array.isArray( selection )
|
||||
? selection.map( ( { id } ) => id )
|
||||
: selection.id;
|
||||
|
||||
recordEvent( 'product_variations_menu_inventory_select', {
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'stock_quantity_set',
|
||||
variation_id: ids,
|
||||
} );
|
||||
handlePrompt( {
|
||||
onOk( value ) {
|
||||
const stockQuantity = Number( value );
|
||||
if ( Number.isNaN( stockQuantity ) ) {
|
||||
return;
|
||||
}
|
||||
recordEvent(
|
||||
'product_variations_menu_inventory_update',
|
||||
{
|
||||
source: TRACKS_SOURCE,
|
||||
action: 'stock_quantity_set',
|
||||
variation_id: ids,
|
||||
}
|
||||
);
|
||||
if ( Array.isArray( selection ) ) {
|
||||
onChange(
|
||||
selection.map( ( { id } ) => ( {
|
||||
id,
|
||||
stock_quantity: stockQuantity,
|
||||
manage_stock: true,
|
||||
} ) )
|
||||
);
|
||||
} else {
|
||||
onChange( {
|
||||
stock_quantity: stockQuantity,
|
||||
manage_stock: true,
|
||||
} );
|
||||
}
|
||||
},
|
||||
} );
|
||||
onClose();
|
||||
} }
|
||||
>
|
||||
{ __( 'Update stock', 'woocommerce' ) }
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
|
@ -33,7 +33,7 @@ describe( 'VariationActionsMenu', () => {
|
|||
it( 'should trigger product_variations_menu_view track when dropdown toggled', () => {
|
||||
const { getByRole } = render(
|
||||
<VariationActionsMenu
|
||||
variation={ mockVariation }
|
||||
selection={ mockVariation }
|
||||
onChange={ onChangeMock }
|
||||
onDelete={ onDeleteMock }
|
||||
/>
|
||||
|
@ -51,7 +51,7 @@ describe( 'VariationActionsMenu', () => {
|
|||
it( 'should render dropdown with pricing, inventory, and delete options when opened', () => {
|
||||
const { queryByText, getByRole } = render(
|
||||
<VariationActionsMenu
|
||||
variation={ mockVariation }
|
||||
selection={ mockVariation }
|
||||
onChange={ onChangeMock }
|
||||
onDelete={ onDeleteMock }
|
||||
/>
|
||||
|
@ -65,7 +65,7 @@ describe( 'VariationActionsMenu', () => {
|
|||
it( 'should call onDelete when Delete menuItem is clicked', async () => {
|
||||
const { getByRole, getByText } = render(
|
||||
<VariationActionsMenu
|
||||
variation={ mockVariation }
|
||||
selection={ mockVariation }
|
||||
onChange={ onChangeMock }
|
||||
onDelete={ onDeleteMock }
|
||||
/>
|
||||
|
@ -79,7 +79,7 @@ describe( 'VariationActionsMenu', () => {
|
|||
it( 'should open Inventory sub-menu if Inventory is clicked with click track', async () => {
|
||||
const { queryByText, getByRole, getByText } = render(
|
||||
<VariationActionsMenu
|
||||
variation={ { ...mockVariation } }
|
||||
selection={ { ...mockVariation } }
|
||||
onChange={ onChangeMock }
|
||||
onDelete={ onDeleteMock }
|
||||
/>
|
||||
|
@ -106,7 +106,7 @@ describe( 'VariationActionsMenu', () => {
|
|||
window.prompt = jest.fn().mockReturnValue( '10' );
|
||||
const { getByRole, getByText } = render(
|
||||
<VariationActionsMenu
|
||||
variation={ { ...mockVariation } }
|
||||
selection={ { ...mockVariation } }
|
||||
onChange={ onChangeMock }
|
||||
onDelete={ onDeleteMock }
|
||||
/>
|
||||
|
@ -141,7 +141,7 @@ describe( 'VariationActionsMenu', () => {
|
|||
window.prompt = jest.fn().mockReturnValue( null );
|
||||
const { getByRole, getByText } = render(
|
||||
<VariationActionsMenu
|
||||
variation={ { ...mockVariation } }
|
||||
selection={ { ...mockVariation } }
|
||||
onChange={ onChangeMock }
|
||||
onDelete={ onDeleteMock }
|
||||
/>
|
||||
|
@ -175,7 +175,7 @@ describe( 'VariationActionsMenu', () => {
|
|||
it( 'should call onChange with toggled manage_stock when toggle "track quantity" is clicked', async () => {
|
||||
const { getByRole, getByText, rerender } = render(
|
||||
<VariationActionsMenu
|
||||
variation={ { ...mockVariation } }
|
||||
selection={ { ...mockVariation } }
|
||||
onChange={ onChangeMock }
|
||||
onDelete={ onDeleteMock }
|
||||
/>
|
||||
|
@ -198,7 +198,7 @@ describe( 'VariationActionsMenu', () => {
|
|||
onChangeMock.mockClear();
|
||||
rerender(
|
||||
<VariationActionsMenu
|
||||
variation={ { ...mockVariation, manage_stock: true } }
|
||||
selection={ { ...mockVariation, manage_stock: true } }
|
||||
onChange={ onChangeMock }
|
||||
onDelete={ onDeleteMock }
|
||||
/>
|
||||
|
@ -214,7 +214,7 @@ describe( 'VariationActionsMenu', () => {
|
|||
it( 'should call onChange with toggled stock_status when toggle "Set status to In stock" is clicked', async () => {
|
||||
const { getByRole, getByText } = render(
|
||||
<VariationActionsMenu
|
||||
variation={ { ...mockVariation } }
|
||||
selection={ { ...mockVariation } }
|
||||
onChange={ onChangeMock }
|
||||
onDelete={ onDeleteMock }
|
||||
/>
|
||||
|
@ -240,7 +240,7 @@ describe( 'VariationActionsMenu', () => {
|
|||
it( 'should call onChange with toggled stock_status when toggle "Set status to Out of stock" is clicked', async () => {
|
||||
const { getByRole, getByText } = render(
|
||||
<VariationActionsMenu
|
||||
variation={ { ...mockVariation } }
|
||||
selection={ { ...mockVariation } }
|
||||
onChange={ onChangeMock }
|
||||
onDelete={ onDeleteMock }
|
||||
/>
|
||||
|
@ -266,7 +266,7 @@ describe( 'VariationActionsMenu', () => {
|
|||
it( 'should call onChange with toggled stock_status when toggle "Set status to On back order" is clicked', async () => {
|
||||
const { getByRole, getByText } = render(
|
||||
<VariationActionsMenu
|
||||
variation={ { ...mockVariation } }
|
||||
selection={ { ...mockVariation } }
|
||||
onChange={ onChangeMock }
|
||||
onDelete={ onDeleteMock }
|
||||
/>
|
||||
|
@ -293,7 +293,7 @@ describe( 'VariationActionsMenu', () => {
|
|||
window.prompt = jest.fn().mockReturnValue( '7' );
|
||||
const { getByRole, getByText } = render(
|
||||
<VariationActionsMenu
|
||||
variation={ { ...mockVariation } }
|
||||
selection={ { ...mockVariation } }
|
||||
onChange={ onChangeMock }
|
||||
onDelete={ onDeleteMock }
|
||||
/>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import { ProductVariation } from '@woocommerce/data';
|
||||
|
||||
export type VariationActionsMenuProps = {
|
||||
variation: ProductVariation;
|
||||
selection: ProductVariation;
|
||||
onChange( variation: Partial< ProductVariation > ): void;
|
||||
onDelete( variationId: number ): void;
|
||||
onDelete( variation: ProductVariation ): void;
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
@ -18,25 +17,10 @@ import { InventoryMenuItem } from '../inventory-menu-item';
|
|||
import { PricingMenuItem } from '../pricing-menu-item';
|
||||
|
||||
export function VariationActionsMenu( {
|
||||
variation,
|
||||
selection,
|
||||
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 }
|
||||
|
@ -45,7 +29,7 @@ export function VariationActionsMenu( {
|
|||
onClick() {
|
||||
recordEvent( 'product_variations_menu_view', {
|
||||
source: TRACKS_SOURCE,
|
||||
variation_id: variation.id,
|
||||
variation_id: selection.id,
|
||||
} );
|
||||
},
|
||||
} }
|
||||
|
@ -56,15 +40,15 @@ export function VariationActionsMenu( {
|
|||
label={ sprintf(
|
||||
/** Translators: Variation ID */
|
||||
__( 'Variation Id: %s', 'woocommerce' ),
|
||||
variation.id
|
||||
selection.id
|
||||
) }
|
||||
>
|
||||
<MenuItem
|
||||
href={ variation.permalink }
|
||||
href={ selection.permalink }
|
||||
onClick={ () => {
|
||||
recordEvent( 'product_variations_preview', {
|
||||
source: TRACKS_SOURCE,
|
||||
variation_id: variation.id,
|
||||
variation_id: selection.id,
|
||||
} );
|
||||
} }
|
||||
>
|
||||
|
@ -73,19 +57,18 @@ export function VariationActionsMenu( {
|
|||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<PricingMenuItem
|
||||
variation={ variation }
|
||||
handlePrompt={ handlePrompt }
|
||||
selection={ selection }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
<InventoryMenuItem
|
||||
variation={ variation }
|
||||
handlePrompt={ handlePrompt }
|
||||
selection={ selection }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
<ShippingMenuItem
|
||||
variation={ variation }
|
||||
handlePrompt={ handlePrompt }
|
||||
selection={ selection }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
</MenuGroup>
|
||||
|
@ -95,7 +78,7 @@ export function VariationActionsMenu( {
|
|||
label={ __( 'Delete variation', 'woocommerce' ) }
|
||||
variant="link"
|
||||
onClick={ () => {
|
||||
onDelete( variation.id );
|
||||
onDelete( selection.id );
|
||||
onClose();
|
||||
} }
|
||||
className="woocommerce-product-variations__actions--delete"
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from './variations-actions-menu';
|
||||
export * from './types';
|
|
@ -0,0 +1,9 @@
|
|||
.variations-actions-menu {
|
||||
&__toogle {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
> span {
|
||||
margin: 0 6px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { ProductVariation } from '@woocommerce/data';
|
||||
|
||||
export type VariationsActionsMenuProps = {
|
||||
disabled?: boolean;
|
||||
selection: ProductVariation[];
|
||||
onChange( variations: Partial< ProductVariation >[] ): void;
|
||||
onDelete( variations: ProductVariation[] ): void;
|
||||
};
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Button, Dropdown, MenuGroup, MenuItem } from '@wordpress/components';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { chevronDown, chevronUp } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
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,
|
||||
disabled,
|
||||
onChange,
|
||||
onDelete,
|
||||
}: VariationsActionsMenuProps ) {
|
||||
return (
|
||||
<Dropdown
|
||||
position="bottom left"
|
||||
renderToggle={ ( { isOpen, onToggle } ) => (
|
||||
<Button
|
||||
disabled={ disabled }
|
||||
aria-expanded={ isOpen }
|
||||
icon={ isOpen ? chevronUp : chevronDown }
|
||||
variant="secondary"
|
||||
onClick={ onToggle }
|
||||
className="variations-actions-menu__toogle"
|
||||
>
|
||||
<span>{ __( 'Quick update', 'woocommerce' ) }</span>
|
||||
</Button>
|
||||
) }
|
||||
renderContent={ ( { onClose } ) => (
|
||||
<div className="components-dropdown-menu__menu">
|
||||
<MenuGroup>
|
||||
<UpdateStockMenuItem
|
||||
selection={ selection }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
<SetListPriceMenuItem
|
||||
selection={ selection }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<PricingMenuItem
|
||||
selection={ selection }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
<InventoryMenuItem
|
||||
selection={ selection }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
<ShippingMenuItem
|
||||
selection={ selection }
|
||||
onChange={ onChange }
|
||||
onClose={ onClose }
|
||||
/>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<MenuItem
|
||||
isDestructive
|
||||
variant="link"
|
||||
onClick={ () => {
|
||||
onDelete( selection );
|
||||
onClose();
|
||||
} }
|
||||
className="woocommerce-product-variations__actions--delete"
|
||||
>
|
||||
{ __( 'Delete', 'woocommerce' ) }
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
</div>
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -13,8 +13,21 @@ import {
|
|||
ProductVariation,
|
||||
} from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { ListItem, Pagination, Sortable, Tag } from '@woocommerce/components';
|
||||
import { useContext, useState, createElement } from '@wordpress/element';
|
||||
import {
|
||||
ListItem,
|
||||
Sortable,
|
||||
Tag,
|
||||
PaginationPageSizePicker,
|
||||
PaginationPageArrowsWithPicker,
|
||||
usePagination,
|
||||
} from '@woocommerce/components';
|
||||
import {
|
||||
useContext,
|
||||
useState,
|
||||
createElement,
|
||||
useRef,
|
||||
useMemo,
|
||||
} from '@wordpress/element';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import classnames from 'classnames';
|
||||
import truncate from 'lodash/truncate';
|
||||
|
@ -31,12 +44,13 @@ import HiddenIcon from './hidden-icon';
|
|||
import VisibleIcon from './visible-icon';
|
||||
import { getProductStockStatus, getProductStockStatusClass } from '../../utils';
|
||||
import {
|
||||
DEFAULT_PER_PAGE_OPTION,
|
||||
DEFAULT_VARIATION_PER_PAGE_OPTION,
|
||||
PRODUCT_VARIATION_TITLE_LIMIT,
|
||||
TRACKS_SOURCE,
|
||||
} from '../../constants';
|
||||
import { VariationActionsMenu } from './variation-actions-menu';
|
||||
import { useSelection } from '../../hooks/use-selection';
|
||||
import { VariationsActionsMenu } from './variations-actions-menu';
|
||||
|
||||
const NOT_VISIBLE_TEXT = __( 'Not visible to customers', 'woocommerce' );
|
||||
const VISIBLE_TEXT = __( 'Visible to customers', 'woocommerce' );
|
||||
|
@ -44,7 +58,10 @@ const UPDATING_TEXT = __( 'Updating product variation', 'woocommerce' );
|
|||
|
||||
export function VariationsTable() {
|
||||
const [ currentPage, setCurrentPage ] = useState( 1 );
|
||||
const [ perPage, setPerPage ] = useState( DEFAULT_PER_PAGE_OPTION );
|
||||
const lastVariations = useRef< ProductVariation[] | null >( null );
|
||||
const [ perPage, setPerPage ] = useState(
|
||||
DEFAULT_VARIATION_PER_PAGE_OPTION
|
||||
);
|
||||
const [ isUpdating, setIsUpdating ] = useState< Record< string, boolean > >(
|
||||
{}
|
||||
);
|
||||
|
@ -58,53 +75,83 @@ export function VariationsTable() {
|
|||
} = useSelection();
|
||||
|
||||
const productId = useEntityId( 'postType', 'product' );
|
||||
const requestParams = useMemo(
|
||||
() => ( {
|
||||
product_id: productId,
|
||||
page: currentPage,
|
||||
per_page: perPage,
|
||||
order: 'asc',
|
||||
orderby: 'menu_order',
|
||||
} ),
|
||||
[ productId, currentPage, perPage ]
|
||||
);
|
||||
const totalCountRequestParams = useMemo(
|
||||
() => ( {
|
||||
product_id: productId,
|
||||
order: 'asc',
|
||||
orderby: 'menu_order',
|
||||
} ),
|
||||
[ productId ]
|
||||
);
|
||||
const context = useContext( CurrencyContext );
|
||||
const { formatAmount } = context;
|
||||
const { isLoading, variations, totalCount, isGeneratingVariations } =
|
||||
useSelect(
|
||||
( select ) => {
|
||||
const {
|
||||
getProductVariations,
|
||||
hasFinishedResolution,
|
||||
getProductVariationsTotalCount,
|
||||
isGeneratingVariations: getIsGeneratingVariations,
|
||||
} = select( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME );
|
||||
const requestParams = {
|
||||
product_id: productId,
|
||||
page: currentPage,
|
||||
per_page: perPage,
|
||||
order: 'asc',
|
||||
orderby: 'menu_order',
|
||||
};
|
||||
return {
|
||||
isLoading: ! hasFinishedResolution(
|
||||
'getProductVariations',
|
||||
[ requestParams ]
|
||||
),
|
||||
isGeneratingVariations: getIsGeneratingVariations( {
|
||||
product_id: productId,
|
||||
} ),
|
||||
variations:
|
||||
getProductVariations< ProductVariation[] >(
|
||||
requestParams
|
||||
),
|
||||
totalCount:
|
||||
getProductVariationsTotalCount< number >(
|
||||
requestParams
|
||||
),
|
||||
};
|
||||
},
|
||||
[ currentPage, perPage, productId ]
|
||||
);
|
||||
const { totalCount } = useSelect(
|
||||
( select ) => {
|
||||
const { getProductVariationsTotalCount } = select(
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
|
||||
);
|
||||
|
||||
const { updateProductVariation, deleteProductVariation } = useDispatch(
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
|
||||
return {
|
||||
totalCount: getProductVariationsTotalCount< number >(
|
||||
totalCountRequestParams
|
||||
),
|
||||
};
|
||||
},
|
||||
[ productId ]
|
||||
);
|
||||
const { isLoading, latestVariations, isGeneratingVariations } = useSelect(
|
||||
( select ) => {
|
||||
const {
|
||||
getProductVariations,
|
||||
hasFinishedResolution,
|
||||
isGeneratingVariations: getIsGeneratingVariations,
|
||||
} = select( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME );
|
||||
return {
|
||||
isLoading: ! hasFinishedResolution( 'getProductVariations', [
|
||||
requestParams,
|
||||
] ),
|
||||
isGeneratingVariations: getIsGeneratingVariations( {
|
||||
product_id: requestParams.product_id,
|
||||
} ),
|
||||
latestVariations:
|
||||
getProductVariations< ProductVariation[] >( requestParams ),
|
||||
};
|
||||
},
|
||||
[ currentPage, perPage, productId ]
|
||||
);
|
||||
|
||||
const paginationProps = usePagination( {
|
||||
totalCount,
|
||||
defaultPerPage: DEFAULT_VARIATION_PER_PAGE_OPTION,
|
||||
onPageChange: setCurrentPage,
|
||||
onPerPageChange: setPerPage,
|
||||
} );
|
||||
|
||||
const {
|
||||
updateProductVariation,
|
||||
deleteProductVariation,
|
||||
batchUpdateProductVariations,
|
||||
invalidateResolution,
|
||||
} = useDispatch( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME );
|
||||
|
||||
const { createSuccessNotice, createErrorNotice } =
|
||||
useDispatch( 'core/notices' );
|
||||
|
||||
if ( ! variations && isLoading ) {
|
||||
if ( latestVariations && latestVariations !== lastVariations.current ) {
|
||||
lastVariations.current = latestVariations;
|
||||
}
|
||||
|
||||
if ( isLoading && lastVariations.current === null ) {
|
||||
return (
|
||||
<div className="woocommerce-product-variations__loading">
|
||||
<Spinner />
|
||||
|
@ -116,6 +163,8 @@ export function VariationsTable() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
// this prevents a weird jump from happening while changing pages.
|
||||
const variations = latestVariations || lastVariations.current;
|
||||
|
||||
const variationIds = variations.map( ( { id } ) => id );
|
||||
|
||||
|
@ -174,22 +223,72 @@ export function VariationsTable() {
|
|||
);
|
||||
}
|
||||
|
||||
function handleUpdateAll( update: Partial< ProductVariation >[] ) {
|
||||
batchUpdateProductVariations< { update: [] } >(
|
||||
{ product_id: productId },
|
||||
{ update }
|
||||
)
|
||||
.then( ( response ) =>
|
||||
invalidateResolution( 'getProductVariations', [
|
||||
requestParams,
|
||||
] ).then( () => response )
|
||||
)
|
||||
.then( ( response ) => {
|
||||
createSuccessNotice(
|
||||
sprintf(
|
||||
/* translators: The updated variations count */
|
||||
__( '%s variation/s updated.', 'woocommerce' ),
|
||||
response.update.length
|
||||
)
|
||||
);
|
||||
} )
|
||||
.catch( () => {
|
||||
createErrorNotice(
|
||||
__( 'Failed to update variations.', 'woocommerce' )
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
function handleDeleteAll( values: Partial< ProductVariation >[] ) {
|
||||
batchUpdateProductVariations< { delete: [] } >(
|
||||
{ product_id: productId },
|
||||
{
|
||||
delete: values.map( ( { id } ) => id ),
|
||||
}
|
||||
)
|
||||
.then( ( response ) =>
|
||||
invalidateResolution( 'getProductVariations', [
|
||||
requestParams,
|
||||
] ).then( () => response )
|
||||
)
|
||||
.then( ( response ) => {
|
||||
createSuccessNotice(
|
||||
sprintf(
|
||||
/* translators: The updated variations count */
|
||||
__( '%s variation/s updated.', 'woocommerce' ),
|
||||
response.delete.length
|
||||
)
|
||||
);
|
||||
} )
|
||||
.catch( () => {
|
||||
createErrorNotice(
|
||||
__( 'Failed to delete variations.', 'woocommerce' )
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="woocommerce-product-variations">
|
||||
{ isLoading ||
|
||||
( isGeneratingVariations && (
|
||||
<div className="woocommerce-product-variations__loading">
|
||||
<Spinner />
|
||||
{ isGeneratingVariations && (
|
||||
<span>
|
||||
{ __(
|
||||
'Generating variations…',
|
||||
'woocommerce'
|
||||
) }
|
||||
</span>
|
||||
) }
|
||||
</div>
|
||||
) ) }
|
||||
{ ( isLoading || isGeneratingVariations ) && (
|
||||
<div className="woocommerce-product-variations__loading">
|
||||
<Spinner />
|
||||
{ isGeneratingVariations && (
|
||||
<span>
|
||||
{ __( 'Generating variations…', 'woocommerce' ) }
|
||||
</span>
|
||||
) }
|
||||
</div>
|
||||
) }
|
||||
<div className="woocommerce-product-variations__header">
|
||||
<div className="woocommerce-product-variations__selection">
|
||||
<CheckboxControl
|
||||
|
@ -203,7 +302,7 @@ export function VariationsTable() {
|
|||
onChange={ onSelectAll( variationIds ) }
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="woocommerce-product-variations__filters">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
disabled={ areAllSelected( variationIds ) }
|
||||
|
@ -219,8 +318,18 @@ export function VariationsTable() {
|
|||
{ __( 'Clear selection', 'woocommerce' ) }
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<VariationsActionsMenu
|
||||
selection={ variations.filter( ( variation ) =>
|
||||
isSelected( variation.id )
|
||||
) }
|
||||
disabled={ ! hasSelection( variationIds ) }
|
||||
onChange={ handleUpdateAll }
|
||||
onDelete={ handleDeleteAll }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Sortable>
|
||||
<Sortable className="woocommerce-product-variations__table">
|
||||
{ variations.map( ( variation ) => (
|
||||
<ListItem key={ `${ variation.id }` }>
|
||||
<div className="woocommerce-product-variations__selection">
|
||||
|
@ -353,26 +462,38 @@ export function VariationsTable() {
|
|||
</Tooltip>
|
||||
) }
|
||||
<VariationActionsMenu
|
||||
variation={ variation }
|
||||
selection={ variation }
|
||||
onChange={ ( value ) =>
|
||||
handleVariationChange( variation.id, value )
|
||||
}
|
||||
onDelete={ handleDeleteVariationClick }
|
||||
onDelete={ ( { id } ) =>
|
||||
handleDeleteVariationClick( id )
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ListItem>
|
||||
) ) }
|
||||
</Sortable>
|
||||
|
||||
<Pagination
|
||||
className="woocommerce-product-variations__footer"
|
||||
page={ currentPage }
|
||||
perPage={ perPage }
|
||||
total={ totalCount }
|
||||
showPagePicker={ false }
|
||||
onPageChange={ setCurrentPage }
|
||||
onPerPageChange={ setPerPage }
|
||||
/>
|
||||
{ totalCount > 5 && (
|
||||
<div className="woocommerce-product-variations__footer woocommerce-pagination">
|
||||
<div>
|
||||
{ sprintf(
|
||||
__( 'Viewing %d-%d of %d items', 'woocommerce' ),
|
||||
paginationProps.start,
|
||||
paginationProps.end,
|
||||
totalCount
|
||||
) }
|
||||
</div>
|
||||
<PaginationPageArrowsWithPicker { ...paginationProps } />
|
||||
<PaginationPageSizePicker
|
||||
{ ...paginationProps }
|
||||
total={ totalCount }
|
||||
perPageOptions={ [ 5, 10, 25 ] }
|
||||
label=""
|
||||
/>
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -62,3 +62,5 @@ export const TRACKS_SOURCE = 'product-block-editor-v1';
|
|||
* @see https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/components/src/pagination/index.js#L12
|
||||
*/
|
||||
export const DEFAULT_PER_PAGE_OPTION = 25;
|
||||
|
||||
export const DEFAULT_VARIATION_PER_PAGE_OPTION = 5;
|
||||
|
|
|
@ -172,15 +172,18 @@ describe( 'useProductAttributes', () => {
|
|||
jest.runOnlyPendingTimers();
|
||||
await waitForNextUpdate();
|
||||
result.current.handleChange( [
|
||||
allAttributes[ 0 ],
|
||||
allAttributes[ 1 ],
|
||||
{ ...testAttributes[ 0 ] },
|
||||
] );
|
||||
expect( onChange ).toHaveBeenCalledWith( [
|
||||
{ ...allAttributes[ 0 ], position: 0 },
|
||||
{ ...allAttributes[ 1 ], position: 1 },
|
||||
{ ...testAttributes[ 0 ], variation: false, position: 2 },
|
||||
{ ...allAttributes[ 0 ], isDefault: false },
|
||||
{ ...allAttributes[ 1 ], isDefault: false },
|
||||
{ ...testAttributes[ 0 ], isDefault: false },
|
||||
] );
|
||||
expect( onChange ).toHaveBeenCalledWith(
|
||||
[
|
||||
{ ...allAttributes[ 0 ], position: 0 },
|
||||
{ ...allAttributes[ 1 ], position: 1 },
|
||||
{ ...testAttributes[ 0 ], variation: false, position: 2 },
|
||||
],
|
||||
[]
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should keep both variable and non variable as part of the onChange list, when isVariation is false', async () => {
|
||||
|
@ -202,12 +205,17 @@ describe( 'useProductAttributes', () => {
|
|||
);
|
||||
jest.runOnlyPendingTimers();
|
||||
await waitForNextUpdate();
|
||||
result.current.handleChange( [ { ...testAttributes[ 0 ] } ] );
|
||||
expect( onChange ).toHaveBeenCalledWith( [
|
||||
{ ...testAttributes[ 0 ], variation: false, position: 0 },
|
||||
{ ...allAttributes[ 0 ], position: 1 },
|
||||
{ ...allAttributes[ 1 ], position: 2 },
|
||||
result.current.handleChange( [
|
||||
{ ...testAttributes[ 0 ], isDefault: false },
|
||||
] );
|
||||
expect( onChange ).toHaveBeenCalledWith(
|
||||
[
|
||||
{ ...testAttributes[ 0 ], variation: false, position: 0 },
|
||||
{ ...allAttributes[ 0 ], position: 1 },
|
||||
{ ...allAttributes[ 1 ], position: 2 },
|
||||
],
|
||||
[]
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should keep both variable and non variable as part of the onChange list, when isVariation is true', async () => {
|
||||
|
@ -229,12 +237,17 @@ describe( 'useProductAttributes', () => {
|
|||
);
|
||||
jest.runOnlyPendingTimers();
|
||||
await waitForNextUpdate();
|
||||
result.current.handleChange( [ { ...testAttributes[ 0 ] } ] );
|
||||
expect( onChange ).toHaveBeenCalledWith( [
|
||||
{ ...allAttributes[ 0 ], position: 0 },
|
||||
{ ...allAttributes[ 1 ], position: 1 },
|
||||
{ ...testAttributes[ 0 ], variation: true, position: 2 },
|
||||
result.current.handleChange( [
|
||||
{ ...testAttributes[ 0 ], isDefault: false },
|
||||
] );
|
||||
expect( onChange ).toHaveBeenCalledWith(
|
||||
[
|
||||
{ ...allAttributes[ 0 ], position: 0 },
|
||||
{ ...allAttributes[ 1 ], position: 1 },
|
||||
{ ...testAttributes[ 0 ], variation: true, position: 2 },
|
||||
],
|
||||
[]
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should remove duplicate globals', async () => {
|
||||
|
@ -256,11 +269,16 @@ describe( 'useProductAttributes', () => {
|
|||
);
|
||||
jest.runOnlyPendingTimers();
|
||||
await waitForNextUpdate();
|
||||
result.current.handleChange( [ { ...testAttributes[ 1 ] } ] );
|
||||
expect( onChange ).toHaveBeenCalledWith( [
|
||||
{ ...allAttributes[ 1 ], position: 0 },
|
||||
{ ...allAttributes[ 0 ], position: 1, variation: true },
|
||||
result.current.handleChange( [
|
||||
{ ...testAttributes[ 1 ], isDefault: false },
|
||||
] );
|
||||
expect( onChange ).toHaveBeenCalledWith(
|
||||
[
|
||||
{ ...allAttributes[ 1 ], position: 0 },
|
||||
{ ...allAttributes[ 0 ], position: 1, variation: true },
|
||||
],
|
||||
[]
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should remove duplicate locals by name', async () => {
|
||||
|
@ -282,11 +300,94 @@ describe( 'useProductAttributes', () => {
|
|||
);
|
||||
jest.runOnlyPendingTimers();
|
||||
await waitForNextUpdate();
|
||||
result.current.handleChange( [ { ...testAttributes[ 0 ] } ] );
|
||||
expect( onChange ).toHaveBeenCalledWith( [
|
||||
{ ...allAttributes[ 1 ], position: 0 },
|
||||
{ ...allAttributes[ 0 ], position: 1, variation: true },
|
||||
result.current.handleChange( [
|
||||
{ ...testAttributes[ 0 ], isDefault: false },
|
||||
] );
|
||||
expect( onChange ).toHaveBeenCalledWith(
|
||||
[
|
||||
{ ...allAttributes[ 1 ], position: 0 },
|
||||
{ ...allAttributes[ 0 ], position: 1, variation: true },
|
||||
],
|
||||
[]
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should pass default attributes as second param, defaulting to true when isDefault is not defined', async () => {
|
||||
const allAttributes = [
|
||||
{ ...testAttributes[ 0 ] },
|
||||
{ ...testAttributes[ 1 ] },
|
||||
];
|
||||
const onChange = jest.fn();
|
||||
const { result, waitForNextUpdate } = renderHook(
|
||||
useProductAttributes,
|
||||
{
|
||||
initialProps: {
|
||||
allAttributes,
|
||||
onChange,
|
||||
isVariationAttributes: true,
|
||||
productId: 123,
|
||||
},
|
||||
}
|
||||
);
|
||||
jest.runOnlyPendingTimers();
|
||||
await waitForNextUpdate();
|
||||
result.current.handleChange( [ { ...testAttributes[ 0 ] } ] );
|
||||
expect( onChange ).toHaveBeenCalledWith(
|
||||
[
|
||||
{ ...allAttributes[ 1 ], position: 0 },
|
||||
{ ...allAttributes[ 0 ], position: 1, variation: true },
|
||||
],
|
||||
[
|
||||
{
|
||||
id: testAttributes[ 0 ].id,
|
||||
name: testAttributes[ 0 ].name,
|
||||
option: testAttributes[ 0 ].options[ 0 ],
|
||||
},
|
||||
]
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should pass default attributes as second param, when isDefault is true', async () => {
|
||||
const allAttributes = [
|
||||
{ ...testAttributes[ 0 ] },
|
||||
{ ...testAttributes[ 1 ] },
|
||||
];
|
||||
const onChange = jest.fn();
|
||||
const { result, waitForNextUpdate } = renderHook(
|
||||
useProductAttributes,
|
||||
{
|
||||
initialProps: {
|
||||
allAttributes,
|
||||
onChange,
|
||||
isVariationAttributes: true,
|
||||
productId: 123,
|
||||
},
|
||||
}
|
||||
);
|
||||
jest.runOnlyPendingTimers();
|
||||
await waitForNextUpdate();
|
||||
result.current.handleChange( [
|
||||
{ ...testAttributes[ 0 ], isDefault: true },
|
||||
{ ...testAttributes[ 1 ], isDefault: true },
|
||||
] );
|
||||
expect( onChange ).toHaveBeenCalledWith(
|
||||
[
|
||||
{ ...allAttributes[ 0 ], position: 0, variation: true },
|
||||
{ ...allAttributes[ 1 ], position: 1, variation: true },
|
||||
],
|
||||
[
|
||||
{
|
||||
id: testAttributes[ 0 ].id,
|
||||
name: testAttributes[ 0 ].name,
|
||||
option: testAttributes[ 0 ].options[ 0 ],
|
||||
},
|
||||
{
|
||||
id: testAttributes[ 1 ].id,
|
||||
name: testAttributes[ 1 ].name,
|
||||
option: testAttributes[ 1 ].options[ 0 ],
|
||||
},
|
||||
]
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
*/
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME,
|
||||
Product,
|
||||
ProductAttribute,
|
||||
ProductAttributeTerm,
|
||||
ProductDefaultAttribute,
|
||||
} from '@woocommerce/data';
|
||||
import { resolveSelect } from '@wordpress/data';
|
||||
import { useCallback, useEffect, useState } from '@wordpress/element';
|
||||
|
@ -23,7 +25,10 @@ export type EnhancedProductAttribute = ProductAttribute & {
|
|||
type useProductAttributesProps = {
|
||||
allAttributes: ProductAttribute[];
|
||||
isVariationAttributes?: boolean;
|
||||
onChange: ( attributes: ProductAttribute[] ) => void;
|
||||
onChange: (
|
||||
attributes: ProductAttribute[],
|
||||
defaultAttributes: ProductDefaultAttribute[]
|
||||
) => void;
|
||||
productId?: number;
|
||||
};
|
||||
|
||||
|
@ -36,6 +41,29 @@ const getFilteredAttributes = (
|
|||
: attr.filter( ( attribute ) => ! attribute.variation );
|
||||
};
|
||||
|
||||
function manageDefaultAttributes( values: EnhancedProductAttribute[] ) {
|
||||
return values.reduce< Product[ 'default_attributes' ] >(
|
||||
( prevDefaultAttributes, currentAttribute ) => {
|
||||
if (
|
||||
// defaults to true.
|
||||
currentAttribute.isDefault === undefined ||
|
||||
currentAttribute.isDefault === true
|
||||
) {
|
||||
return [
|
||||
...prevDefaultAttributes,
|
||||
{
|
||||
id: currentAttribute.id,
|
||||
name: currentAttribute.name,
|
||||
option: currentAttribute.options[ 0 ],
|
||||
},
|
||||
];
|
||||
}
|
||||
return prevDefaultAttributes;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
export function useProductAttributes( {
|
||||
allAttributes = [],
|
||||
isVariationAttributes = false,
|
||||
|
@ -91,6 +119,7 @@ export function useProductAttributes( {
|
|||
};
|
||||
|
||||
const handleChange = ( newAttributes: EnhancedProductAttribute[] ) => {
|
||||
const defaultAttributes = manageDefaultAttributes( newAttributes );
|
||||
let otherAttributes = isVariationAttributes
|
||||
? allAttributes.filter( ( attribute ) => ! attribute.variation )
|
||||
: allAttributes.filter( ( attribute ) => !! attribute.variation );
|
||||
|
@ -126,15 +155,15 @@ export function useProductAttributes( {
|
|||
);
|
||||
|
||||
if ( isVariationAttributes ) {
|
||||
onChange( [
|
||||
...otherAugmentedAttributes,
|
||||
...newAugmentedAttributes,
|
||||
] );
|
||||
onChange(
|
||||
[ ...otherAugmentedAttributes, ...newAugmentedAttributes ],
|
||||
defaultAttributes
|
||||
);
|
||||
} else {
|
||||
onChange( [
|
||||
...newAugmentedAttributes,
|
||||
...otherAugmentedAttributes,
|
||||
] );
|
||||
onChange(
|
||||
[ ...newAugmentedAttributes, ...otherAugmentedAttributes ],
|
||||
defaultAttributes
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,10 @@
|
|||
import { useDispatch } from '@wordpress/data';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { useCallback, useState } from '@wordpress/element';
|
||||
import { EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME } from '@woocommerce/data';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
|
||||
ProductDefaultAttribute,
|
||||
} from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -25,7 +28,10 @@ export function useProductVariationsHelper() {
|
|||
const [ isGenerating, setIsGenerating ] = useState( false );
|
||||
|
||||
const generateProductVariations = useCallback(
|
||||
async ( attributes: EnhancedProductAttribute[] ) => {
|
||||
async (
|
||||
attributes: EnhancedProductAttribute[],
|
||||
defaultAttributes?: ProductDefaultAttribute[]
|
||||
) => {
|
||||
setIsGenerating( true );
|
||||
|
||||
const hasVariableAttribute = attributes.some(
|
||||
|
@ -42,6 +48,7 @@ export function useProductVariationsHelper() {
|
|||
{
|
||||
type: hasVariableAttribute ? 'variable' : 'simple',
|
||||
attributes,
|
||||
default_attributes: defaultAttributes,
|
||||
},
|
||||
{
|
||||
delete: true,
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export type HandlePromptProps = {
|
||||
message?: string;
|
||||
defaultValue?: string;
|
||||
onOk( value: string ): void;
|
||||
onCancel?(): void;
|
||||
};
|
||||
|
||||
export async function handlePrompt( {
|
||||
message = __( 'Enter a value', 'woocommerce' ),
|
||||
defaultValue,
|
||||
onOk,
|
||||
onCancel,
|
||||
}: HandlePromptProps ) {
|
||||
// eslint-disable-next-line no-alert
|
||||
const value = window.prompt( message, defaultValue );
|
||||
|
||||
if ( value === null ) {
|
||||
if ( onCancel ) {
|
||||
onCancel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
onOk( value );
|
||||
}
|
|
@ -5,7 +5,6 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { useState } from '@wordpress/element';
|
||||
import {
|
||||
useReducedMotion,
|
||||
useResizeObserver,
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
# Core Profiler
|
||||
|
||||
The Core Profiler feature is a modernized and simplified new user setup experience for WooCommerce core. It is the first thing a new merchant will see upon installation of WooCommerce.
|
||||
|
||||
It requests the minimum amount of information from a merchant to get their store up and running, and suggests some optional extensions that may fulfil common needs for new stores.
|
||||
|
||||
There are 4 pages in the Core Profiler:
|
||||
|
||||
1. Introduction & Data sharing opt-in
|
||||
2. User Profile - Some questions determining the user's entry point to the WooCommerce setup
|
||||
3. Business Information - Store Details and Location
|
||||
4. Extensions - Optional extensions that may be useful to the merchant
|
||||
|
||||
If the merchant chooses to install any extensions that require Jetpack, they will then be redirected to WordPress.com to login to Jetpack after the extensions page. Upon completion of that, they will be returned back to the WooCommerce Admin homescreen which contains the Task List. The Task List will provide next steps for the merchant to continue with their store setup.
|
||||
|
||||
## Development
|
||||
|
||||
The Core Profiler is gated behind the `core-profiler` feature flag, but is enabled by default.
|
||||
|
||||
This feature is the first feature in WooCommerce to use XState for state management, and so naming and organisational conventions will be developed as the team's experience with XState grows.
|
||||
|
||||
Refer to the [XState Dev Tooling](xstate.md) documentation for information on how to use the XState visualizer to debug the state machines.
|
||||
|
||||
The state machine for the Core Profiler is centrally located at `./client/core-profiler/index.tsx`, and is responsible for managing the state of the entire feature. It is responsible for rendering the correct page based on the current state, handling events that are triggered by the user, triggering side effects such as API calls and handling the responses. It also handles updating the browser URL state as well as responding to changes in it.
|
||||
|
||||
While working on this feature, bear in mind that the state machine should interact with WordPress and WooCommerce via actions and services, and the UI code should not be responsible for any API calls or interaction with WordPress Data Stores. This allows us to easily render the UI pages in isolation, for example use in Storybook. The UI pages should only send events back to the state machine in order to trigger side effects.
|
||||
|
||||
## Saving and retrieving data
|
||||
|
||||
As of writing, the following options are saved (and retrieved if the user has already completed the Core Profiler):
|
||||
|
||||
- `blogname`: string
|
||||
|
||||
This stores the name of the store, which is used in the store header and in the browser tab title, among other places.
|
||||
|
||||
- `woocommerce_onboarding_profile`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
business_choice: "im_just_starting_my_business" | "im_already_selling" | "im_setting_up_a_store_for_a_client" | undefined
|
||||
selling_online_answer: "yes_im_selling_online" | "no_im_selling_offline" | "im_selling_both_online_and_offline" | undefined
|
||||
selling_platforms: ("amazon" | "adobe_commerce" | "big_cartel" | "big_commerce" | "ebay" | "ecwid" | "etsy" | "facebook_marketplace" | "google_shopping" | "pinterest" | "shopify" | "square" | "squarespace" | "wix" | "wordpress")[] | undefined
|
||||
is_store_country_set: true | false
|
||||
industry: "clothing_and_accessories" | "health_and_beauty" | "food_and_drink" | "home_furniture_and_garden" | "education_and_learning" | "electronics_and_computers" | "other"
|
||||
}
|
||||
```
|
||||
|
||||
This stores the merchant's onboarding profile, some of which are used for suggesting extensions and toggling other features.
|
||||
|
||||
- `woocommerce_default_country`: e.g 'US:CA', 'SG', 'AU:VIC'
|
||||
|
||||
This stores the location that the WooCommerce store believes it is in. This is used for determining extensions eligibility.
|
||||
|
||||
- `woocommerce_allow_tracking`: 'yes' | 'no'
|
||||
|
||||
This determines whether we return telemetry to Automattic.
|
||||
|
||||
As this information is not automatically updated, it would be best to refer directly to the data types present in the source code for the most up to date information.
|
||||
|
||||
### API Calls
|
||||
|
||||
The following WP Data API calls are used in the Core Profiler:
|
||||
|
||||
- `resolveSelect( ONBOARDING_STORE_NAME ).getFreeExtensions()`
|
||||
|
||||
This is used to retrieve the list of extensions that will be shown on the Extensions page. It makes an API call to the WooCommerce REST API, which will make a call to WooCommerce.com if permitted. Otherwise it retrieves the locally stored list of free extensions.
|
||||
|
||||
- `resolveSelect( COUNTRIES_STORE_NAME ).getCountries()`
|
||||
|
||||
This is used to retrieve the list of countries that will be shown in the Country dropdown on the Business Information page. It makes an API call to the WooCommerce REST API.
|
||||
|
||||
- `resolveSelect( COUNTRIES_STORE_NAME ).geolocate()`
|
||||
|
||||
This is used to retrieve the country that the store believes it is in. It makes an API call to the WordPress.com geolocation API, if permitted. Otherwise it will not be used.
|
||||
|
||||
- `resolveSelect( PLUGINS_STORE_NAME ).isJetpackConnected()`
|
||||
|
||||
This is used to determine whether the store is connected to Jetpack.
|
||||
|
||||
- `resolveSelect( ONBOARDING_STORE_NAME ).getJetpackAuthUrl()`
|
||||
|
||||
This is used to retrieve the URL that the browser should be redirected to in order to connect to Jetpack.
|
||||
|
||||
### Extensions Installation
|
||||
|
||||
The Core Profiler has a loading screen that is shown after the Extensions page. This loading screen is meant to hide the installation of Extensions, while also giving the user a sense of progress. At the same time, some extensions take extremely long to install, and thus we have a 30 second timeout.
|
||||
|
||||
The selected extensions will be put into an installation queue, and the queue will be processed sequentially while the loader is on screen.
|
||||
|
||||
Beyond the 30 second timeout, the remaining plugins will be installed in the background, and the user will be redirected to the WooCommerce Admin homescreen or the Jetpack connection page.
|
|
@ -1,4 +1,8 @@
|
|||
# WooCommerce Onboarding
|
||||
# WooCommerce Onboarding (DEPRECATED)
|
||||
|
||||
**Refer to the new [Core Profiler](./core-profiler.md) documentation for the latest onboarding experience.**
|
||||
|
||||
**Some parts of this documentation are still relevant and will be gradually moved to the new Core Profiler documentation.**
|
||||
|
||||
The onboarding feature is a reimagined new user setup experience for WooCommerce core. It contains two sections aimed at getting merchants started with their stores. The merchant begins with the "profile wizard", which helps with initial steps like setting a store address, making extension recommendations, and connecting to Jetpack for additional features. Once the profile wizard is complete, the merchant can purchase & install optional extensions via WooCommerce.com, before continuing to the "task list". From the task list, merchants are walked through a variety of items to help them start selling, such as adding their first product and setting up payment methods.
|
||||
|
||||
|
@ -6,7 +10,7 @@ The onboarding feature is a reimagined new user setup experience for WooCommerce
|
|||
|
||||
If you run the development version of WooCommerce Admin from GitHub directly, no further action should be needed to enable Onboarding.
|
||||
|
||||
Users of the published WooCommerce Admin plugin need to either opt-in to using the new onboarding experience manually, or become part of the a/b test in core. See https://github.com/woocommerce/woocommerce/pull/24991 for more details on the testing implementation.
|
||||
Users of the published WooCommerce Admin plugin need to either opt-in to using the new onboarding experience manually, or become part of the a/b test in core. See [https://github.com/woocommerce/woocommerce/pull/24991](https://github.com/woocommerce/woocommerce/pull/24991) for more details on the testing implementation.
|
||||
|
||||
To enable the new onboarding experience manually, log-in to `wp-admin`, and go to `WooCommerce > Settings > Help > Setup Wizard`. Click `Enable` under the `New onboarding experience` heading.
|
||||
|
||||
|
@ -63,7 +67,7 @@ To make the connection from the new onboarding experience possible, we build our
|
|||
|
||||
Both of these endpoints use WooCommerce Core's `WC_Helper_API` directly. The main difference with our connection (compared to the connection on the subscriptions page) is the addition of two additional query string parameters:
|
||||
|
||||
* `wccom-from=onboarding`, which is used to tell WooCommerce.com & WordPress.com that we are connecting from the new onboarding flow. This parameter is passed from the user's site to WooCommerce.com and finally into Calypso, so that the Calypso login and sign-up screens match the rest of the profile wizard (https://github.com/Automattic/wp-calypso/pull/35193). Without this parameter, you would end up on the existing WooCommerce OAuth screen.
|
||||
* `wccom-from=onboarding`, which is used to tell WooCommerce.com & WordPress.com that we are connecting from the new onboarding flow. This parameter is passed from the user's site to WooCommerce.com and finally into Calypso, so that the Calypso login and sign-up screens match the rest of the profile wizard [https://github.com/Automattic/wp-calypso/pull/35193](https://github.com/Automattic/wp-calypso/pull/35193). Without this parameter, you would end up on the existing WooCommerce OAuth screen.
|
||||
* `calypso_env` allows us to load different versions of Calypso when testing. See the Calypso section below.
|
||||
|
||||
To disconnect from WooCommerce.com, go to `WooCommerce > Extensions > WooCommerce.com Subscriptions > Connected to WooCommerce.com > Disconnect`.
|
||||
|
@ -77,7 +81,7 @@ We have a special Jetpack connection flow designed specifically for WooCommerce
|
|||
We use Jetpack's `build_connect_url` function directly, but add the following two query parameters:
|
||||
|
||||
* `calypso_env`, which allows us to load different versions of Calypso when testing. See the Calypso section below.
|
||||
* `from=woocommerce-onboarding`, which is used to conditionally show the WooCommerce themed Jetpack authorization process (https://github.com/Automattic/wp-calypso/pull/34380). Without this parameter, you would end up in the normal Jetpack authorization flow.
|
||||
* `from=woocommerce-onboarding`, which is used to conditionally show the WooCommerce themed Jetpack authorization process [https://github.com/Automattic/wp-calypso/pull/34380](https://github.com/Automattic/wp-calypso/pull/34380). Without this parameter, you would end up in the normal Jetpack authorization flow.
|
||||
|
||||
The user is prompted to install and connect to Jetpack as the first step of the profile wizard. If the user hasn't connected when they arrive at the task list, we also prompt them on certain tasks to make the setup process easier, such as the shipping and tax steps.
|
||||
|
||||
|
@ -89,7 +93,7 @@ To disconnect from Jetpack, go to `Jetpack > Dashboard > Connections > Site conn
|
|||
|
||||
Both the WooCommerce.com & Jetpack connection processes (outlined below) send the user to [Calypso](https://github.com/Automattic/wp-calypso), the interface that powers WordPress.com, to sign-up or login.
|
||||
|
||||
By default, a merchant will end up on a production version of Calypso (https://wordpress.com). If we make changes to the Calypso part of the flow and want to test them, we can do so with a `calypso_env` query parameter passed by both of our connection methods.
|
||||
By default, a merchant will end up on a production version of Calypso [https://wordpress.com](https://wordpress.com). If we make changes to the Calypso part of the flow and want to test them, we can do so with a `calypso_env` query parameter passed by both of our connection methods.
|
||||
|
||||
To change the value of `calypso_env`, set `WOOCOMMERCE_CALYPSO_ENVIRONMENT` to one of the following values in your `wp-config.php`:
|
||||
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
# XState Dev Tooling
|
||||
|
||||
XState is a state management framework for Javascript applications. It is based on the concept of [finite state machines](https://en.wikipedia.org/wiki/Finite-state_machine) and statecharts.
|
||||
|
||||
In order to help you visualize and debug the state machines within WooCommerce Core, XState provides a [visualizer](https://stately.ai/viz) that you can use in your browser.
|
||||
|
||||
|
||||
## Enabling the visualizer
|
||||
|
||||
To enable this, run this command in your browser's developer console:
|
||||
|
||||
```js
|
||||
localStorage.setItem('xstate_inspect', 'true');
|
||||
```
|
||||
|
||||
Then, a new tab with the XState visualizer should appear for pages that have state machines.
|
||||
|
||||
## Using the visualizer
|
||||
|
||||
|
||||
### Main View
|
||||
|
||||
The main panel in the visualizer will show you the current state of the state machine. Current states are shown with a blue border, and available transitions are solid blue bubbles. If the events are simple without payload requirements, you can click on the transition bubbles to trigger the transition. Otherwise, you can use the 'Send event' button on the bottom right of the visualizer to send events to the state machine.
|
||||
|
||||
|
||||
### State Tab
|
||||
|
||||
The context is the working memory that the state machine has access to. It is a plain Javascript object that can be modified by the state machine.
|
||||
|
||||
Within the 'State' tab, you can see the printouts of three objects: "Value", "Context", and "State".
|
||||
|
||||
The 'Value' object is simply the current state that the state machine is in, and it may be an object if there are nested state machines.
|
||||
|
||||
The 'Context' object is the current context of the state machine. It is a plain Javascript object that can be modified by the state machine. The context is used to store information that is relevant to the state machine, but not part of the state itself. For example, the context may contain information about the user, or the current page that the user is on.
|
||||
|
||||
The 'State' object is the current state of the state machine. It contains information about the current state, the events that led to the current state, as well as the history object which contains the previous states.
|
||||
|
||||
### Events Tab
|
||||
|
||||
The 'Events' tab shows the events that have occurred since the state machine was initialized. You can click on the events to see the payload of the events.
|
||||
|
||||
Clicking on 'Show built-in events' will include built-in events in the list. These are non-user events that are triggered by the state machine itself, such as invoked promises.
|
||||
|
||||
These events are useful for debugging, as they can help you understand what events have occurred and what the payload of the events are, and provides a similar functionality to Redux Dev Tools.
|
||||
|
||||
### Actors Tab
|
||||
|
||||
If there is more than one state machine active (e.g, there are multiple state machines or there are child state machines), you can select which one to inspect by clicking on the 'Actors' tab on the top right of the visualizer.
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
|
@ -268,16 +268,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v5.4.26",
|
||||
"version": "v5.4.28",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "b504a3d266ad2bb632f196c0936ef2af5ff6e273"
|
||||
"reference": "f4f71842f24c2023b91237c72a365306f3c58827"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/b504a3d266ad2bb632f196c0936ef2af5ff6e273",
|
||||
"reference": "b504a3d266ad2bb632f196c0936ef2af5ff6e273",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/f4f71842f24c2023b91237c72a365306f3c58827",
|
||||
"reference": "f4f71842f24c2023b91237c72a365306f3c58827",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -347,7 +347,7 @@
|
|||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v5.4.26"
|
||||
"source": "https://github.com/symfony/console/tree/v5.4.28"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -363,7 +363,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-07-19T20:11:33+00:00"
|
||||
"time": "2023-08-07T06:12:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
|
@ -497,16 +497,16 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.27.0",
|
||||
"version": "v1.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
|
||||
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
|
||||
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
|
||||
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -521,7 +521,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
|
@ -559,7 +559,7 @@
|
|||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -575,20 +575,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
"time": "2023-01-26T09:26:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"version": "v1.27.0",
|
||||
"version": "v1.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
|
||||
"reference": "511a08c03c1960e08a883f4cffcacd219b758354"
|
||||
"reference": "875e90aeea2777b6f135677f618529449334a612"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354",
|
||||
"reference": "511a08c03c1960e08a883f4cffcacd219b758354",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612",
|
||||
"reference": "875e90aeea2777b6f135677f618529449334a612",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -600,7 +600,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
|
@ -640,7 +640,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -656,20 +656,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
"time": "2023-01-26T09:26:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-intl-normalizer",
|
||||
"version": "v1.27.0",
|
||||
"version": "v1.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
|
||||
"reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6"
|
||||
"reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6",
|
||||
"reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
|
||||
"reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -681,7 +681,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
|
@ -724,7 +724,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0"
|
||||
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -740,20 +740,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
"time": "2023-01-26T09:26:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"version": "v1.27.0",
|
||||
"version": "v1.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
|
||||
"reference": "42292d99c55abe617799667f454222c54c60e229"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
||||
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
|
||||
"reference": "42292d99c55abe617799667f454222c54c60e229",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -768,7 +768,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
|
@ -807,7 +807,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
|
||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -823,20 +823,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
"time": "2023-07-28T09:04:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php73",
|
||||
"version": "v1.27.0",
|
||||
"version": "v1.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php73.git",
|
||||
"reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9"
|
||||
"reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9e8ecb5f92152187c4799efd3c96b78ccab18ff9",
|
||||
"reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5",
|
||||
"reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -845,7 +845,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
|
@ -886,7 +886,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php73/tree/v1.27.0"
|
||||
"source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -902,20 +902,20 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
"time": "2023-01-26T09:26:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
"version": "v1.27.0",
|
||||
"version": "v1.28.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
|
||||
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
|
||||
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
|
||||
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -924,7 +924,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.27-dev"
|
||||
"dev-main": "1.28-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
|
@ -969,7 +969,7 @@
|
|||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0"
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -985,7 +985,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-11-03T14:55:06+00:00"
|
||||
"time": "2023-01-26T09:26:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
|
@ -1169,5 +1169,5 @@
|
|||
"platform-overrides": {
|
||||
"php": "7.3"
|
||||
},
|
||||
"plugin-api-version": "2.3.0"
|
||||
"plugin-api-version": "2.1.0"
|
||||
}
|
||||
|
|
|
@ -258,16 +258,16 @@
|
|||
},
|
||||
{
|
||||
"name": "sirbrillig/phpcs-changed",
|
||||
"version": "v2.11.2",
|
||||
"version": "v2.11.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sirbrillig/phpcs-changed.git",
|
||||
"reference": "03492be0d8ef076c6ca5189312ee39eb36767442"
|
||||
"reference": "9fc94ed9adee571b7e30da4467b1d41073a9dd1c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sirbrillig/phpcs-changed/zipball/03492be0d8ef076c6ca5189312ee39eb36767442",
|
||||
"reference": "03492be0d8ef076c6ca5189312ee39eb36767442",
|
||||
"url": "https://api.github.com/repos/sirbrillig/phpcs-changed/zipball/9fc94ed9adee571b7e30da4467b1d41073a9dd1c",
|
||||
"reference": "9fc94ed9adee571b7e30da4467b1d41073a9dd1c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -306,9 +306,9 @@
|
|||
"description": "Run phpcs on files, but only report warnings/errors from lines which were changed.",
|
||||
"support": {
|
||||
"issues": "https://github.com/sirbrillig/phpcs-changed/issues",
|
||||
"source": "https://github.com/sirbrillig/phpcs-changed/tree/v2.11.2"
|
||||
"source": "https://github.com/sirbrillig/phpcs-changed/tree/v2.11.3"
|
||||
},
|
||||
"time": "2023-08-21T15:07:52+00:00"
|
||||
"time": "2023-08-24T23:27:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "squizlabs/php_codesniffer",
|
||||
|
@ -473,5 +473,5 @@
|
|||
"platform-overrides": {
|
||||
"php": "7.2"
|
||||
},
|
||||
"plugin-api-version": "2.3.0"
|
||||
"plugin-api-version": "2.1.0"
|
||||
}
|
||||
|
|
|
@ -1711,5 +1711,5 @@
|
|||
"platform-overrides": {
|
||||
"php": "7.0"
|
||||
},
|
||||
"plugin-api-version": "2.3.0"
|
||||
"plugin-api-version": "2.1.0"
|
||||
}
|
||||
|
|
|
@ -634,5 +634,5 @@
|
|||
"platform-overrides": {
|
||||
"php": "7.0"
|
||||
},
|
||||
"plugin-api-version": "2.3.0"
|
||||
"plugin-api-version": "2.1.0"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Add Variation options section back to the product blocks template
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add woocommerce_block_template_register action.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: tweak
|
||||
|
||||
Add order property to every block in SimpleProductTemplate
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Added documentation for the Core Profiler
|
|
@ -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
|
||||
|
||||
Fixed missed lint error in Assembler Hub
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
update the SqlQuery filter prefix in data.md
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
Fix a minor code typo, no change in functionality
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
Comment: Updated WC API Core tests readme.
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Update WooCommerce Blocks to 11.0.0
|
|
@ -23,7 +23,7 @@
|
|||
"maxmind-db/reader": "^1.11",
|
||||
"pelago/emogrifier": "^6.0",
|
||||
"woocommerce/action-scheduler": "3.6.2",
|
||||
"woocommerce/woocommerce-blocks": "10.9.3"
|
||||
"woocommerce/woocommerce-blocks": "11.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"automattic/jetpack-changelogger": "^3.3.0",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "0fbbf35adfe8924174d461ce53cfe1c6",
|
||||
"content-hash": "0c27792dd3b1927fff20aa226d428068",
|
||||
"packages": [
|
||||
{
|
||||
"name": "automattic/jetpack-a8c-mc-stats",
|
||||
|
@ -1004,16 +1004,16 @@
|
|||
},
|
||||
{
|
||||
"name": "woocommerce/woocommerce-blocks",
|
||||
"version": "10.9.3",
|
||||
"version": "11.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/woocommerce/woocommerce-blocks.git",
|
||||
"reference": "c9b6f46758f446b21cfd9f98ae1d2ca714effbc3"
|
||||
"reference": "926f174399738ea25de191b1db05cf42ef4635d3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/c9b6f46758f446b21cfd9f98ae1d2ca714effbc3",
|
||||
"reference": "c9b6f46758f446b21cfd9f98ae1d2ca714effbc3",
|
||||
"url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/926f174399738ea25de191b1db05cf42ef4635d3",
|
||||
"reference": "926f174399738ea25de191b1db05cf42ef4635d3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -1062,9 +1062,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/woocommerce/woocommerce-blocks/issues",
|
||||
"source": "https://github.com/woocommerce/woocommerce-blocks/tree/v10.9.3"
|
||||
"source": "https://github.com/woocommerce/woocommerce-blocks/tree/v11.0.0"
|
||||
},
|
||||
"time": "2023-08-24T20:50:04+00:00"
|
||||
"time": "2023-08-30T10:20:08+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
|
@ -3947,5 +3947,5 @@
|
|||
"platform-overrides": {
|
||||
"php": "7.4"
|
||||
},
|
||||
"plugin-api-version": "2.3.0"
|
||||
"plugin-api-version": "2.1.0"
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue