Merge branch 'trunk' into add/tt3-comp-button-classes
This commit is contained in:
commit
f43b943523
|
@ -1,5 +1,5 @@
|
|||
name: ✨ Enhancement Request
|
||||
description: If you have an idea to improve an existing feature in core or need something for development (such as a new hook) please let us know or submit a Pull Request!
|
||||
description: If you have an idea to improve existing functionality in core or need something for development (such as a new hook) please let us know or submit a Pull Request!
|
||||
title: "[Enhancement]: "
|
||||
labels: ["type: enhancement", "status: awaiting triage"]
|
||||
body:
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Feature Requests
|
||||
url: https://woocommerce.com/feature-requests/woocommerce/
|
||||
about: If you have an idea for a new feature that you wished existed in WooCommerce, take a look at our Feature Requests and vote, or open a new Feature Request yourself!
|
||||
- name: 🔒 Security issue
|
||||
url: https://hackerone.com/automattic/
|
||||
about: For security reasons, please report all security issues via HackerOne. If the issue is valid, a bug bounty will be paid out to you. Please disclose responsibly and not via GitHub (which allows for exploiting issues in the wild before the patch is released).
|
||||
|
|
|
@ -135,7 +135,7 @@ function get_latest_version_with_release() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Function to retreive the sha1 reference for a given branch name.
|
||||
* Function to retrieve the sha1 reference for a given branch name.
|
||||
*
|
||||
* @param string $branch The name of the branch.
|
||||
* @return string Returns the name of the branch, or a falsey value on error.
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
* Fix - Prevent possible warning arising from use of woocommerce_wp_* family of functions. [#37026](https://github.com/woocommerce/woocommerce/pull/37026)
|
||||
* Fix - Record values for toggled checkboxes/features in settings [#37242](https://github.com/woocommerce/woocommerce/pull/37242)
|
||||
* Fix - Restore the sort order when orders are cached. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
|
||||
* Fix - Treat order as seperate resource when validating for webhook since it's not necessarily a CPT anymore. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
|
||||
* Fix - Treat order as separate resource when validating for webhook since it's not necessarily a CPT anymore. [#36650](https://github.com/woocommerce/woocommerce/pull/36650)
|
||||
* Fix - Update Customers report with latest user data after editing user. [#37237](https://github.com/woocommerce/woocommerce/pull/37237)
|
||||
* Add - Add "Create a new campaign" modal in Campaigns card in Multichannel Marketing page. [#37044](https://github.com/woocommerce/woocommerce/pull/37044)
|
||||
* Add - Add a cache for orders, to use when custom order tables are enabled [#35014](https://github.com/woocommerce/woocommerce/pull/35014)
|
||||
|
@ -432,7 +432,7 @@
|
|||
* Tweak - Resolve an error in the product tracking code by testing to see if the `post_type` query var is set before checking its value. [#34501](https://github.com/woocommerce/woocommerce/pull/34501)
|
||||
* Tweak - Simplify wording within the customer emails for on-hold orders. [#31886](https://github.com/woocommerce/woocommerce/pull/31886)
|
||||
* Tweak - WooCommerce has now been tested up to WordPress 6.1.x. [#35985](https://github.com/woocommerce/woocommerce/pull/35985)
|
||||
* Performance - Split CALC_FOUND_ROW query into seperate count query for better performance. [#35468](https://github.com/woocommerce/woocommerce/pull/35468)
|
||||
* Performance - Split CALC_FOUND_ROW query into separate count query for better performance. [#35468](https://github.com/woocommerce/woocommerce/pull/35468)
|
||||
* Enhancement - Add a bottom padding to the whole form [#35721](https://github.com/woocommerce/woocommerce/pull/35721)
|
||||
* Enhancement - Add a confirmation modal when the user tries to navigate away with unsaved changes [#35625](https://github.com/woocommerce/woocommerce/pull/35625)
|
||||
* Enhancement - Add a default placeholder title for newly added attributes and always show remove button for attributes [#35904](https://github.com/woocommerce/woocommerce/pull/35904)
|
||||
|
@ -3371,7 +3371,7 @@
|
|||
* Fix - Add protection around func_get_args_call for backwards compatibility. #28677
|
||||
* Fix - Restore stock_status in REST API V3 response. #28731
|
||||
* Fix - Revert some of the changes related to perf enhancements (27735) as it was breaking stock_status filter. #28745
|
||||
* Dev - Hook for intializing price slider in frontend. #28014
|
||||
* Dev - Hook for initializing price slider in frontend. #28014
|
||||
* Dev - Add support for running a custom initialization script for tests. #28041
|
||||
* Dev - Use the coenjacobs/mozart package to renamespace vendor packages. #28147
|
||||
* Dev - Documentation for `wc_get_container`. #28269
|
||||
|
@ -5290,7 +5290,7 @@
|
|||
* Fix - Fix edge case where `get_plugins` would not have the custom WooCommerce plugin headers if `get_plugins` was called early. #21669
|
||||
* Fix - Prevent PHP warning when deprecated user meta starts with uppercase. #21943
|
||||
* Fix - Fixed support for multiple query parameters translated to meta queries via REST API requests. #22108
|
||||
* Fix - Prevent PHP errors when trying to access non-existant report tabs. #22183
|
||||
* Fix - Prevent PHP errors when trying to access non-existent report tabs. #22183
|
||||
* Fix - Filter by attributes dropdown placeholder text should not be wrapped in quotes. #22185
|
||||
* Fix - Apply sale price until end of closing sale date. #22189
|
||||
* Fix - Allow empty schema again when registering a custom field for the API. #22204
|
||||
|
@ -6612,7 +6612,7 @@
|
|||
* Removed internal scroll from log viewer.
|
||||
* Add reply-to to admin emails.
|
||||
* Improved the zone setup flow.
|
||||
* Made wc_get_wildcard_postcodes return the orignal postcode plus * since wildcards should match empty strings too.
|
||||
* Made wc_get_wildcard_postcodes return the original postcode plus * since wildcards should match empty strings too.
|
||||
* Use all paid statuses in $customer->get_total_spent().
|
||||
* Move location of billing email field to work with password managers.
|
||||
* Option to restrict selling locations by country.
|
||||
|
@ -8122,7 +8122,7 @@
|
|||
* Tweak - Flat rate shipping support for percentage factor of additional costs.
|
||||
* Tweak - local delivery _ pattern matching for postcodes. e.g. NG1___ would match NG1 1AA but not NG10 1AA.
|
||||
* Tweak - Improved layered nav OR count logic
|
||||
* Tweak - Make shipping methods check if taxable, so when customer is VAT excempt taxes are not included in price.
|
||||
* Tweak - Make shipping methods check if taxable, so when customer is VAT exempt taxes are not included in price.
|
||||
* Tweak - Coupon in admin bar new menu #3974
|
||||
* Tweak - Shortcode tag filters + updated menu names to make white labelling easier.
|
||||
* Tweak - Removed placeholder polyfill. Use this plugin to replace functionality if required: https://wordpress.org/plugins/html5-placeholder-polyfill/
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Migrate select control component to TS
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
Correct spelling errors
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Show comma separated list in ready only mode of select tree control
|
|
@ -94,7 +94,7 @@ Name | Type | Default | Description
|
|||
`legendPosition` | One of: 'bottom', 'side', 'top', 'hidden' | `null` | Position the legend must be displayed in. If it's not defined, it's calculated depending on the viewport width and the mode
|
||||
`legendTotals` | Object | `null` | Values to overwrite the legend totals. If not defined, the sum of all line values will be used
|
||||
`screenReaderFormat` | One of type: string, func | `'%B %-d, %Y'` | A datetime formatting string or overriding function to format the screen reader labels
|
||||
`showHeaderControls` | Boolean | `true` | Wether header UI controls must be displayed
|
||||
`showHeaderControls` | Boolean | `true` | Whether header UI controls must be displayed
|
||||
`title` | String | `null` | A title describing this chart
|
||||
`tooltipLabelFormat` | One of type: string, func | `'%B %-d, %Y'` | A datetime formatting string or overriding function to format the tooltip label
|
||||
`tooltipValueFormat` | One of type: string, func | `','` | A number formatting string or function to format the value displayed in the tooltips
|
||||
|
|
|
@ -577,7 +577,7 @@ Chart.propTypes = {
|
|||
PropTypes.func,
|
||||
] ),
|
||||
/**
|
||||
* Wether header UI controls must be displayed.
|
||||
* Whether header UI controls must be displayed.
|
||||
*/
|
||||
showHeaderControls: PropTypes.bool,
|
||||
/**
|
||||
|
|
|
@ -75,8 +75,8 @@ export const ComboBox = ( {
|
|||
<input
|
||||
{ ...inputProps }
|
||||
ref={ ( node ) => {
|
||||
inputRef.current = node;
|
||||
if ( typeof inputProps.ref === 'function' ) {
|
||||
inputRef.current = node;
|
||||
(
|
||||
inputProps.ref as unknown as (
|
||||
node: HTMLInputElement | null
|
||||
|
|
|
@ -12,6 +12,18 @@
|
|||
border-color: var( --wp-admin-theme-color );
|
||||
}
|
||||
|
||||
&.is-read-only.is-multiple.has-selected-items {
|
||||
.woocommerce-experimental-select-control__combo-box-wrapper {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.woocommerce-experimental-select-control__input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: inline-block;
|
||||
margin-bottom: $gap-smaller;
|
||||
|
|
|
@ -9,13 +9,14 @@ import {
|
|||
useMultipleSelection,
|
||||
GetInputPropsOptions,
|
||||
} from 'downshift';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
createElement,
|
||||
Fragment,
|
||||
} from '@wordpress/element';
|
||||
import { search } from '@wordpress/icons';
|
||||
import { chevronDown } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -123,12 +124,16 @@ function SelectControl< ItemType = DefaultItemType >( {
|
|||
className,
|
||||
disabled,
|
||||
inputProps = {},
|
||||
suffix = <SuffixIcon icon={ search } />,
|
||||
suffix = <SuffixIcon icon={ chevronDown } />,
|
||||
showToggleButton = false,
|
||||
__experimentalOpenMenuOnFocus = false,
|
||||
}: SelectControlProps< ItemType > ) {
|
||||
const [ isFocused, setIsFocused ] = useState( false );
|
||||
const [ inputValue, setInputValue ] = useState( '' );
|
||||
const instanceId = useInstanceId(
|
||||
SelectControl,
|
||||
'woocommerce-experimental-select-control'
|
||||
);
|
||||
|
||||
let selectedItems = selected === null ? [] : selected;
|
||||
selectedItems = Array.isArray( selectedItems )
|
||||
|
@ -230,15 +235,24 @@ function SelectControl< ItemType = DefaultItemType >( {
|
|||
},
|
||||
} );
|
||||
|
||||
const isEventOutside = ( event: React.FocusEvent ) => {
|
||||
return ! document
|
||||
.querySelector( '.' + instanceId )
|
||||
?.contains( event.relatedTarget );
|
||||
};
|
||||
|
||||
const onRemoveItem = ( item: ItemType ) => {
|
||||
selectItem( null );
|
||||
removeSelectedItem( item );
|
||||
onRemove( item );
|
||||
};
|
||||
|
||||
const isReadOnly = ! isOpen && ! isFocused;
|
||||
|
||||
const selectedItemTags = multiple ? (
|
||||
<SelectedItems
|
||||
items={ selectedItems }
|
||||
isReadOnly={ isReadOnly }
|
||||
getItemLabel={ getItemLabel }
|
||||
getItemValue={ getItemValue }
|
||||
getSelectedItemProps={ getSelectedItemProps }
|
||||
|
@ -251,8 +265,12 @@ function SelectControl< ItemType = DefaultItemType >( {
|
|||
className={ classnames(
|
||||
'woocommerce-experimental-select-control',
|
||||
className,
|
||||
instanceId,
|
||||
{
|
||||
'is-read-only': isReadOnly,
|
||||
'is-focused': isFocused,
|
||||
'is-multiple': multiple,
|
||||
'has-selected-items': selectedItems.length,
|
||||
}
|
||||
) }
|
||||
>
|
||||
|
@ -282,7 +300,11 @@ function SelectControl< ItemType = DefaultItemType >( {
|
|||
openMenu();
|
||||
}
|
||||
},
|
||||
onBlur: () => setIsFocused( false ),
|
||||
onBlur: ( event: React.FocusEvent ) => {
|
||||
if ( isEventOutside( event ) ) {
|
||||
setIsFocused( false );
|
||||
}
|
||||
},
|
||||
placeholder,
|
||||
disabled,
|
||||
...inputProps,
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
.woocommerce-experimental-select-control__selected-items.is-read-only {
|
||||
font-size: 13px;
|
||||
color: $gray-900;
|
||||
font-family: var(--wp--preset--font-family--system-font);
|
||||
}
|
||||
|
||||
.woocommerce-experimental-select-control__selected-item {
|
||||
margin-right: $gap-smallest;
|
||||
margin-top: 2px;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement, Fragment } from '@wordpress/element';
|
||||
import classnames from 'classnames';
|
||||
import { createElement } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -10,6 +11,7 @@ import Tag from '../tag';
|
|||
import { getItemLabelType, getItemValueType } from './types';
|
||||
|
||||
type SelectedItemsProps< ItemType > = {
|
||||
isReadOnly: boolean;
|
||||
items: ItemType[];
|
||||
getItemLabel: getItemLabelType< ItemType >;
|
||||
getItemValue: getItemValueType< ItemType >;
|
||||
|
@ -22,14 +24,34 @@ type SelectedItemsProps< ItemType > = {
|
|||
};
|
||||
|
||||
export const SelectedItems = < ItemType, >( {
|
||||
isReadOnly,
|
||||
items,
|
||||
getItemLabel,
|
||||
getItemValue,
|
||||
getSelectedItemProps,
|
||||
onRemove,
|
||||
}: SelectedItemsProps< ItemType > ) => {
|
||||
const classes = classnames(
|
||||
'woocommerce-experimental-select-control__selected-items',
|
||||
{
|
||||
'is-read-only': isReadOnly,
|
||||
}
|
||||
);
|
||||
|
||||
if ( isReadOnly ) {
|
||||
return (
|
||||
<div className={ classes }>
|
||||
{ items
|
||||
.map( ( item ) => {
|
||||
return getItemLabel( item );
|
||||
} )
|
||||
.join( ', ' ) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={ classes }>
|
||||
{ items.map( ( item, index ) => {
|
||||
return (
|
||||
// Disable reason: We prevent the default action to keep the input focused on click.
|
||||
|
@ -42,6 +64,9 @@ export const SelectedItems = < ItemType, >( {
|
|||
selectedItem: item,
|
||||
index,
|
||||
} ) }
|
||||
onMouseDown={ ( event ) => {
|
||||
event.preventDefault();
|
||||
} }
|
||||
onClick={ ( event ) => {
|
||||
event.preventDefault();
|
||||
} }
|
||||
|
@ -56,6 +81,6 @@ export const SelectedItems = < ItemType, >( {
|
|||
</div>
|
||||
);
|
||||
} ) }
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from '../experimental-tree-control';
|
||||
|
||||
type MenuProps = {
|
||||
isEventOutside: ( event: React.FocusEvent ) => boolean;
|
||||
isOpen: boolean;
|
||||
isLoading?: boolean;
|
||||
position?: Popover.Position;
|
||||
|
@ -32,6 +33,7 @@ type MenuProps = {
|
|||
} & Omit< TreeControlProps, 'items' >;
|
||||
|
||||
export const SelectTreeMenu = ( {
|
||||
isEventOutside,
|
||||
isLoading,
|
||||
isOpen,
|
||||
className,
|
||||
|
@ -103,8 +105,10 @@ export const SelectTreeMenu = ( {
|
|||
) }
|
||||
position={ position }
|
||||
animate={ false }
|
||||
onFocusOutside={ () => {
|
||||
onClose();
|
||||
onFocusOutside={ ( event ) => {
|
||||
if ( isEventOutside( event ) ) {
|
||||
onClose();
|
||||
}
|
||||
} }
|
||||
>
|
||||
{ isOpen && (
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement, useState } from '@wordpress/element';
|
||||
import { chevronDown } from '@wordpress/icons';
|
||||
import classNames from 'classnames';
|
||||
import { search } from '@wordpress/icons';
|
||||
import { createElement, useState } from '@wordpress/element';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
|
||||
/**
|
||||
|
@ -32,7 +32,7 @@ export const SelectTree = function SelectTree( {
|
|||
items,
|
||||
getSelectedItemProps,
|
||||
treeRef: ref,
|
||||
suffix = <SuffixIcon icon={ search } />,
|
||||
suffix = <SuffixIcon icon={ chevronDown } />,
|
||||
placeholder,
|
||||
isLoading,
|
||||
onInputChange,
|
||||
|
@ -40,24 +40,37 @@ export const SelectTree = function SelectTree( {
|
|||
...props
|
||||
}: SelectTreeProps ) {
|
||||
const linkedTree = useLinkedTree( items );
|
||||
const selectTreeInstanceId = useInstanceId(
|
||||
SelectTree,
|
||||
'woocommerce-experimental-select-tree-control__dropdown'
|
||||
);
|
||||
const menuInstanceId = useInstanceId(
|
||||
SelectTree,
|
||||
'woocommerce-select-tree-control__menu'
|
||||
);
|
||||
const isEventOutside = ( event: React.FocusEvent ) => {
|
||||
return ! document
|
||||
.querySelector( '.' + selectTreeInstanceId )
|
||||
?.contains( event.relatedTarget );
|
||||
};
|
||||
|
||||
const [ isFocused, setIsFocused ] = useState( false );
|
||||
const [ isOpen, setIsOpen ] = useState( false );
|
||||
const isReadOnly = ! isOpen && ! isFocused;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="woocommerce-experimental-select-tree-control__dropdown"
|
||||
className={ `woocommerce-experimental-select-tree-control__dropdown ${ selectTreeInstanceId }` }
|
||||
tabIndex={ -1 }
|
||||
>
|
||||
<div
|
||||
className={ classNames(
|
||||
'woocommerce-experimental-select-control',
|
||||
{
|
||||
'is-read-only': isReadOnly,
|
||||
'is-focused': isFocused,
|
||||
'is-multiple': props.multiple,
|
||||
'has-selected-items': props.selected?.length,
|
||||
}
|
||||
) }
|
||||
>
|
||||
|
@ -93,12 +106,7 @@ export const SelectTree = function SelectTree( {
|
|||
},
|
||||
onBlur: ( event ) => {
|
||||
// if blurring to an element inside the dropdown, don't close it
|
||||
if (
|
||||
isOpen &&
|
||||
! document
|
||||
.querySelector( '.' + menuInstanceId )
|
||||
?.contains( event.relatedTarget )
|
||||
) {
|
||||
if ( isEventOutside( event ) ) {
|
||||
setIsOpen( false );
|
||||
}
|
||||
setIsFocused( false );
|
||||
|
@ -126,13 +134,13 @@ export const SelectTree = function SelectTree( {
|
|||
suffix={ suffix }
|
||||
>
|
||||
<SelectedItems
|
||||
isReadOnly={ isReadOnly }
|
||||
items={ ( props.selected as Item[] ) || [] }
|
||||
getItemLabel={ ( item ) => item?.label || '' }
|
||||
getItemValue={ ( item ) => item?.value || '' }
|
||||
onRemove={ ( item ) => {
|
||||
if ( ! Array.isArray( item ) && props.onRemove ) {
|
||||
props.onRemove( item );
|
||||
setIsOpen( false );
|
||||
}
|
||||
} }
|
||||
getSelectedItemProps={ () => ( {} ) }
|
||||
|
@ -144,10 +152,13 @@ export const SelectTree = function SelectTree( {
|
|||
id={ `${ props.id }-menu` }
|
||||
className={ menuInstanceId.toString() }
|
||||
ref={ ref }
|
||||
isEventOutside={ isEventOutside }
|
||||
isOpen={ isOpen }
|
||||
items={ linkedTree }
|
||||
shouldShowCreateButton={ shouldShowCreateButton }
|
||||
onClose={ () => setIsOpen( false ) }
|
||||
onClose={ () => {
|
||||
setIsOpen( false );
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -65,8 +65,8 @@ The `config` prop has the following structure:
|
|||
|
||||
- `label`: String - A label above the filter selector.
|
||||
- `staticParams`: Array - Url parameters to persist when selecting a new filter.
|
||||
- `param`: String - The url paramter this filter will modify.
|
||||
- `defaultValue`: String - The default paramter value to use instead of 'all'.
|
||||
- `param`: String - The url parameter this filter will modify.
|
||||
- `defaultValue`: String - The default parameter value to use instead of 'all'.
|
||||
- `showFilters`: Function - Determine if the filter should be shown. Supply a function with the query object as an argument returning a boolean.
|
||||
- `filters`: Array - Array of filter objects.
|
||||
|
||||
|
|
|
@ -374,11 +374,11 @@ FilterPicker.propTypes = {
|
|||
*/
|
||||
staticParams: PropTypes.array.isRequired,
|
||||
/**
|
||||
* The url paramter this filter will modify.
|
||||
* The url parameter this filter will modify.
|
||||
*/
|
||||
param: PropTypes.string.isRequired,
|
||||
/**
|
||||
* The default paramter value to use instead of 'all'.
|
||||
* The default parameter value to use instead of 'all'.
|
||||
*/
|
||||
defaultValue: PropTypes.string,
|
||||
/**
|
||||
|
|
|
@ -45,7 +45,7 @@ export const EditorWritingFlow = ( {
|
|||
};
|
||||
} );
|
||||
|
||||
// This is a workaround to prevent focusing the block on intialization.
|
||||
// This is a workaround to prevent focusing the block on initialization.
|
||||
// Changing to a mode other than "edit" ensures that no initial position
|
||||
// is found and no element gets subsequently focused.
|
||||
// See https://github.com/WordPress/gutenberg/blob/411b6eee8376e31bf9db4c15c92a80524ae38e9b/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js#L42
|
||||
|
|
|
@ -153,7 +153,7 @@ describe( 'buildTermsTree', () => {
|
|||
] );
|
||||
} );
|
||||
|
||||
test( 'should return a tree of items, with orphan categories appended to the end, with children of thier own', () => {
|
||||
test( 'should return a tree of items, with orphan categories appended to the end, with children of their own', () => {
|
||||
const filteredList = [
|
||||
{ id: 1, name: 'Apricots', parent: 0 },
|
||||
{ id: 3, name: 'Elderberry', parent: 2 },
|
||||
|
|
|
@ -6,18 +6,149 @@ import { BACKSPACE, DOWN, UP } from '@wordpress/keycodes';
|
|||
import { createElement, Component, createRef } from '@wordpress/element';
|
||||
import { Icon, search } from '@wordpress/icons';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isArray } from 'lodash';
|
||||
import {
|
||||
RefObject,
|
||||
ChangeEvent,
|
||||
FocusEvent,
|
||||
KeyboardEvent,
|
||||
InputHTMLAttributes,
|
||||
} from 'react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Tags from './tags';
|
||||
import { Selected, Option } from './types';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Bool to determine if tags should be rendered.
|
||||
*/
|
||||
hasTags?: boolean;
|
||||
/**
|
||||
* Help text to be appended beneath the input.
|
||||
*/
|
||||
help?: string | JSX.Element;
|
||||
/**
|
||||
* Render tags inside input, otherwise render below input.
|
||||
*/
|
||||
inlineTags?: boolean;
|
||||
/**
|
||||
* Allow the select options to be filtered by search input.
|
||||
*/
|
||||
isSearchable?: boolean;
|
||||
/**
|
||||
* ID of the main SelectControl instance.
|
||||
*/
|
||||
instanceId?: number;
|
||||
/**
|
||||
* A label to use for the main input.
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* ID used for a11y in the listbox.
|
||||
*/
|
||||
listboxId?: string;
|
||||
/**
|
||||
* Function called when the input is blurred.
|
||||
*/
|
||||
onBlur?: () => void;
|
||||
/**
|
||||
* Function called when selected results change, passed result list.
|
||||
*/
|
||||
onChange: ( selected: Option[] ) => void;
|
||||
/**
|
||||
* Function called when input field is changed or focused.
|
||||
*/
|
||||
onSearch: ( query: string ) => void;
|
||||
/**
|
||||
* A placeholder for the search input.
|
||||
*/
|
||||
placeholder?: string;
|
||||
/**
|
||||
* Search query entered by user.
|
||||
*/
|
||||
query?: string | null;
|
||||
/**
|
||||
* An array of objects describing selected values. If the label of the selected
|
||||
* value is omitted, the Tag of that value will not be rendered inside the
|
||||
* search box.
|
||||
*/
|
||||
selected?: Selected;
|
||||
/**
|
||||
* Show all options on focusing, even if a query exists.
|
||||
*/
|
||||
showAllOnFocus?: boolean;
|
||||
/**
|
||||
* Control input autocomplete field, defaults: off.
|
||||
*/
|
||||
autoComplete?: string;
|
||||
/**
|
||||
* Function to execute when the control should be expanded or collapsed.
|
||||
*/
|
||||
setExpanded: ( expanded: boolean ) => void;
|
||||
/**
|
||||
* Function to execute when the search value changes.
|
||||
*/
|
||||
updateSearchOptions: ( query: string ) => void;
|
||||
/**
|
||||
* Function to execute when keyboard navigation should decrement the selected index.
|
||||
*/
|
||||
decrementSelectedIndex: () => void;
|
||||
/**
|
||||
* Function to execute when keyboard navigation should increment the selected index.
|
||||
*/
|
||||
incrementSelectedIndex: () => void;
|
||||
/**
|
||||
* Multi-select mode allows multiple options to be selected.
|
||||
*/
|
||||
multiple?: boolean;
|
||||
/**
|
||||
* Is the control currently focused.
|
||||
*/
|
||||
isFocused?: boolean;
|
||||
/**
|
||||
* ID for accessibility purposes. aria-activedescendant will be set to this value.
|
||||
*/
|
||||
activeId?: string;
|
||||
/**
|
||||
* Disable the control.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Is the control currently expanded. This is for accessibility purposes.
|
||||
*/
|
||||
isExpanded?: boolean;
|
||||
/**
|
||||
* The type of input to use for the search field.
|
||||
*/
|
||||
searchInputType?: InputHTMLAttributes< HTMLInputElement >[ 'type' ];
|
||||
/**
|
||||
* The aria label for the search input.
|
||||
*/
|
||||
ariaLabel?: string;
|
||||
/**
|
||||
* Class name to be added to the input.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Show the clear button.
|
||||
*/
|
||||
showClearButton?: boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A search control to allow user input to filter the options.
|
||||
*/
|
||||
class Control extends Component {
|
||||
constructor( props ) {
|
||||
class Control extends Component< Props, State > {
|
||||
input: RefObject< HTMLInputElement >;
|
||||
|
||||
constructor( props: Props ) {
|
||||
super( props );
|
||||
this.state = {
|
||||
isActive: false,
|
||||
|
@ -31,13 +162,13 @@ class Control extends Component {
|
|||
this.onKeyDown = this.onKeyDown.bind( this );
|
||||
}
|
||||
|
||||
updateSearch( onSearch ) {
|
||||
return ( event ) => {
|
||||
updateSearch( onSearch: ( query: string ) => void ) {
|
||||
return ( event: ChangeEvent< HTMLInputElement > ) => {
|
||||
onSearch( event.target.value );
|
||||
};
|
||||
}
|
||||
|
||||
onFocus( onSearch ) {
|
||||
onFocus( onSearch: ( query: string ) => void ) {
|
||||
const {
|
||||
isSearchable,
|
||||
setExpanded,
|
||||
|
@ -45,7 +176,7 @@ class Control extends Component {
|
|||
updateSearchOptions,
|
||||
} = this.props;
|
||||
|
||||
return ( event ) => {
|
||||
return ( event: FocusEvent< HTMLInputElement > ) => {
|
||||
this.setState( { isActive: true } );
|
||||
if ( isSearchable && showAllOnFocus ) {
|
||||
event.target.select();
|
||||
|
@ -68,7 +199,7 @@ class Control extends Component {
|
|||
this.setState( { isActive: false } );
|
||||
}
|
||||
|
||||
onKeyDown( event ) {
|
||||
onKeyDown( event: KeyboardEvent< HTMLInputElement > ) {
|
||||
const {
|
||||
decrementSelectedIndex,
|
||||
incrementSelectedIndex,
|
||||
|
@ -78,7 +209,12 @@ class Control extends Component {
|
|||
setExpanded,
|
||||
} = this.props;
|
||||
|
||||
if ( BACKSPACE === event.keyCode && ! query && selected.length ) {
|
||||
if (
|
||||
BACKSPACE === event.keyCode &&
|
||||
! query &&
|
||||
isArray( selected ) &&
|
||||
selected.length
|
||||
) {
|
||||
onChange( [ ...selected.slice( 0, -1 ) ] );
|
||||
}
|
||||
|
||||
|
@ -100,7 +236,7 @@ class Control extends Component {
|
|||
renderButton() {
|
||||
const { multiple, selected } = this.props;
|
||||
|
||||
if ( multiple || ! selected.length ) {
|
||||
if ( multiple || ! isArray( selected ) || ! selected.length ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -151,7 +287,7 @@ class Control extends Component {
|
|||
aria-describedby={
|
||||
hasTags && inlineTags
|
||||
? `search-inline-input-${ instanceId }`
|
||||
: null
|
||||
: undefined
|
||||
}
|
||||
disabled={ disabled }
|
||||
aria-label={ this.props.ariaLabel ?? this.props.label }
|
||||
|
@ -168,7 +304,8 @@ class Control extends Component {
|
|||
query,
|
||||
selected,
|
||||
} = this.props;
|
||||
const selectedValue = selected.length ? selected[ 0 ].label : '';
|
||||
const selectedValue =
|
||||
isArray( selected ) && selected.length ? selected[ 0 ].label : '';
|
||||
|
||||
// Show the selected value for simple select dropdowns.
|
||||
if ( ! multiple && ! isFocused && ! inlineTags ) {
|
||||
|
@ -194,6 +331,8 @@ class Control extends Component {
|
|||
isSearchable,
|
||||
label,
|
||||
query,
|
||||
onChange,
|
||||
showClearButton,
|
||||
} = this.props;
|
||||
const { isActive } = this.state;
|
||||
|
||||
|
@ -213,7 +352,7 @@ class Control extends Component {
|
|||
empty: ! query || query.length === 0,
|
||||
'is-active': isActive,
|
||||
'has-tags': inlineTags && hasTags,
|
||||
'with-value': this.getInputValue().length,
|
||||
'with-value': this.getInputValue()?.length,
|
||||
'has-error': !! help,
|
||||
'is-disabled': disabled,
|
||||
}
|
||||
|
@ -221,8 +360,11 @@ class Control extends Component {
|
|||
onClick={ ( event ) => {
|
||||
// Don't focus the input if the click event is from the error message.
|
||||
if (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - event.target.className is not in the type definition.
|
||||
event.target.className !==
|
||||
'components-base-control__help'
|
||||
'components-base-control__help' &&
|
||||
this.input.current
|
||||
) {
|
||||
this.input.current.focus();
|
||||
}
|
||||
|
@ -234,7 +376,13 @@ class Control extends Component {
|
|||
icon={ search }
|
||||
/>
|
||||
) }
|
||||
{ inlineTags && <Tags { ...this.props } /> }
|
||||
{ inlineTags && (
|
||||
<Tags
|
||||
onChange={ onChange }
|
||||
showClearButton={ showClearButton }
|
||||
selected={ this.props.selected }
|
||||
/>
|
||||
) }
|
||||
|
||||
<div className="components-base-control__field">
|
||||
{ !! label && (
|
||||
|
@ -272,75 +420,4 @@ class Control extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
Control.propTypes = {
|
||||
/**
|
||||
* Bool to determine if tags should be rendered.
|
||||
*/
|
||||
hasTags: PropTypes.bool,
|
||||
/**
|
||||
* Help text to be appended beneath the input.
|
||||
*/
|
||||
help: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ),
|
||||
/**
|
||||
* Render tags inside input, otherwise render below input.
|
||||
*/
|
||||
inlineTags: PropTypes.bool,
|
||||
/**
|
||||
* Allow the select options to be filtered by search input.
|
||||
*/
|
||||
isSearchable: PropTypes.bool,
|
||||
/**
|
||||
* ID of the main SelectControl instance.
|
||||
*/
|
||||
instanceId: PropTypes.number,
|
||||
/**
|
||||
* A label to use for the main input.
|
||||
*/
|
||||
label: PropTypes.string,
|
||||
/**
|
||||
* ID used for a11y in the listbox.
|
||||
*/
|
||||
listboxId: PropTypes.string,
|
||||
/**
|
||||
* Function called when the input is blurred.
|
||||
*/
|
||||
onBlur: PropTypes.func,
|
||||
/**
|
||||
* Function called when selected results change, passed result list.
|
||||
*/
|
||||
onChange: PropTypes.func,
|
||||
/**
|
||||
* Function called when input field is changed or focused.
|
||||
*/
|
||||
onSearch: PropTypes.func,
|
||||
/**
|
||||
* A placeholder for the search input.
|
||||
*/
|
||||
placeholder: PropTypes.string,
|
||||
/**
|
||||
* Search query entered by user.
|
||||
*/
|
||||
query: PropTypes.string,
|
||||
/**
|
||||
* An array of objects describing selected values. If the label of the selected
|
||||
* value is omitted, the Tag of that value will not be rendered inside the
|
||||
* search box.
|
||||
*/
|
||||
selected: PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] )
|
||||
.isRequired,
|
||||
label: PropTypes.string,
|
||||
} )
|
||||
),
|
||||
/**
|
||||
* Show all options on focusing, even if a query exists.
|
||||
*/
|
||||
showAllOnFocus: PropTypes.bool,
|
||||
/**
|
||||
* Control input autocomplete field, defaults: off.
|
||||
*/
|
||||
autoComplete: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Control;
|
|
@ -4,26 +4,205 @@
|
|||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import { Component, createElement } from '@wordpress/element';
|
||||
import { debounce, escapeRegExp, identity, noop } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
debounce,
|
||||
escapeRegExp,
|
||||
identity,
|
||||
isArray,
|
||||
isNumber,
|
||||
noop,
|
||||
} from 'lodash';
|
||||
import { withFocusOutside, withSpokenMessages } from '@wordpress/components';
|
||||
import { withInstanceId, compose } from '@wordpress/compose';
|
||||
import { ChangeEvent, InputHTMLAttributes } from 'react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Option, Selected } from './types';
|
||||
import List from './list';
|
||||
import Tags from './tags';
|
||||
import Control from './control';
|
||||
|
||||
const initialState = { isExpanded: false, isFocused: false, query: '' };
|
||||
type Props = {
|
||||
/**
|
||||
* Name to use for the autofill field, not used if no string is passed.
|
||||
*/
|
||||
autofill?: string;
|
||||
/**
|
||||
* A renderable component (or string) which will be displayed before the `Control` of this component.
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
/**
|
||||
* Class name applied to parent div.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Class name applied to control wrapper.
|
||||
*/
|
||||
controlClassName?: string;
|
||||
/**
|
||||
* Allow the select options to be disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Exclude already selected options from the options list.
|
||||
*/
|
||||
excludeSelectedOptions?: boolean;
|
||||
/**
|
||||
* Add or remove items to the list of options after filtering,
|
||||
* passed the array of filtered options and should return an array of options.
|
||||
*/
|
||||
onFilter?: (
|
||||
options: Array< Option >,
|
||||
query: string | null
|
||||
) => Array< Option >;
|
||||
/**
|
||||
* Function to add regex expression to the filter the results, passed the search query.
|
||||
*/
|
||||
getSearchExpression?: ( query: string ) => RegExp;
|
||||
/**
|
||||
* Help text to be appended beneath the input.
|
||||
*/
|
||||
help?: string | JSX.Element;
|
||||
/**
|
||||
* Render tags inside input, otherwise render below input.
|
||||
*/
|
||||
inlineTags?: boolean;
|
||||
/**
|
||||
* Allow the select options to be filtered by search input.
|
||||
*/
|
||||
isSearchable?: boolean;
|
||||
/**
|
||||
* A label to use for the main input.
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* Function called when selected results change, passed result list.
|
||||
*/
|
||||
onChange?: ( selected: string | Option[], query?: string | null ) => void;
|
||||
/**
|
||||
* Function run after search query is updated, passed previousOptions and query,
|
||||
* should return a promise with an array of updated options.
|
||||
*/
|
||||
onSearch?: (
|
||||
previousOptions: Array< Option >,
|
||||
query: string | null
|
||||
) => Promise< Array< Option > >;
|
||||
/**
|
||||
* An array of objects for the options list. The option along with its key, label and
|
||||
* value will be returned in the onChange event.
|
||||
*/
|
||||
options: Option[];
|
||||
/**
|
||||
* A placeholder for the search input.
|
||||
*/
|
||||
placeholder?: string;
|
||||
/**
|
||||
* Time in milliseconds to debounce the search function after typing.
|
||||
*/
|
||||
searchDebounceTime?: number;
|
||||
/**
|
||||
* An array of objects describing selected values or optionally a string for a single value.
|
||||
* If the label of the selected value is omitted, the Tag of that value will not
|
||||
* be rendered inside the search box.
|
||||
*/
|
||||
selected?: Selected;
|
||||
/**
|
||||
* A limit for the number of results shown in the options menu. Set to 0 for no limit.
|
||||
*/
|
||||
maxResults?: number;
|
||||
/**
|
||||
* Allow multiple option selections.
|
||||
*/
|
||||
multiple?: boolean;
|
||||
/**
|
||||
* Render a 'Clear' button next to the input box to remove its contents.
|
||||
*/
|
||||
showClearButton?: boolean;
|
||||
/**
|
||||
* The input type for the search box control.
|
||||
*/
|
||||
searchInputType?: InputHTMLAttributes< HTMLInputElement >[ 'type' ];
|
||||
/**
|
||||
* Only show list options after typing a search query.
|
||||
*/
|
||||
hideBeforeSearch?: boolean;
|
||||
/**
|
||||
* Show all options on focusing, even if a query exists.
|
||||
*/
|
||||
showAllOnFocus?: boolean;
|
||||
/**
|
||||
* Render results list positioned statically instead of absolutely.
|
||||
*/
|
||||
staticList?: boolean;
|
||||
/**
|
||||
* autocomplete prop for the Control input field.
|
||||
*/
|
||||
autoComplete?: string;
|
||||
/**
|
||||
* Instance ID for the component.
|
||||
*/
|
||||
instanceId?: number;
|
||||
/**
|
||||
* From withSpokenMessages
|
||||
*/
|
||||
debouncedSpeak?: ( message: string, assertive?: string ) => void;
|
||||
/**
|
||||
* aria-label for the search input.
|
||||
*/
|
||||
ariaLabel?: string;
|
||||
/**
|
||||
* On Blur callback.
|
||||
*/
|
||||
onBlur?: () => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
isExpanded: boolean;
|
||||
isFocused: boolean;
|
||||
query: string | null;
|
||||
searchOptions: Option[];
|
||||
selectedIndex?: number | null;
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
isExpanded: false,
|
||||
isFocused: false,
|
||||
query: '',
|
||||
searchOptions: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* A search box which filters options while typing,
|
||||
* allowing a user to select from an option from a filtered list.
|
||||
*/
|
||||
export class SelectControl extends Component {
|
||||
constructor( props ) {
|
||||
export class SelectControl extends Component< Props, State > {
|
||||
static defaultProps: Partial< Props > = {
|
||||
excludeSelectedOptions: true,
|
||||
getSearchExpression: identity,
|
||||
inlineTags: false,
|
||||
isSearchable: false,
|
||||
onChange: noop,
|
||||
onFilter: identity,
|
||||
onSearch: ( options: Option[] ) => Promise.resolve( options ),
|
||||
maxResults: 0,
|
||||
multiple: false,
|
||||
searchDebounceTime: 0,
|
||||
searchInputType: 'search',
|
||||
selected: [],
|
||||
showAllOnFocus: false,
|
||||
showClearButton: false,
|
||||
hideBeforeSearch: false,
|
||||
staticList: false,
|
||||
autoComplete: 'off',
|
||||
};
|
||||
|
||||
node: HTMLDivElement | null = null;
|
||||
activePromise: Promise< void | Option[] > | null = null;
|
||||
cacheSearchOptions: Option[] = [];
|
||||
|
||||
constructor( props: Props ) {
|
||||
super( props );
|
||||
|
||||
const { selected, options, excludeSelectedOptions } = props;
|
||||
|
@ -50,7 +229,7 @@ export class SelectControl extends Component {
|
|||
this.setNewValue = this.setNewValue.bind( this );
|
||||
}
|
||||
|
||||
bindNode( node ) {
|
||||
bindNode( node: HTMLDivElement ) {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
|
@ -58,7 +237,12 @@ export class SelectControl extends Component {
|
|||
const { multiple, excludeSelectedOptions } = this.props;
|
||||
const newState = { ...initialState };
|
||||
// Reset selectedIndex if single selection.
|
||||
if ( ! multiple && selected.length && selected[ 0 ].key ) {
|
||||
if (
|
||||
! multiple &&
|
||||
isArray( selected ) &&
|
||||
selected.length &&
|
||||
selected[ 0 ].key
|
||||
) {
|
||||
newState.selectedIndex = ! excludeSelectedOptions
|
||||
? this.props.options.findIndex(
|
||||
( i ) => i.key === selected[ 0 ].key
|
||||
|
@ -101,9 +285,12 @@ export class SelectControl extends Component {
|
|||
return selectedOption ? [ selectedOption ] : [];
|
||||
}
|
||||
|
||||
selectOption( option ) {
|
||||
selectOption( option: Option ) {
|
||||
const { multiple, selected } = this.props;
|
||||
const newSelected = multiple ? [ ...selected, option ] : [ option ];
|
||||
const newSelected =
|
||||
multiple && isArray( selected )
|
||||
? [ ...selected, option ]
|
||||
: [ option ];
|
||||
|
||||
this.reset( newSelected );
|
||||
|
||||
|
@ -129,25 +316,24 @@ export class SelectControl extends Component {
|
|||
} );
|
||||
}
|
||||
|
||||
setNewValue( newValue ) {
|
||||
setNewValue( newValue: Option[] ) {
|
||||
const { onChange, selected, multiple } = this.props;
|
||||
const { query } = this.state;
|
||||
// Trigger a change if the selected value is different and pass back
|
||||
// an array or string depending on the original value.
|
||||
if ( multiple || Array.isArray( selected ) ) {
|
||||
onChange( newValue, query );
|
||||
onChange!( newValue, query );
|
||||
} else {
|
||||
onChange( newValue.length > 0 ? newValue[ 0 ].key : '', query );
|
||||
onChange!( newValue.length > 0 ? newValue[ 0 ].key : '', query );
|
||||
}
|
||||
}
|
||||
|
||||
decrementSelectedIndex() {
|
||||
const { selectedIndex } = this.state;
|
||||
const options = this.getOptions();
|
||||
const nextSelectedIndex =
|
||||
selectedIndex !== null
|
||||
? ( selectedIndex === 0 ? options.length : selectedIndex ) - 1
|
||||
: options.length - 1;
|
||||
const nextSelectedIndex = isNumber( selectedIndex )
|
||||
? ( selectedIndex === 0 ? options.length : selectedIndex ) - 1
|
||||
: options.length - 1;
|
||||
|
||||
this.setState( { selectedIndex: nextSelectedIndex } );
|
||||
}
|
||||
|
@ -155,13 +341,14 @@ export class SelectControl extends Component {
|
|||
incrementSelectedIndex() {
|
||||
const { selectedIndex } = this.state;
|
||||
const options = this.getOptions();
|
||||
const nextSelectedIndex =
|
||||
selectedIndex !== null ? ( selectedIndex + 1 ) % options.length : 0;
|
||||
const nextSelectedIndex = isNumber( selectedIndex )
|
||||
? ( selectedIndex + 1 ) % options.length
|
||||
: 0;
|
||||
|
||||
this.setState( { selectedIndex: nextSelectedIndex } );
|
||||
}
|
||||
|
||||
announce( searchOptions ) {
|
||||
announce( searchOptions: Option[] ) {
|
||||
const { debouncedSpeak } = this.props;
|
||||
if ( ! debouncedSpeak ) {
|
||||
return;
|
||||
|
@ -169,6 +356,7 @@ export class SelectControl extends Component {
|
|||
if ( !! searchOptions.length ) {
|
||||
debouncedSpeak(
|
||||
sprintf(
|
||||
// translators: %d: number of results.
|
||||
_n(
|
||||
'%d result found, use up and down arrow keys to navigate.',
|
||||
'%d results found, use up and down arrow keys to navigate.',
|
||||
|
@ -187,23 +375,26 @@ export class SelectControl extends Component {
|
|||
getOptions() {
|
||||
const { isSearchable, options, excludeSelectedOptions } = this.props;
|
||||
const { searchOptions } = this.state;
|
||||
const selectedKeys = this.getSelected().map( ( option ) => option.key );
|
||||
const selected = this.getSelected();
|
||||
const selectedKeys = isArray( selected )
|
||||
? selected.map( ( option ) => option.key )
|
||||
: [];
|
||||
const shownOptions = isSearchable ? searchOptions : options;
|
||||
|
||||
if ( excludeSelectedOptions ) {
|
||||
return shownOptions.filter(
|
||||
return shownOptions?.filter(
|
||||
( option ) => ! selectedKeys.includes( option.key )
|
||||
);
|
||||
}
|
||||
return shownOptions;
|
||||
}
|
||||
|
||||
getOptionsByQuery( options, query ) {
|
||||
getOptionsByQuery( options: Option[], query: string | null ) {
|
||||
const { getSearchExpression, maxResults, onFilter } = this.props;
|
||||
const filtered = [];
|
||||
|
||||
// Create a regular expression to filter the options.
|
||||
const expression = getSearchExpression(
|
||||
const expression = getSearchExpression!(
|
||||
escapeRegExp( query ? query.trim() : '' )
|
||||
);
|
||||
const search = expression ? new RegExp( expression, 'i' ) : /^$/;
|
||||
|
@ -232,14 +423,14 @@ export class SelectControl extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
return onFilter( filtered, query );
|
||||
return onFilter!( filtered, query );
|
||||
}
|
||||
|
||||
setExpanded( value ) {
|
||||
setExpanded( value: boolean ) {
|
||||
this.setState( { isExpanded: value } );
|
||||
}
|
||||
|
||||
search( query ) {
|
||||
search( query: string | null ) {
|
||||
const cacheSearchOptions = this.cacheSearchOptions || [];
|
||||
const searchOptions =
|
||||
query !== null && ! query.length && ! this.props.hideBeforeSearch
|
||||
|
@ -252,11 +443,13 @@ export class SelectControl extends Component {
|
|||
isFocused: true,
|
||||
searchOptions,
|
||||
selectedIndex:
|
||||
query?.length > 0 ? null : this.state.selectedIndex, // Only reset selectedIndex if we're actually searching.
|
||||
query && query?.length > 0
|
||||
? null
|
||||
: this.state.selectedIndex, // Only reset selectedIndex if we're actually searching.
|
||||
},
|
||||
() => {
|
||||
this.setState( {
|
||||
isExpanded: Boolean( this.getOptions().length ),
|
||||
isExpanded: Boolean( this.getOptions()?.length ),
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
@ -264,11 +457,11 @@ export class SelectControl extends Component {
|
|||
this.updateSearchOptions( query );
|
||||
}
|
||||
|
||||
updateSearchOptions( query ) {
|
||||
updateSearchOptions( query: string | null ) {
|
||||
const { hideBeforeSearch, options, onSearch } = this.props;
|
||||
|
||||
const promise = ( this.activePromise = Promise.resolve(
|
||||
onSearch( options, query )
|
||||
onSearch!( options, query )
|
||||
).then( ( promiseOptions ) => {
|
||||
if ( promise !== this.activePromise ) {
|
||||
// Another promise has become active since this one was asked to resolve, so do nothing,
|
||||
|
@ -288,7 +481,9 @@ export class SelectControl extends Component {
|
|||
{
|
||||
searchOptions,
|
||||
selectedIndex:
|
||||
query?.length > 0 ? null : this.state.selectedIndex, // Only reset selectedIndex if we're actually searching.
|
||||
query && query?.length > 0
|
||||
? null
|
||||
: this.state.selectedIndex, // Only reset selectedIndex if we're actually searching.
|
||||
},
|
||||
() => {
|
||||
this.setState( {
|
||||
|
@ -300,7 +495,7 @@ export class SelectControl extends Component {
|
|||
} ) );
|
||||
}
|
||||
|
||||
onAutofillChange( event ) {
|
||||
onAutofillChange( event: ChangeEvent< HTMLInputElement > ) {
|
||||
const { options } = this.props;
|
||||
const searchOptions = this.getOptionsByQuery(
|
||||
options,
|
||||
|
@ -327,13 +522,14 @@ export class SelectControl extends Component {
|
|||
const { isExpanded, isFocused, selectedIndex } = this.state;
|
||||
|
||||
const hasMultiple = this.hasMultiple();
|
||||
const { key: selectedKey = '' } = options[ selectedIndex ] || {};
|
||||
const { key: selectedKey = '' } =
|
||||
( isNumber( selectedIndex ) && options[ selectedIndex ] ) || {};
|
||||
const listboxId = isExpanded
|
||||
? `woocommerce-select-control__listbox-${ instanceId }`
|
||||
: null;
|
||||
: undefined;
|
||||
const activeId = isExpanded
|
||||
? `woocommerce-select-control__option-${ instanceId }-${ selectedKey }`
|
||||
: null;
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -354,13 +550,25 @@ export class SelectControl extends Component {
|
|||
name={ autofill }
|
||||
type="text"
|
||||
className="woocommerce-select-control__autofill-input"
|
||||
tabIndex="-1"
|
||||
tabIndex={ -1 }
|
||||
/>
|
||||
) }
|
||||
{ children }
|
||||
<Control
|
||||
{ ...this.props }
|
||||
{ ...this.state }
|
||||
help={ this.props.help }
|
||||
label={ this.props.label }
|
||||
inlineTags={ inlineTags }
|
||||
isSearchable={ isSearchable }
|
||||
isFocused={ isFocused }
|
||||
instanceId={ instanceId }
|
||||
searchInputType={ this.props.searchInputType }
|
||||
query={ this.state.query }
|
||||
placeholder={ this.props.placeholder }
|
||||
autoComplete={ this.props.autoComplete }
|
||||
multiple={ this.props.multiple }
|
||||
ariaLabel={ this.props.ariaLabel }
|
||||
onBlur={ this.props.onBlur }
|
||||
showAllOnFocus={ this.props.showAllOnFocus }
|
||||
activeId={ activeId }
|
||||
className={ controlClassName }
|
||||
disabled={ disabled }
|
||||
|
@ -374,15 +582,20 @@ export class SelectControl extends Component {
|
|||
updateSearchOptions={ this.updateSearchOptions }
|
||||
decrementSelectedIndex={ this.decrementSelectedIndex }
|
||||
incrementSelectedIndex={ this.incrementSelectedIndex }
|
||||
showClearButton={ this.props.showClearButton }
|
||||
/>
|
||||
{ ! inlineTags && hasMultiple && (
|
||||
<Tags { ...this.props } selected={ this.getSelected() } />
|
||||
<Tags
|
||||
onChange={ this.props.onChange! }
|
||||
showClearButton={ this.props.showClearButton }
|
||||
selected={ this.getSelected() }
|
||||
/>
|
||||
) }
|
||||
{ isExpanded && (
|
||||
<List
|
||||
{ ...this.props }
|
||||
{ ...this.state }
|
||||
activeId={ activeId }
|
||||
instanceId={ instanceId! }
|
||||
selectedIndex={ selectedIndex }
|
||||
staticList={ this.props.staticList! }
|
||||
listboxId={ listboxId }
|
||||
node={ this.node }
|
||||
onSelect={ this.selectOption }
|
||||
|
@ -398,171 +611,6 @@ export class SelectControl extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
SelectControl.propTypes = {
|
||||
/**
|
||||
* Name to use for the autofill field, not used if no string is passed.
|
||||
*/
|
||||
autofill: PropTypes.string,
|
||||
/**
|
||||
* A renderable component (or string) which will be displayed before the `Control` of this component.
|
||||
*/
|
||||
children: PropTypes.node,
|
||||
/**
|
||||
* Class name applied to parent div.
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
/**
|
||||
* Class name applied to control wrapper.
|
||||
*/
|
||||
controlClassName: PropTypes.string,
|
||||
/**
|
||||
* Allow the select options to be disabled.
|
||||
*/
|
||||
disabled: PropTypes.bool,
|
||||
/**
|
||||
* Exclude already selected options from the options list.
|
||||
*/
|
||||
excludeSelectedOptions: PropTypes.bool,
|
||||
/**
|
||||
* Add or remove items to the list of options after filtering,
|
||||
* passed the array of filtered options and should return an array of options.
|
||||
*/
|
||||
onFilter: PropTypes.func,
|
||||
/**
|
||||
* Function to add regex expression to the filter the results, passed the search query.
|
||||
*/
|
||||
getSearchExpression: PropTypes.func,
|
||||
/**
|
||||
* Help text to be appended beneath the input.
|
||||
*/
|
||||
help: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ),
|
||||
/**
|
||||
* Render tags inside input, otherwise render below input.
|
||||
*/
|
||||
inlineTags: PropTypes.bool,
|
||||
/**
|
||||
* Allow the select options to be filtered by search input.
|
||||
*/
|
||||
isSearchable: PropTypes.bool,
|
||||
/**
|
||||
* A label to use for the main input.
|
||||
*/
|
||||
label: PropTypes.string,
|
||||
/**
|
||||
* Function called when selected results change, passed result list.
|
||||
*/
|
||||
onChange: PropTypes.func,
|
||||
/**
|
||||
* Function run after search query is updated, passed previousOptions and query,
|
||||
* should return a promise with an array of updated options.
|
||||
*/
|
||||
onSearch: PropTypes.func,
|
||||
/**
|
||||
* An array of objects for the options list. The option along with its key, label and
|
||||
* value will be returned in the onChange event.
|
||||
*/
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
isDisabled: PropTypes.bool,
|
||||
key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] )
|
||||
.isRequired,
|
||||
keywords: PropTypes.arrayOf(
|
||||
PropTypes.oneOfType( [ PropTypes.string, PropTypes.number ] )
|
||||
),
|
||||
label: PropTypes.oneOfType( [
|
||||
PropTypes.string,
|
||||
PropTypes.object,
|
||||
] ),
|
||||
value: PropTypes.any,
|
||||
} )
|
||||
).isRequired,
|
||||
/**
|
||||
* A placeholder for the search input.
|
||||
*/
|
||||
placeholder: PropTypes.string,
|
||||
/**
|
||||
* Time in milliseconds to debounce the search function after typing.
|
||||
*/
|
||||
searchDebounceTime: PropTypes.number,
|
||||
/**
|
||||
* An array of objects describing selected values or optionally a string for a single value.
|
||||
* If the label of the selected value is omitted, the Tag of that value will not
|
||||
* be rendered inside the search box.
|
||||
*/
|
||||
selected: PropTypes.oneOfType( [
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
key: PropTypes.oneOfType( [
|
||||
PropTypes.number,
|
||||
PropTypes.string,
|
||||
] ).isRequired,
|
||||
label: PropTypes.string,
|
||||
} )
|
||||
),
|
||||
] ),
|
||||
/**
|
||||
* A limit for the number of results shown in the options menu. Set to 0 for no limit.
|
||||
*/
|
||||
maxResults: PropTypes.number,
|
||||
/**
|
||||
* Allow multiple option selections.
|
||||
*/
|
||||
multiple: PropTypes.bool,
|
||||
/**
|
||||
* Render a 'Clear' button next to the input box to remove its contents.
|
||||
*/
|
||||
showClearButton: PropTypes.bool,
|
||||
/**
|
||||
* The input type for the search box control.
|
||||
*/
|
||||
searchInputType: PropTypes.oneOf( [
|
||||
'text',
|
||||
'search',
|
||||
'number',
|
||||
'email',
|
||||
'tel',
|
||||
'url',
|
||||
] ),
|
||||
/**
|
||||
* Only show list options after typing a search query.
|
||||
*/
|
||||
hideBeforeSearch: PropTypes.bool,
|
||||
/**
|
||||
* Show all options on focusing, even if a query exists.
|
||||
*/
|
||||
showAllOnFocus: PropTypes.bool,
|
||||
/**
|
||||
* Render results list positioned statically instead of absolutely.
|
||||
*/
|
||||
staticList: PropTypes.bool,
|
||||
/**
|
||||
* autocomplete prop for the Control input field.
|
||||
*/
|
||||
autoComplete: PropTypes.string,
|
||||
};
|
||||
|
||||
SelectControl.defaultProps = {
|
||||
autofill: null,
|
||||
excludeSelectedOptions: true,
|
||||
getSearchExpression: identity,
|
||||
inlineTags: false,
|
||||
isSearchable: false,
|
||||
onChange: noop,
|
||||
onFilter: identity,
|
||||
onSearch: ( options ) => Promise.resolve( options ),
|
||||
maxResults: 0,
|
||||
multiple: false,
|
||||
searchDebounceTime: 0,
|
||||
searchInputType: 'search',
|
||||
selected: [],
|
||||
showAllOnFocus: false,
|
||||
showClearButton: false,
|
||||
hideBeforeSearch: false,
|
||||
staticList: false,
|
||||
autoComplete: 'off',
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withSpokenMessages,
|
||||
withInstanceId,
|
|
@ -2,18 +2,73 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { Button } from '@wordpress/components';
|
||||
import { RefObject } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { createElement, Component, createRef } from '@wordpress/element';
|
||||
import { isEqual } from 'lodash';
|
||||
import { isEqual, isNumber } from 'lodash';
|
||||
import { ENTER, ESCAPE, UP, DOWN, LEFT, RIGHT, TAB } from '@wordpress/keycodes';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Option } from './types';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* ID of the main SelectControl instance.
|
||||
*/
|
||||
listboxId?: string;
|
||||
/**
|
||||
* ID used for a11y in the listbox.
|
||||
*/
|
||||
instanceId: number;
|
||||
/**
|
||||
* Parent node to bind keyboard events to.
|
||||
*/
|
||||
node: HTMLElement | null;
|
||||
/**
|
||||
* Function to execute when an option is selected.
|
||||
*/
|
||||
onSelect: ( option: Option ) => void;
|
||||
/**
|
||||
* Array of options to display.
|
||||
*/
|
||||
options: Array< Option >;
|
||||
/**
|
||||
* Integer for the currently selected item.
|
||||
*/
|
||||
selectedIndex: number | null | undefined;
|
||||
/**
|
||||
* Bool to determine if the list should be positioned absolutely or staticly.
|
||||
*/
|
||||
staticList: boolean;
|
||||
/**
|
||||
* Function to execute when keyboard navigation should decrement the selected index.
|
||||
*/
|
||||
decrementSelectedIndex: () => void;
|
||||
/**
|
||||
* Function to execute when keyboard navigation should increment the selected index.
|
||||
*/
|
||||
incrementSelectedIndex: () => void;
|
||||
/**
|
||||
* Function to execute when the search value changes.
|
||||
*/
|
||||
onSearch: ( option: string | null ) => void;
|
||||
/**
|
||||
* Function to execute when the list should be expanded or collapsed.
|
||||
*/
|
||||
setExpanded: ( expanded: boolean ) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A list box that displays filtered options after search.
|
||||
*/
|
||||
class List extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
class List extends Component< Props > {
|
||||
optionRefs: { [ key: number ]: RefObject< HTMLButtonElement > };
|
||||
listbox: RefObject< HTMLDivElement >;
|
||||
|
||||
constructor( props: Props ) {
|
||||
super( props );
|
||||
|
||||
this.handleKeyDown = this.handleKeyDown.bind( this );
|
||||
this.select = this.select.bind( this );
|
||||
|
@ -21,7 +76,7 @@ class List extends Component {
|
|||
this.listbox = createRef();
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
componentDidUpdate( prevProps: Props ) {
|
||||
const { options, selectedIndex } = this.props;
|
||||
|
||||
// Remove old option refs to avoid memory leaks.
|
||||
|
@ -29,12 +84,15 @@ class List extends Component {
|
|||
this.optionRefs = {};
|
||||
}
|
||||
|
||||
if ( selectedIndex !== prevProps.selectedIndex ) {
|
||||
if (
|
||||
selectedIndex !== prevProps.selectedIndex &&
|
||||
isNumber( selectedIndex )
|
||||
) {
|
||||
this.scrollToOption( selectedIndex );
|
||||
}
|
||||
}
|
||||
|
||||
getOptionRef( index ) {
|
||||
getOptionRef( index: number ) {
|
||||
if ( ! this.optionRefs.hasOwnProperty( index ) ) {
|
||||
this.optionRefs[ index ] = createRef();
|
||||
}
|
||||
|
@ -42,7 +100,7 @@ class List extends Component {
|
|||
return this.optionRefs[ index ];
|
||||
}
|
||||
|
||||
select( option ) {
|
||||
select( option: Option ) {
|
||||
const { onSelect } = this.props;
|
||||
|
||||
if ( option.isDisabled ) {
|
||||
|
@ -52,9 +110,13 @@ class List extends Component {
|
|||
onSelect( option );
|
||||
}
|
||||
|
||||
scrollToOption( index ) {
|
||||
scrollToOption( index: number ) {
|
||||
const listbox = this.listbox.current;
|
||||
|
||||
if ( ! listbox ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( listbox.scrollHeight <= listbox.clientHeight ) {
|
||||
return;
|
||||
}
|
||||
|
@ -64,6 +126,12 @@ class List extends Component {
|
|||
}
|
||||
|
||||
const option = this.optionRefs[ index ].current;
|
||||
if ( ! option ) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn( 'Option not found, index:', index );
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollBottom = listbox.clientHeight + listbox.scrollTop;
|
||||
const elementBottom = option.offsetTop + option.offsetHeight;
|
||||
if ( elementBottom > scrollBottom ) {
|
||||
|
@ -73,7 +141,7 @@ class List extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleKeyDown( event ) {
|
||||
handleKeyDown( event: KeyboardEvent ) {
|
||||
const {
|
||||
decrementSelectedIndex,
|
||||
incrementSelectedIndex,
|
||||
|
@ -100,7 +168,7 @@ class List extends Component {
|
|||
break;
|
||||
|
||||
case ENTER:
|
||||
if ( options[ selectedIndex ] ) {
|
||||
if ( isNumber( selectedIndex ) && options[ selectedIndex ] ) {
|
||||
this.select( options[ selectedIndex ] );
|
||||
}
|
||||
event.preventDefault();
|
||||
|
@ -118,7 +186,7 @@ class List extends Component {
|
|||
return;
|
||||
|
||||
case TAB:
|
||||
if ( options[ selectedIndex ] ) {
|
||||
if ( isNumber( selectedIndex ) && options[ selectedIndex ] ) {
|
||||
this.select( options[ selectedIndex ] );
|
||||
}
|
||||
setExpanded( false );
|
||||
|
@ -128,8 +196,14 @@ class List extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
toggleKeyEvents( isListening ) {
|
||||
toggleKeyEvents( isListening: boolean ) {
|
||||
const { node } = this.props;
|
||||
if ( ! node ) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn( 'No node to bind events to.' );
|
||||
return;
|
||||
}
|
||||
|
||||
// This exists because we must capture ENTER key presses before RichText.
|
||||
// It seems that react fires the simulated capturing events after the
|
||||
// native browser event has already bubbled so we can't stopPropagation
|
||||
|
@ -138,12 +212,16 @@ class List extends Component {
|
|||
const handler = isListening
|
||||
? 'addEventListener'
|
||||
: 'removeEventListener';
|
||||
node[ handler ]( 'keydown', this.handleKeyDown, true );
|
||||
node[ handler ](
|
||||
'keydown',
|
||||
this.handleKeyDown as ( e: Event ) => void,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { selectedIndex } = this.props;
|
||||
if ( selectedIndex > -1 ) {
|
||||
if ( isNumber( selectedIndex ) && selectedIndex > -1 ) {
|
||||
this.scrollToOption( selectedIndex );
|
||||
}
|
||||
this.toggleKeyEvents( true );
|
||||
|
@ -169,7 +247,7 @@ class List extends Component {
|
|||
id={ listboxId }
|
||||
role="listbox"
|
||||
className={ listboxClasses }
|
||||
tabIndex="-1"
|
||||
tabIndex={ -1 }
|
||||
>
|
||||
{ options.map( ( option, index ) => (
|
||||
<Button
|
||||
|
@ -186,7 +264,7 @@ class List extends Component {
|
|||
}
|
||||
) }
|
||||
onClick={ () => this.select( option ) }
|
||||
tabIndex="-1"
|
||||
tabIndex={ -1 }
|
||||
>
|
||||
{ option.label }
|
||||
</Button>
|
||||
|
@ -196,50 +274,4 @@ class List extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
List.propTypes = {
|
||||
/**
|
||||
* ID of the main SelectControl instance.
|
||||
*/
|
||||
instanceId: PropTypes.number,
|
||||
/**
|
||||
* ID used for a11y in the listbox.
|
||||
*/
|
||||
listboxId: PropTypes.string,
|
||||
/**
|
||||
* Parent node to bind keyboard events to.
|
||||
*/
|
||||
// eslint-disable-next-line no-undef
|
||||
node: PropTypes.instanceOf( Element ).isRequired,
|
||||
/**
|
||||
* Function to execute when an option is selected.
|
||||
*/
|
||||
onSelect: PropTypes.func,
|
||||
/**
|
||||
* Array of options to display.
|
||||
*/
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
isDisabled: PropTypes.bool,
|
||||
key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] )
|
||||
.isRequired,
|
||||
keywords: PropTypes.arrayOf(
|
||||
PropTypes.oneOfType( [ PropTypes.string, PropTypes.number ] )
|
||||
),
|
||||
label: PropTypes.oneOfType( [
|
||||
PropTypes.string,
|
||||
PropTypes.object,
|
||||
] ),
|
||||
value: PropTypes.any,
|
||||
} )
|
||||
).isRequired,
|
||||
/**
|
||||
* Integer for the currently selected item.
|
||||
*/
|
||||
selectedIndex: PropTypes.number,
|
||||
/**
|
||||
* Bool to determine if the list should be positioned absolutely or staticly.
|
||||
*/
|
||||
staticList: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default List;
|
|
@ -1,8 +1,12 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { SelectControl } from '@woocommerce/components';
|
||||
import { useState } from '@wordpress/element';
|
||||
import React from 'react';
|
||||
import { createElement, useState } from '@wordpress/element';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import SelectControl from '../';
|
||||
|
||||
const options = [
|
||||
{
|
|
@ -5,19 +5,36 @@ import { __, sprintf } from '@wordpress/i18n';
|
|||
import { Button } from '@wordpress/components';
|
||||
import { Icon, cancelCircleFilled } from '@wordpress/icons';
|
||||
import { createElement, Component, Fragment } from '@wordpress/element';
|
||||
import { findIndex } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { findIndex, isArray } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Tag from '../tag';
|
||||
import { Option, Selected } from './types';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Function called when selected results change, passed result list.
|
||||
*/
|
||||
onChange: ( selected: Option[] ) => void;
|
||||
/**
|
||||
* An array of objects describing selected values. If the label of the selected
|
||||
* value is omitted, the Tag of that value will not be rendered inside the
|
||||
* search box.
|
||||
*/
|
||||
selected?: Selected;
|
||||
/**
|
||||
* Render a 'Clear' button next to the input box to remove its contents.
|
||||
*/
|
||||
showClearButton?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A list of tags to display selected items.
|
||||
*/
|
||||
class Tags extends Component {
|
||||
constructor( props ) {
|
||||
class Tags extends Component< Props > {
|
||||
constructor( props: Props ) {
|
||||
super( props );
|
||||
this.removeAll = this.removeAll.bind( this );
|
||||
this.removeResult = this.removeResult.bind( this );
|
||||
|
@ -28,9 +45,13 @@ class Tags extends Component {
|
|||
onChange( [] );
|
||||
}
|
||||
|
||||
removeResult( key ) {
|
||||
removeResult( key: string | undefined ) {
|
||||
return () => {
|
||||
const { selected, onChange } = this.props;
|
||||
if ( ! isArray( selected ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const i = findIndex( selected, { key } );
|
||||
onChange( [
|
||||
...selected.slice( 0, i ),
|
||||
|
@ -41,7 +62,7 @@ class Tags extends Component {
|
|||
|
||||
render() {
|
||||
const { selected, showClearButton } = this.props;
|
||||
if ( ! selected.length ) {
|
||||
if ( ! isArray( selected ) || ! selected.length ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -63,6 +84,7 @@ class Tags extends Component {
|
|||
key={ item.key }
|
||||
id={ item.key }
|
||||
label={ item.label }
|
||||
// @ts-expect-error key is a string or undefined here
|
||||
remove={ this.removeResult }
|
||||
screenReaderLabel={ screenReaderLabel }
|
||||
/>
|
||||
|
@ -89,31 +111,4 @@ class Tags extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
Tags.propTypes = {
|
||||
/**
|
||||
* Function called when selected results change, passed result list.
|
||||
*/
|
||||
onChange: PropTypes.func,
|
||||
/**
|
||||
* Function to execute when an option is selected.
|
||||
*/
|
||||
onSelect: PropTypes.func,
|
||||
/**
|
||||
* An array of objects describing selected values. If the label of the selected
|
||||
* value is omitted, the Tag of that value will not be rendered inside the
|
||||
* search box.
|
||||
*/
|
||||
selected: PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] )
|
||||
.isRequired,
|
||||
label: PropTypes.string,
|
||||
} )
|
||||
),
|
||||
/**
|
||||
* Render a 'Clear' button next to the input box to remove its contents.
|
||||
*/
|
||||
showClearButton: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Tags;
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { createElement } from '@wordpress/element';
|
||||
|
@ -9,10 +10,11 @@ import { createElement } from '@wordpress/element';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { SelectControl } from '../index';
|
||||
import { Option } from '../types';
|
||||
|
||||
describe( 'SelectControl', () => {
|
||||
const query = 'lorem';
|
||||
const options = [
|
||||
const options: Option[] = [
|
||||
{ key: '1', label: 'lorem 1', value: { id: '1' } },
|
||||
{ key: '2', label: 'lorem 2', value: { id: '2' } },
|
||||
{ key: '3', label: 'bar', value: { id: '3' } },
|
||||
|
@ -168,9 +170,9 @@ describe( 'SelectControl', () => {
|
|||
} );
|
||||
|
||||
it( 'changes the options on search', async () => {
|
||||
const queriedOptions = [];
|
||||
const queriedOptions: Option[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const queryOptions = ( options, searchedQuery ) => {
|
||||
const queryOptions = async ( options: Option[], searchedQuery: string | null ) => {
|
||||
if ( searchedQuery === 'test' ) {
|
||||
queriedOptions.push( {
|
||||
key: 'test-option',
|
||||
|
@ -209,7 +211,7 @@ describe( 'SelectControl', () => {
|
|||
isSearchable
|
||||
showAllOnFocus
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
onChange={ onChangeMock }
|
||||
/>
|
||||
|
@ -229,7 +231,7 @@ describe( 'SelectControl', () => {
|
|||
isSearchable
|
||||
selected={ [ { ...options[ 0 ] } ] }
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
onChange={ onChangeMock }
|
||||
/>
|
||||
|
@ -258,7 +260,7 @@ describe( 'SelectControl', () => {
|
|||
isSearchable
|
||||
selected={ options[ 0 ].key }
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
onChange={ onChangeMock }
|
||||
/>
|
||||
|
@ -289,7 +291,7 @@ describe( 'SelectControl', () => {
|
|||
showAllOnFocus
|
||||
selected={ options[ 2 ].key }
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
onChange={ onChangeMock }
|
||||
excludeSelectedOptions={ false }
|
||||
|
@ -316,7 +318,7 @@ describe( 'SelectControl', () => {
|
|||
showAllOnFocus
|
||||
selected={ options[ 2 ].key }
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
onChange={ onChangeMock }
|
||||
excludeSelectedOptions={ false }
|
||||
|
@ -364,7 +366,7 @@ describe( 'SelectControl', () => {
|
|||
isSearchable
|
||||
selected={ [ { ...options[ 0 ] } ] }
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
onChange={ onChangeMock }
|
||||
/>
|
||||
|
@ -383,7 +385,7 @@ describe( 'SelectControl', () => {
|
|||
isSearchable
|
||||
selected={ options[ 0 ].key }
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
onChange={ onChangeMock }
|
||||
/>
|
||||
|
@ -416,9 +418,8 @@ describe( 'SelectControl', () => {
|
|||
const { getByRole } = render(
|
||||
<SelectControl
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
selected={ null }
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -440,7 +441,7 @@ describe( 'SelectControl', () => {
|
|||
const { getByRole } = render(
|
||||
<SelectControl
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
selected={ options[ 1 ].key }
|
||||
excludeSelectedOptions={ false }
|
||||
|
@ -465,7 +466,7 @@ describe( 'SelectControl', () => {
|
|||
const { getByRole } = render(
|
||||
<SelectControl
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
selected={ options[ options.length - 1 ].key }
|
||||
excludeSelectedOptions={ false }
|
||||
|
@ -490,9 +491,8 @@ describe( 'SelectControl', () => {
|
|||
const { getByRole } = render(
|
||||
<SelectControl
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
selected={ null }
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -514,9 +514,8 @@ describe( 'SelectControl', () => {
|
|||
const { getByRole, queryByRole } = render(
|
||||
<SelectControl
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
selected={ null }
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -537,9 +536,8 @@ describe( 'SelectControl', () => {
|
|||
const { getByRole } = render(
|
||||
<SelectControl
|
||||
options={ options }
|
||||
onSearch={ () => options }
|
||||
onSearch={ async () => options }
|
||||
onFilter={ () => options }
|
||||
selected={ null }
|
||||
excludeSelectedOptions={ false }
|
||||
onChange={ onChangeMock }
|
||||
/>
|
|
@ -0,0 +1,9 @@
|
|||
export type Option = {
|
||||
key: string;
|
||||
label: string;
|
||||
isDisabled?: boolean;
|
||||
keywords?: Array< string >;
|
||||
value?: unknown;
|
||||
};
|
||||
|
||||
export type Selected = string | Option[];
|
|
@ -110,7 +110,7 @@ export const Sortable = ( {
|
|||
|
||||
// Items before the current item cause a one off error when
|
||||
// removed from the old array and spliced into the new array.
|
||||
// TODO: Issue with dragging into same position having to do with isBefore returning true intially.
|
||||
// TODO: Issue with dragging into same position having to do with isBefore returning true initially.
|
||||
let targetIndex = dragIndex < index ? index : index + 1;
|
||||
if ( isBefore( event, isHorizontal ) ) {
|
||||
targetIndex--;
|
||||
|
|
|
@ -61,7 +61,7 @@ Name | Type | Default | Description
|
|||
`ids` | Array | `null` | A list of IDs, matching to the row list so that ids[ 0 ] contains the object ID for the object displayed in row[ 0 ]
|
||||
`isLoading` | Boolean | `false` | Defines if the table contents are loading. It will display `TablePlaceholder` component instead of `Table` if that's the case
|
||||
`onQueryChange` | Function | `noop` | A function which returns a callback function to update the query string for a given `param`
|
||||
`onColumnsChange` | Function | `noop` | A function which returns a callback function which is called upon the user changing the visiblity of columns
|
||||
`onColumnsChange` | Function | `noop` | A function which returns a callback function which is called upon the user changing the visibility of columns
|
||||
`onSearch` | Function | `noop` | A function which is called upon the user searching in the table header
|
||||
`onSort` | Function | `undefined` | A function which is called upon the user changing the sorting of the table
|
||||
`downloadable` | Boolean | `false` | Whether the table must be downloadable. If true, the download button will appear
|
||||
|
|
|
@ -158,7 +158,7 @@ export type TableCardProps = CommonTableProps & {
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onQueryChange?: ( param: string ) => ( ...props: any ) => void;
|
||||
/**
|
||||
* A function which returns a callback function which is called upon the user changing the visiblity of columns.
|
||||
* A function which returns a callback function which is called upon the user changing the visibility of columns.
|
||||
*/
|
||||
onColumnsChange?: ( showCols: Array< string >, key?: string ) => void;
|
||||
/**
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
## Breaking changes
|
||||
|
||||
- Fix the batch fetch logic for the options data store. #7587
|
||||
- Add backwards compability for old function format. #7688
|
||||
- Add backwards compatibility for old function format. #7688
|
||||
- Add console warning for inbox note contents exceeding 320 characters and add dompurify dependency. #7869
|
||||
- Fix race condition in data package's options module. #7947
|
||||
- Remove dev dependency `@woocommerce/wc-admin-settings`. #8057
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
Correct spelling errors
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
Correct spelling errors
|
|
@ -163,7 +163,7 @@ describe( 'Experimental List', () => {
|
|||
} );
|
||||
|
||||
describe( 'ExperimentalListItemCollapse', () => {
|
||||
it( 'should not render its children intially, but an extra list footer with show text', () => {
|
||||
it( 'should not render its children initially, but an extra list footer with show text', () => {
|
||||
const { container } = render(
|
||||
<ExperimentalCollapsibleList
|
||||
collapseLabel="Show less"
|
||||
|
|
|
@ -185,7 +185,7 @@ describe( 'InboxNoteCard', () => {
|
|||
);
|
||||
} );
|
||||
|
||||
it( 'should call onVisible when visiblity sensor calls it', () => {
|
||||
it( 'should call onVisible when visibility sensor calls it', () => {
|
||||
const onVisible = jest.fn();
|
||||
const { getByText } = render(
|
||||
<InboxNoteCard
|
||||
|
@ -199,7 +199,7 @@ describe( 'InboxNoteCard', () => {
|
|||
expect( onVisible ).toHaveBeenCalledWith( note );
|
||||
} );
|
||||
|
||||
it( 'should call onVisible when visiblity sensor calls it, but only once', () => {
|
||||
it( 'should call onVisible when visibility sensor calls it, but only once', () => {
|
||||
const onVisible = jest.fn();
|
||||
const { getByText } = render(
|
||||
<InboxNoteCard
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add Sale price validation#37985
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Fix summary toolbar positioning and selection on blur
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Prevent double debouncing of iframe editor callback
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Fixing spacing when no images added on image gallery block.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Removing WritingFlow component which was suppressing tabbing behavior in form.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Fix broken assertion with comma separated list in category select control
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Update shipping dimensions image in new product blocks editor
|
|
@ -1,7 +1,4 @@
|
|||
.wp-block-woocommerce-product-images-field {
|
||||
.woocommerce-image-gallery {
|
||||
margin-top: $gap-largest;
|
||||
}
|
||||
.woocommerce-media-uploader {
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -13,6 +10,12 @@
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
&.has-images {
|
||||
.woocommerce-image-gallery {
|
||||
margin-top: $gap-largest;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.has-images) {
|
||||
.woocommerce-sortable {
|
||||
display: none;
|
||||
|
|
|
@ -9,6 +9,8 @@ export { init as initSku } from './inventory-sku';
|
|||
export { init as initName } from './name';
|
||||
export { init as initPricing } from './pricing';
|
||||
export { init as initRadio } from './radio';
|
||||
export { init as initRegularPrice } from './regular-price';
|
||||
export { init as initSalePrice } from './sale-price';
|
||||
export { init as initScheduleSale } from './schedule-sale';
|
||||
export { init as initSection } from './section';
|
||||
export { init as initShippingDimensions } from './shipping-dimensions';
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2,
|
||||
"name": "woocommerce/product-regular-price-field",
|
||||
"description": "A product price block with currency display.",
|
||||
"title": "Product regular price",
|
||||
"category": "widgets",
|
||||
"keywords": [ "products", "price" ],
|
||||
"textdomain": "default",
|
||||
"attributes": {
|
||||
"label": {
|
||||
"type": "string",
|
||||
"__experimentalRole": "content"
|
||||
},
|
||||
"help": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false,
|
||||
"inserter": false,
|
||||
"lock": false,
|
||||
"__experimentalToolbar": false
|
||||
},
|
||||
"editorStyle": "file:./editor.css"
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classNames from 'classnames';
|
||||
import { Link } from '@woocommerce/components';
|
||||
import { CurrencyContext } from '@woocommerce/currency';
|
||||
import { getNewPath } from '@woocommerce/navigation';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { BlockEditProps } from '@wordpress/blocks';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import {
|
||||
createElement,
|
||||
useContext,
|
||||
createInterpolateElement,
|
||||
} from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
BaseControl,
|
||||
// @ts-expect-error `__experimentalInputControl` does exist.
|
||||
__experimentalInputControl as InputControl,
|
||||
} from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useCurrencyInputProps } from '../../hooks/use-currency-input-props';
|
||||
import { formatCurrencyDisplayValue } from '../../utils';
|
||||
import { SalePriceBlockAttributes } from './types';
|
||||
import { useValidation } from '../../hooks/use-validation';
|
||||
|
||||
export function Edit( {
|
||||
attributes,
|
||||
}: BlockEditProps< SalePriceBlockAttributes > ) {
|
||||
const blockProps = useBlockProps();
|
||||
const { label, help } = attributes;
|
||||
const [ regularPrice, setRegularPrice ] = useEntityProp< string >(
|
||||
'postType',
|
||||
'product',
|
||||
'regular_price'
|
||||
);
|
||||
const [ salePrice ] = useEntityProp< string >(
|
||||
'postType',
|
||||
'product',
|
||||
'sale_price'
|
||||
);
|
||||
const context = useContext( CurrencyContext );
|
||||
const { getCurrencyConfig, formatAmount } = context;
|
||||
const currencyConfig = getCurrencyConfig();
|
||||
const inputProps = useCurrencyInputProps( {
|
||||
value: regularPrice,
|
||||
setValue: setRegularPrice,
|
||||
} );
|
||||
|
||||
const interpolatedHelp = help
|
||||
? createInterpolateElement( help, {
|
||||
PricingTab: (
|
||||
<Link
|
||||
href={ getNewPath( { tab: 'pricing' } ) }
|
||||
onClick={ () => {
|
||||
recordEvent( 'product_pricing_help_click' );
|
||||
} }
|
||||
/>
|
||||
),
|
||||
} )
|
||||
: null;
|
||||
|
||||
const regularPriceId = useInstanceId(
|
||||
BaseControl,
|
||||
'wp-block-woocommerce-product-regular-price-field'
|
||||
) as string;
|
||||
|
||||
const regularPriceValidationError = useValidation(
|
||||
'product/regular_price',
|
||||
function regularPriceValidator() {
|
||||
const listPrice = Number.parseFloat( regularPrice );
|
||||
if ( listPrice ) {
|
||||
if ( listPrice < 0 ) {
|
||||
return __(
|
||||
'List price must be greater than or equals to zero.',
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
if (
|
||||
salePrice &&
|
||||
listPrice <= Number.parseFloat( salePrice )
|
||||
) {
|
||||
return __(
|
||||
'List price must be greater than the sale price.',
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<BaseControl
|
||||
id={ regularPriceId }
|
||||
help={
|
||||
regularPriceValidationError
|
||||
? regularPriceValidationError
|
||||
: interpolatedHelp
|
||||
}
|
||||
className={ classNames( {
|
||||
'has-error': regularPriceValidationError,
|
||||
} ) }
|
||||
>
|
||||
<InputControl
|
||||
{ ...inputProps }
|
||||
id={ regularPriceId }
|
||||
name={ 'regular_price' }
|
||||
label={ label }
|
||||
value={ formatCurrencyDisplayValue(
|
||||
String( regularPrice ),
|
||||
currencyConfig,
|
||||
formatAmount
|
||||
) }
|
||||
onChange={ setRegularPrice }
|
||||
/>
|
||||
</BaseControl>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
.wp-block-woocommerce-product-regular-price-field {
|
||||
.components-currency-control {
|
||||
.components-input-control__prefix {
|
||||
color: $gray-700;
|
||||
}
|
||||
|
||||
.components-input-control__input {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { BlockConfiguration } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { initBlock } from '../../utils/init-blocks';
|
||||
import blockConfiguration from './block.json';
|
||||
import { Edit } from './edit';
|
||||
import { SalePriceBlockAttributes } from './types';
|
||||
|
||||
const { name, ...metadata } =
|
||||
blockConfiguration as BlockConfiguration< SalePriceBlockAttributes >;
|
||||
|
||||
export { metadata, name };
|
||||
|
||||
export const settings: Partial<
|
||||
BlockConfiguration< SalePriceBlockAttributes >
|
||||
> = {
|
||||
example: {},
|
||||
edit: Edit,
|
||||
};
|
||||
|
||||
export function init() {
|
||||
return initBlock( { name, metadata, settings } );
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { BlockAttributes } from '@wordpress/blocks';
|
||||
|
||||
export interface SalePriceBlockAttributes extends BlockAttributes {
|
||||
label: string;
|
||||
help?: string;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2,
|
||||
"name": "woocommerce/product-sale-price-field",
|
||||
"description": "A product price block with currency display.",
|
||||
"title": "Product sale price",
|
||||
"category": "widgets",
|
||||
"keywords": [ "products", "price" ],
|
||||
"textdomain": "default",
|
||||
"attributes": {
|
||||
"label": {
|
||||
"type": "string",
|
||||
"__experimentalRole": "content"
|
||||
},
|
||||
"help": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false,
|
||||
"inserter": false,
|
||||
"lock": false,
|
||||
"__experimentalToolbar": false
|
||||
},
|
||||
"editorStyle": "file:./editor.css"
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classNames from 'classnames';
|
||||
import { CurrencyContext } from '@woocommerce/currency';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { BlockEditProps } from '@wordpress/blocks';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { createElement, useContext } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
BaseControl,
|
||||
// @ts-expect-error `__experimentalInputControl` does exist.
|
||||
__experimentalInputControl as InputControl,
|
||||
} from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useCurrencyInputProps } from '../../hooks/use-currency-input-props';
|
||||
import { formatCurrencyDisplayValue } from '../../utils';
|
||||
import { SalePriceBlockAttributes } from './types';
|
||||
import { useValidation } from '../../hooks/use-validation';
|
||||
|
||||
export function Edit( {
|
||||
attributes,
|
||||
}: BlockEditProps< SalePriceBlockAttributes > ) {
|
||||
const blockProps = useBlockProps();
|
||||
const { label, help } = attributes;
|
||||
const [ regularPrice ] = useEntityProp< string >(
|
||||
'postType',
|
||||
'product',
|
||||
'regular_price'
|
||||
);
|
||||
const [ salePrice, setSalePrice ] = useEntityProp< string >(
|
||||
'postType',
|
||||
'product',
|
||||
'sale_price'
|
||||
);
|
||||
const context = useContext( CurrencyContext );
|
||||
const { getCurrencyConfig, formatAmount } = context;
|
||||
const currencyConfig = getCurrencyConfig();
|
||||
const inputProps = useCurrencyInputProps( {
|
||||
value: salePrice,
|
||||
setValue: setSalePrice,
|
||||
} );
|
||||
|
||||
const salePriceId = useInstanceId(
|
||||
BaseControl,
|
||||
'wp-block-woocommerce-product-sale-price-field'
|
||||
) as string;
|
||||
|
||||
const salePriceValidationError = useValidation(
|
||||
'product/sale_price',
|
||||
function salePriceValidator() {
|
||||
if ( salePrice ) {
|
||||
if ( Number.parseFloat( salePrice ) < 0 ) {
|
||||
return __(
|
||||
'Sale price must be greater than or equals to zero.',
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
const listPrice = Number.parseFloat( regularPrice );
|
||||
if (
|
||||
! listPrice ||
|
||||
listPrice <= Number.parseFloat( salePrice )
|
||||
) {
|
||||
return __(
|
||||
'Sale price must be lower than the list price.',
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<BaseControl
|
||||
id={ salePriceId }
|
||||
help={
|
||||
salePriceValidationError ? salePriceValidationError : help
|
||||
}
|
||||
className={ classNames( {
|
||||
'has-error': salePriceValidationError,
|
||||
} ) }
|
||||
>
|
||||
<InputControl
|
||||
{ ...inputProps }
|
||||
id={ salePriceId }
|
||||
name={ 'sale_price' }
|
||||
onChange={ setSalePrice }
|
||||
label={ label }
|
||||
value={ formatCurrencyDisplayValue(
|
||||
String( salePrice ),
|
||||
currencyConfig,
|
||||
formatAmount
|
||||
) }
|
||||
/>
|
||||
</BaseControl>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
.wp-block-woocommerce-product-sale-price-field {
|
||||
.components-currency-control {
|
||||
.components-input-control__prefix {
|
||||
color: $gray-700;
|
||||
}
|
||||
|
||||
.components-input-control__input {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { BlockConfiguration } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { initBlock } from '../../utils/init-blocks';
|
||||
import blockConfiguration from './block.json';
|
||||
import { Edit } from './edit';
|
||||
import { SalePriceBlockAttributes } from './types';
|
||||
|
||||
const { name, ...metadata } =
|
||||
blockConfiguration as BlockConfiguration< SalePriceBlockAttributes >;
|
||||
|
||||
export { metadata, name };
|
||||
|
||||
export const settings: Partial<
|
||||
BlockConfiguration< SalePriceBlockAttributes >
|
||||
> = {
|
||||
example: {},
|
||||
edit: Edit,
|
||||
};
|
||||
|
||||
export function init() {
|
||||
return initBlock( { name, metadata, settings } );
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { BlockAttributes } from '@wordpress/blocks';
|
||||
|
||||
export interface SalePriceBlockAttributes extends BlockAttributes {
|
||||
label: string;
|
||||
help?: string;
|
||||
}
|
|
@ -205,6 +205,17 @@ export function Edit( {}: BlockEditProps< ShippingDimensionsBlockAttributes > )
|
|||
<ShippingDimensionsImage
|
||||
highlight={ highlightSide }
|
||||
className="wp-block-woocommerce-product-shipping-dimensions-fields__dimensions-image"
|
||||
labels={ {
|
||||
A: dimensionsWidthProps.value?.length
|
||||
? dimensionsWidthProps.value
|
||||
: undefined,
|
||||
B: dimensionsLengthProps.value?.length
|
||||
? dimensionsLengthProps.value
|
||||
: undefined,
|
||||
C: dimensionsHeightProps.value?.length
|
||||
? dimensionsHeightProps.value
|
||||
: undefined,
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -18,5 +18,6 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
padding: $gap;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
@import 'inventory-sku/editor.scss';
|
||||
@import 'name/editor.scss';
|
||||
@import 'pricing/editor.scss';
|
||||
@import 'regular-price/editor.scss';
|
||||
@import 'sale-price/editor.scss';
|
||||
@import 'schedule-sale/editor.scss';
|
||||
@import 'section/editor.scss';
|
||||
@import 'shipping-dimensions/editor.scss';
|
||||
|
|
|
@ -5,8 +5,9 @@ import { __ } from '@wordpress/i18n';
|
|||
import { createElement } from '@wordpress/element';
|
||||
import { BlockEditProps } from '@wordpress/blocks';
|
||||
import { BaseControl } from '@wordpress/components';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import uniqueId from 'lodash/uniqueId';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
@ -14,6 +15,7 @@ import {
|
|||
AlignmentControl,
|
||||
BlockControls,
|
||||
RichText,
|
||||
store as blockEditorStore,
|
||||
useBlockProps,
|
||||
} from '@wordpress/block-editor';
|
||||
|
||||
|
@ -32,12 +34,18 @@ export function Edit( {
|
|||
const blockProps = useBlockProps( {
|
||||
style: { direction },
|
||||
} );
|
||||
const id = uniqueId();
|
||||
const contentId = useInstanceId(
|
||||
Edit,
|
||||
'wp-block-woocommerce-product-summary-field__content'
|
||||
);
|
||||
const [ summary, setSummary ] = useEntityProp< string >(
|
||||
'postType',
|
||||
'product',
|
||||
'short_description'
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore No types for this exist yet.
|
||||
const { clearSelectedBlock } = useDispatch( blockEditorStore );
|
||||
|
||||
function handleAlignmentChange( value: SummaryAttributes[ 'align' ] ) {
|
||||
setAttributes( { align: value } );
|
||||
|
@ -47,8 +55,21 @@ export function Edit( {
|
|||
setAttributes( { direction: value } );
|
||||
}
|
||||
|
||||
function handleBlur( event: React.FocusEvent< 'p', Element > ) {
|
||||
const isToolbar = event.relatedTarget?.closest(
|
||||
'.block-editor-block-contextual-toolbar'
|
||||
);
|
||||
if ( ! isToolbar ) {
|
||||
clearSelectedBlock();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<div
|
||||
className={
|
||||
'wp-block wp-block-woocommerce-product-summary-field-wrapper'
|
||||
}
|
||||
>
|
||||
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
|
||||
{ /* @ts-ignore No types for this exist yet. */ }
|
||||
<BlockControls group="block">
|
||||
|
@ -65,26 +86,29 @@ export function Edit( {
|
|||
</BlockControls>
|
||||
|
||||
<BaseControl
|
||||
id={ id }
|
||||
id={ contentId.toString() }
|
||||
label={ label || __( 'Summary', 'woocommerce' ) }
|
||||
>
|
||||
<RichText
|
||||
id={ id }
|
||||
identifier="content"
|
||||
tagName="p"
|
||||
value={ summary }
|
||||
onChange={ setSummary }
|
||||
placeholder={ __(
|
||||
"Summarize this product in 1-2 short sentences. We'll show it at the top of the page.",
|
||||
'woocommerce'
|
||||
) }
|
||||
data-empty={ Boolean( summary ) }
|
||||
className={ classNames( 'components-summary-control', {
|
||||
[ `has-text-align-${ align }` ]: align,
|
||||
} ) }
|
||||
dir={ direction }
|
||||
allowedFormats={ allowedFormats }
|
||||
/>
|
||||
<div { ...blockProps }>
|
||||
<RichText
|
||||
id={ contentId.toString() }
|
||||
identifier="content"
|
||||
tagName="p"
|
||||
value={ summary }
|
||||
onChange={ setSummary }
|
||||
placeholder={ __(
|
||||
"Summarize this product in 1-2 short sentences. We'll show it at the top of the page.",
|
||||
'woocommerce'
|
||||
) }
|
||||
data-empty={ Boolean( summary ) }
|
||||
className={ classNames( 'components-summary-control', {
|
||||
[ `has-text-align-${ align }` ]: align,
|
||||
} ) }
|
||||
dir={ direction }
|
||||
allowedFormats={ allowedFormats }
|
||||
onBlur={ handleBlur }
|
||||
/>
|
||||
</div>
|
||||
</BaseControl>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
BlockTools,
|
||||
EditorSettings,
|
||||
EditorBlockListSettings,
|
||||
WritingFlow,
|
||||
ObserveTyping,
|
||||
} from '@wordpress/block-editor';
|
||||
// It doesn't seem to notice the External dependency block whn @ts-ignore is added.
|
||||
|
@ -106,11 +105,9 @@ export function BlockEditor( {
|
|||
{ /* @ts-ignore No types for this exist yet. */ }
|
||||
<BlockEditorKeyboardShortcuts.Register />
|
||||
<BlockTools>
|
||||
<WritingFlow>
|
||||
<ObserveTyping>
|
||||
<BlockList className="woocommerce-product-block-editor__block-list" />
|
||||
</ObserveTyping>
|
||||
</WritingFlow>
|
||||
<ObserveTyping>
|
||||
<BlockList className="woocommerce-product-block-editor__block-list" />
|
||||
</ObserveTyping>
|
||||
</BlockTools>
|
||||
</div>
|
||||
</BlockEditorProvider>
|
||||
|
|
|
@ -74,7 +74,6 @@ describe( 'CategoryField', () => {
|
|||
</Form>
|
||||
);
|
||||
queryByPlaceholderText( 'Search or create category…' )?.focus();
|
||||
expect( queryAllByText( 'Test' ) ).toHaveLength( 2 );
|
||||
expect( queryAllByText( 'Clothing' ) ).toHaveLength( 2 );
|
||||
expect( queryAllByText( 'Test, Clothing' ) ).toHaveLength( 1 );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -4,13 +4,8 @@
|
|||
import { BlockInstance } from '@wordpress/blocks';
|
||||
import { Popover } from '@wordpress/components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import {
|
||||
createElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from '@wordpress/element';
|
||||
import { useDebounce, useResizeObserver } from '@wordpress/compose';
|
||||
import { createElement, useEffect, useState } from '@wordpress/element';
|
||||
import { useResizeObserver } from '@wordpress/compose';
|
||||
import {
|
||||
BlockEditorProvider,
|
||||
BlockInspector,
|
||||
|
@ -66,15 +61,6 @@ export function IframeEditor( {
|
|||
updateSettings( productBlockEditorSettings );
|
||||
}, [] );
|
||||
|
||||
const handleChange = useCallback(
|
||||
( updatedBlocks: BlockInstance[] ) => {
|
||||
onChange( updatedBlocks );
|
||||
},
|
||||
[ onChange ]
|
||||
);
|
||||
|
||||
const debouncedOnChange = useDebounce( handleChange, 200 );
|
||||
|
||||
return (
|
||||
<div className="woocommerce-iframe-editor">
|
||||
<BlockEditorProvider
|
||||
|
@ -86,7 +72,7 @@ export function IframeEditor( {
|
|||
value={ blocks }
|
||||
onChange={ ( updatedBlocks: BlockInstance[] ) => {
|
||||
setBlocks( updatedBlocks );
|
||||
debouncedOnChange( updatedBlocks );
|
||||
onChange( updatedBlocks );
|
||||
} }
|
||||
useSubRegistry={ true }
|
||||
>
|
||||
|
@ -104,7 +90,13 @@ export function IframeEditor( {
|
|||
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
|
||||
{ /* @ts-ignore */ }
|
||||
<BlockEditorKeyboardShortcuts.Register />
|
||||
{ onClose && <BackButton onClick={ onClose } /> }
|
||||
{ onClose && (
|
||||
<BackButton
|
||||
onClick={ () => {
|
||||
setTimeout( onClose, 550 );
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
<ResizableEditor
|
||||
enableResizing={ true }
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
|
|
@ -10,90 +10,111 @@ import { ShippingDimensionsImageProps } from './types';
|
|||
|
||||
export function ShippingDimensionsImage( {
|
||||
highlight,
|
||||
labels = {},
|
||||
...props
|
||||
}: ShippingDimensionsImageProps ) {
|
||||
return (
|
||||
<svg
|
||||
{ ...props }
|
||||
viewBox="0 0 288 195"
|
||||
width="295"
|
||||
height="195"
|
||||
viewBox="0 0 295 195"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{ ...props }
|
||||
>
|
||||
{ /* Side A */ }
|
||||
<path
|
||||
d="M10.4922 134.221V35.2617C10.4922 33.8539 11.9079 32.8867 13.2193 33.3986L98.3109 66.6076C99.0711 66.9043 99.5748 67.633 99.5837 68.449L100.703 171.089C100.719 172.534 99.2449 173.518 97.9167 172.95L11.7054 136.06C10.9695 135.745 10.4922 135.022 10.4922 134.221Z"
|
||||
fill={ highlight === 'A' ? '#F0F6FC' : '#F6F7F7' }
|
||||
d="M11.5664 134.604V35.3599C11.5664 33.9482 12.9862 32.9782 14.3014 33.4915L99.6373 66.7959C100.4 67.0935 100.905 67.8243 100.914 68.6426L102.037 171.578C102.052 173.027 100.574 174.014 99.2419 173.444L12.7831 136.448C12.0451 136.132 11.5664 135.407 11.5664 134.604Z"
|
||||
fill={ highlight === 'A' ? '#F0F6FC' : '#FFFFFF' }
|
||||
/>
|
||||
<path
|
||||
d="M43.9062 84.2338V44.7946L187.953 11.877L211.485 20.5392L67.0049 53.3546V93.6078L43.9062 84.2338Z"
|
||||
fill="#F0F0F0"
|
||||
stroke="#DDDDDD"
|
||||
strokeWidth="2"
|
||||
d="M11.5664 134.603V35.3599C11.5664 33.9482 12.9862 32.9782 14.3014 33.4915L99.624 66.7908C100.393 67.0909 100.9 67.8314 100.901 68.6569L101.024 174.131L12.7844 136.447C12.0457 136.132 11.5664 135.406 11.5664 134.603Z"
|
||||
stroke="#E0E0E0"
|
||||
strokeWidth="2.00574"
|
||||
/>
|
||||
{ /* Line A */ }
|
||||
<path
|
||||
d="M43.9062 99.8824V90.6973L67.0049 100.301V109.256L43.9062 99.8824Z"
|
||||
fill="#F0F0F0"
|
||||
stroke="#DDDDDD"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M10.4922 134.22V35.2617C10.4922 33.8539 11.9079 32.8867 13.2193 33.3986L98.2977 66.6025C99.0645 66.9017 99.5696 67.6402 99.5705 68.4633L99.6936 173.635L11.7067 136.06C10.9701 135.745 10.4922 135.021 10.4922 134.22Z"
|
||||
stroke="#DDDDDD"
|
||||
strokeWidth="2"
|
||||
d="M1.25977 150.388L86.0112 188.183"
|
||||
stroke={ highlight === 'A' ? '#006FAD' : '#CCCCCC' }
|
||||
strokeWidth="1.50431"
|
||||
strokeMiterlimit="16"
|
||||
/>
|
||||
{ /* Side B */ }
|
||||
<path
|
||||
d="M249.015 32.8879L99.5703 66.7689V172.489C99.5703 173.801 100.812 174.758 102.081 174.423L249.968 135.378C250.846 135.146 251.458 134.352 251.458 133.444V34.8384C251.458 33.5554 250.267 32.6042 249.015 32.8879Z"
|
||||
fill={ highlight === 'B' ? '#F0F6FC' : '#F6F7F7' }
|
||||
stroke="#DDDDDD"
|
||||
strokeWidth="2"
|
||||
d="M250.775 32.9793L100.9 66.9577V172.981C100.9 174.297 102.146 175.257 103.418 174.921L251.73 135.764C252.611 135.531 253.224 134.735 253.224 133.824V34.9354C253.224 33.6488 252.03 32.6948 250.775 32.9793Z"
|
||||
fill={ highlight === 'B' ? '#F0F6FC' : '#FFFFFF' }
|
||||
stroke="#E0E0E0"
|
||||
strokeWidth="2.00574"
|
||||
/>
|
||||
{ /* Line C */ }
|
||||
<path
|
||||
d="M270.402 28.9875V132.064"
|
||||
stroke={ highlight === 'C' ? '#006FAD' : '#CCCCCC' }
|
||||
strokeWidth="1.50431"
|
||||
strokeMiterlimit="16"
|
||||
/>
|
||||
{ /* Line B */ }
|
||||
<path
|
||||
d="M257.804 152.679L107.771 192.765"
|
||||
stroke={ highlight === 'B' ? '#006FAD' : '#CCCCCC' }
|
||||
strokeWidth="1.50431"
|
||||
strokeMiterlimit="16"
|
||||
/>
|
||||
<path
|
||||
d="M154.224 117.401L115.969 126.13C115.059 126.337 114.414 127.147 114.414 128.08V154.212C114.414 155.526 115.658 156.483 116.928 156.145L155.182 145.98C156.058 145.747 156.668 144.954 156.668 144.047V119.351C156.668 118.067 155.475 117.115 154.224 117.401Z"
|
||||
fill="#F0F0F0"
|
||||
stroke="#DDDDDD"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeDasharray="6 6"
|
||||
/>
|
||||
<path
|
||||
d="M12.0625 33.3189L159.943 1.6182C160.304 1.54091 160.679 1.5648 161.027 1.68725L249.4 32.7973"
|
||||
stroke="#DDDDDD"
|
||||
strokeWidth="2"
|
||||
d="M13.1406 33.41L161.446 1.61817C161.808 1.54066 162.184 1.56462 162.533 1.68742L251.16 32.8868"
|
||||
stroke="#E0E0E0"
|
||||
strokeWidth="2.00574"
|
||||
/>
|
||||
{ /* Label C */ }
|
||||
{ labels.C ? (
|
||||
<text
|
||||
x="280"
|
||||
y="85"
|
||||
fontSize={ 11 }
|
||||
fill={ highlight === 'C' ? '#007CBA' : '#949494' }
|
||||
>
|
||||
{ labels.C }
|
||||
</text>
|
||||
) : (
|
||||
<path
|
||||
d="M282.123 80.7892C282.123 79.5323 282.435 78.5405 283.058 77.8136C283.685 77.0867 284.537 76.7233 285.615 76.7233C286.467 76.7233 287.192 76.9739 287.79 77.4752C288.391 77.9729 288.741 78.6175 288.837 79.4088H287.639C287.525 78.9326 287.285 78.553 286.92 78.2701C286.558 77.9873 286.123 77.8458 285.615 77.8458C284.92 77.8458 284.368 78.1108 283.96 78.6407C283.556 79.1671 283.353 79.8833 283.353 80.7892C283.353 81.6915 283.556 82.4077 283.96 82.9376C284.368 83.464 284.922 83.7272 285.62 83.7272C286.132 83.7272 286.569 83.5983 286.93 83.3405C287.296 83.0826 287.532 82.7353 287.639 82.2985H288.837C288.73 83.0647 288.382 83.6824 287.795 84.1515C287.208 84.617 286.483 84.8497 285.62 84.8497C284.542 84.8497 283.69 84.4863 283.063 83.7594C282.437 83.0325 282.123 82.0424 282.123 80.7892Z"
|
||||
fill={ highlight === 'C' ? '#007CBA' : '#949494' }
|
||||
/>
|
||||
) }
|
||||
|
||||
{ /* Arrow A */ }
|
||||
<path
|
||||
d="M0.214844 149.961L5.30102 156.971L8.8282 149.061L0.214844 149.961ZM84.7236 187.648L79.6374 180.638L76.1102 188.548L84.7236 187.648ZM6.07417 153.396L78.2533 185.584L78.8642 184.214L6.68509 152.026L6.07417 153.396Z"
|
||||
fill={ highlight === 'A' ? '#007CBA' : '#BBBBBB' }
|
||||
/>
|
||||
{ /* Arrow B */ }
|
||||
<path
|
||||
d="M256.025 152.246L247.662 149.998L249.897 158.365L256.025 152.246ZM106.422 192.216L114.785 194.463L112.55 186.097L106.422 192.216ZM249.31 153.263L112.75 189.749L113.137 191.198L249.698 154.713L249.31 153.263Z"
|
||||
fill={ highlight === 'B' ? '#007CBA' : '#BBBBBB' }
|
||||
/>
|
||||
{ /* Arrow C */ }
|
||||
<path
|
||||
d="M268.586 28.908L264.256 36.408H272.916L268.586 28.908ZM268.586 131.689L272.916 124.189H264.256L268.586 131.689ZM267.836 35.658V124.939H269.336V35.658H267.836Z"
|
||||
fill={ highlight === 'C' ? '#007CBA' : '#BBBBBB' }
|
||||
/>
|
||||
{ /* Label B */ }
|
||||
{ labels.B ? (
|
||||
<text
|
||||
x="188"
|
||||
y="190"
|
||||
fontSize={ 11 }
|
||||
fill={ highlight === 'B' ? '#007CBA' : '#949494' }
|
||||
>
|
||||
{ labels.B }
|
||||
</text>
|
||||
) : (
|
||||
<path
|
||||
d="M192.281 189.611V181.861H195.396C196.123 181.861 196.692 182.034 197.104 182.382C197.519 182.725 197.727 183.196 197.727 183.794C197.727 184.199 197.596 184.562 197.335 184.885C197.073 185.203 196.751 185.395 196.368 185.459V185.551C196.891 185.604 197.312 185.803 197.63 186.147C197.953 186.487 198.114 186.91 198.114 187.414C198.114 188.098 197.879 188.635 197.41 189.026C196.941 189.416 196.293 189.611 195.466 189.611H192.281ZM193.484 188.591H195.224C195.765 188.591 196.177 188.483 196.459 188.268C196.742 188.054 196.884 187.74 196.884 187.328C196.884 186.924 196.737 186.618 196.443 186.41C196.15 186.199 195.72 186.093 195.154 186.093H193.484V188.591ZM193.484 185.142H194.913C195.442 185.142 195.844 185.048 196.116 184.858C196.391 184.664 196.529 184.383 196.529 184.015C196.529 183.656 196.404 183.379 196.153 183.182C195.906 182.981 195.561 182.881 195.117 182.881H193.484V185.142Z"
|
||||
fill={ highlight === 'B' ? '#007CBA' : '#949494' }
|
||||
/>
|
||||
) }
|
||||
|
||||
{ /* Letter A */ }
|
||||
<path
|
||||
d="M26.8564 184.66L29.6548 176.909H30.9492L33.7476 184.66H32.4692L31.7603 182.603H28.8062L28.0918 184.66H26.8564ZM29.1123 181.593H31.4541L30.3315 178.316H30.2402L29.1123 181.593Z"
|
||||
fill={ highlight === 'A' ? '#007CBA' : '#757575' }
|
||||
/>
|
||||
{ /* Letter B */ }
|
||||
<path
|
||||
d="M189.621 189.228V181.478H192.736C193.463 181.478 194.032 181.651 194.444 181.999C194.859 182.342 195.067 182.813 195.067 183.411C195.067 183.816 194.936 184.179 194.675 184.501C194.413 184.82 194.091 185.012 193.708 185.076V185.167C194.231 185.221 194.652 185.42 194.97 185.764C195.292 186.104 195.454 186.526 195.454 187.031C195.454 187.715 195.219 188.252 194.75 188.643C194.281 189.033 193.633 189.228 192.806 189.228H189.621ZM190.824 188.208H192.564C193.105 188.208 193.516 188.1 193.799 187.885C194.082 187.67 194.224 187.357 194.224 186.945C194.224 186.541 194.077 186.235 193.783 186.027C193.49 185.816 193.06 185.71 192.494 185.71H190.824V188.208ZM190.824 184.759H192.252C192.782 184.759 193.183 184.664 193.456 184.475C193.731 184.281 193.869 184 193.869 183.631C193.869 183.273 193.744 182.996 193.493 182.799C193.246 182.598 192.901 182.498 192.457 182.498H190.824V184.759Z"
|
||||
fill={ highlight === 'B' ? '#007CBA' : '#757575' }
|
||||
/>
|
||||
{ /* Letter C */ }
|
||||
<path
|
||||
d="M279.519 80.2898C279.519 79.033 279.83 78.0411 280.453 77.3142C281.08 76.5873 281.932 76.2239 283.01 76.2239C283.862 76.2239 284.587 76.4745 285.185 76.9758C285.787 77.4736 286.136 78.1181 286.232 78.9094H285.035C284.92 78.4332 284.68 78.0536 284.315 77.7708C283.953 77.4879 283.518 77.3464 283.01 77.3464C282.315 77.3464 281.764 77.6114 281.355 78.1414C280.951 78.6677 280.749 79.3839 280.749 80.2898C280.749 81.1921 280.951 81.9083 281.355 82.4382C281.764 82.9646 282.317 83.2278 283.015 83.2278C283.527 83.2278 283.964 83.0989 284.326 82.8411C284.691 82.5833 284.927 82.2359 285.035 81.7991H286.232C286.125 82.5653 285.778 83.183 285.19 83.6521C284.603 84.1176 283.878 84.3503 283.015 84.3503C281.937 84.3503 281.085 83.9869 280.458 83.26C279.832 82.5331 279.519 81.5431 279.519 80.2898Z"
|
||||
fill={ highlight === 'C' ? '#007CBA' : '#757575' }
|
||||
/>
|
||||
{ /* Label A */ }
|
||||
{ labels.A ? (
|
||||
<text
|
||||
x="18"
|
||||
y="185"
|
||||
fontSize={ 11 }
|
||||
fill={ highlight === 'A' ? '#007CBA' : '#949494' }
|
||||
>
|
||||
{ labels.A }
|
||||
</text>
|
||||
) : (
|
||||
<path
|
||||
d="M22.7694 185.149L25.5678 177.399H26.8622L29.6605 185.149H28.3822L27.6732 183.092H24.7191L24.0048 185.149H22.7694ZM25.0253 182.082H27.3671L26.2445 178.806H26.1532L25.0253 182.082Z"
|
||||
fill={ highlight === 'A' ? '#007CBA' : '#949494' }
|
||||
/>
|
||||
) }
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,4 +2,9 @@ export type HighlightSides = 'A' | 'B' | 'C';
|
|||
|
||||
export type ShippingDimensionsImageProps = React.SVGProps< SVGSVGElement > & {
|
||||
highlight?: HighlightSides;
|
||||
labels?: {
|
||||
A?: string | number;
|
||||
B?: string | number;
|
||||
C?: string | number;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -219,7 +219,7 @@ describe( 'Activity Panel', () => {
|
|||
expect( getByText( 'Finish setup' ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should not render the finish setup link when a user does not have capabilties', () => {
|
||||
it( 'should not render the finish setup link when a user does not have capabilities', () => {
|
||||
useUser.mockImplementation( () => ( {
|
||||
currentUserCan: () => false,
|
||||
} ) );
|
||||
|
|
|
@ -45,7 +45,7 @@ class ReportFilters extends Component {
|
|||
// This event gets triggered in the following cases.
|
||||
// 1. Select "Single product" and choose a product.
|
||||
// 2. Select "Comparison" or any other filter types.
|
||||
// The comparsion and other filter types require a user to click
|
||||
// The comparison and other filter types require a user to click
|
||||
// a button to execute a query, so this is not a good place to
|
||||
// trigger a CES survey for those.
|
||||
const triggerCesFor = [
|
||||
|
|
|
@ -138,7 +138,7 @@ const ReportTable = ( props ) => {
|
|||
};
|
||||
|
||||
const filterShownHeaders = ( headers, hiddenKeys ) => {
|
||||
// If no user preferences, set visibilty based on column default.
|
||||
// If no user preferences, set visibility based on column default.
|
||||
if ( ! hiddenKeys ) {
|
||||
return headers.map( ( header ) => ( {
|
||||
...header,
|
||||
|
@ -146,7 +146,7 @@ const ReportTable = ( props ) => {
|
|||
} ) );
|
||||
}
|
||||
|
||||
// Set visibilty based on user preferences.
|
||||
// Set visibility based on user preferences.
|
||||
return headers.map( ( header ) => ( {
|
||||
...header,
|
||||
visible: header.required || ! hiddenKeys.includes( header.key ),
|
||||
|
|
|
@ -24,7 +24,7 @@ Each menu item is defined by an array containing `id`, `title`, `parent`, and `p
|
|||
|
||||
- `report` (string): The report's id.
|
||||
- `title` (string): The title shown in the sidebar.
|
||||
- `parent` (string): The item's parent in the navigational heirarchy.
|
||||
- `parent` (string): The item's parent in the navigational hierarchy.
|
||||
- `path` (string): The report's relative path.
|
||||
|
||||
Next, hook into the JavaScript reports filter, `woocommerce_admin_reports_list`, to add a report component.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Settings
|
||||
=======
|
||||
|
||||
The settings used to modify the way data is retreived or displayed in WooCommerce reports.
|
||||
The settings used to modify the way data is retrieved or displayed in WooCommerce reports.
|
||||
|
||||
## Extending Settings
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ const DEFAULT_SECTIONS_FILTER = 'woocommerce_dashboard_default_sections';
|
|||
* @property {string} key Unique identifying string.
|
||||
* @property {Node} component React component to render.
|
||||
* @property {string} title Title.
|
||||
* @property {boolean} isVisible The default visibilty.
|
||||
* @property {boolean} isVisible The default visibility.
|
||||
* @property {Node} icon Section icon.
|
||||
* @property {Array.<string>} hiddenBlocks Blocks that are hidden by default.
|
||||
*/
|
||||
|
|
|
@ -47,7 +47,7 @@ export const useSendMagicLink = () => {
|
|||
setRequestState( SendMagicLinkStates.ERROR );
|
||||
createNotice(
|
||||
'error',
|
||||
__( 'Sorry, an unknown error occured.', 'woocommerce' )
|
||||
__( 'Sorry, an unknown error occurred.', 'woocommerce' )
|
||||
);
|
||||
}
|
||||
} )
|
||||
|
|
|
@ -289,7 +289,7 @@ export const getPages = () => {
|
|||
container: SettingsGroup,
|
||||
path: '/settings/:page',
|
||||
breadcrumbs: ( { match } ) => {
|
||||
// @todo This might need to be refactored to retreive groups via data store.
|
||||
// @todo This might need to be refactored to retrieve groups via data store.
|
||||
const settingsPages = getAdminSetting( 'settingsPages' );
|
||||
const page = settingsPages[ match.params.page ];
|
||||
if ( ! page ) {
|
||||
|
|
|
@ -19,7 +19,7 @@ const Item = ( { item } ) => {
|
|||
// and should not be a tabbable element.
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
||||
|
||||
// Only render a slot if a coresponding Fill exists and the item is not a category
|
||||
// Only render a slot if a corresponding Fill exists and the item is not a category
|
||||
if ( hasFills && ! item.isCategory ) {
|
||||
return (
|
||||
<NavigationItem key={ item.id } item={ item.id }>
|
||||
|
|
|
@ -190,7 +190,7 @@ export const sortMenuItems = ( menuItems: Item[] ): Item[] => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Get a flat tree structure of all Categories and thier children grouped by menuId
|
||||
* Get a flat tree structure of all Categories and their children grouped by menuId
|
||||
*
|
||||
* @param {Array} menuItems Array of menu items.
|
||||
* @param {Function} currentUserCan Callback method passed the capability to determine if a menu item is visible.
|
||||
|
|
|
@ -94,7 +94,7 @@ const EditProductPage: React.FC = () => {
|
|||
);
|
||||
|
||||
useEffect( () => {
|
||||
// used for determening the wasDeletedUsingAction condition.
|
||||
// used for determining the wasDeletedUsingAction condition.
|
||||
if (
|
||||
previousProductRef.current &&
|
||||
product &&
|
||||
|
|
|
@ -181,7 +181,7 @@ export class StoreDetails extends Component {
|
|||
* `await` and performs an update aysnchronously. This means the following
|
||||
* screen may not be initialized with correct profile settings.
|
||||
*
|
||||
* This comment may be removed when a refactor to wp.data datatores is complete.
|
||||
* This comment may be removed when a refactor to wp.data datastores is complete.
|
||||
*/
|
||||
if (
|
||||
region !== 'US' &&
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Temporary fix for compability with the Jetpack masterbar
|
||||
// Temporary fix for compatibility with the Jetpack masterbar
|
||||
// See https://github.com/Automattic/jetpack/issues/9608
|
||||
@include breakpoint( '<782px' ) {
|
||||
.jetpack-masterbar {
|
||||
|
|
|
@ -210,7 +210,7 @@ class Appearance extends Component {
|
|||
this.setState( { isUpdatingLogo: false } );
|
||||
createNotice(
|
||||
'success',
|
||||
__( 'Store logo updated sucessfully', 'woocommerce' )
|
||||
__( 'Store logo updated successfully', 'woocommerce' )
|
||||
);
|
||||
this.completeStep();
|
||||
} else {
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
.woocommerce-products-load-sample-product-confirm-modal-overlay {
|
||||
@media (min-width: 783px ) {
|
||||
& {
|
||||
left: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
@include break-large {
|
||||
& {
|
||||
left: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-products-load-sample-product-confirm-modal {
|
||||
.components-truncate.components-text.woocommerce-confirmation-modal__message {
|
||||
color: $studio-gray-60;
|
||||
|
|
|
@ -22,6 +22,7 @@ export const LoadSampleProductConfirmModal: React.VFC< Props > = ( {
|
|||
return (
|
||||
<Modal
|
||||
className="woocommerce-products-load-sample-product-confirm-modal"
|
||||
overlayClassName="woocommerce-products-load-sample-product-confirm-modal-overlay"
|
||||
title={ __( 'Load sample products', 'woocommerce' ) }
|
||||
onRequestClose={ onCancel }
|
||||
>
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.components-modal__content {
|
||||
.components-modal__content,
|
||||
.components-modal__header + div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
Scripts located in this directory are meant to be loaded on wp-admin pages outside the context of WooCommerce Admin, such as the post editor. Adding the script name to `wpAdminScripts` in the Webpack config will automatically build these scripts.
|
||||
|
||||
Scripts must be manually enqueued with any neccessary dependencies. For example, `onboarding-homepage-notice` uses the WooCommerce navigation package:
|
||||
Scripts must be manually enqueued with any necessary dependencies. For example, `onboarding-homepage-notice` uses the WooCommerce navigation package:
|
||||
|
||||
`wp_enqueue_script( 'onboarding-homepage-notice', Loader::get_url( 'wp-scripts/onboarding-homepage-notice.js' ), array( 'wc-navigation' ) );`
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* This is a workaround to bypass intalling the gateway plugins from WP.org.
|
||||
* This is a workaround to bypass installing the gateway plugins from WP.org.
|
||||
* This is not necessary provided your suggestion is a valid WP.org plugin.
|
||||
*
|
||||
* @param array $response Response data.
|
||||
|
|
|
@ -12,7 +12,7 @@ The fastest way to get started is by creating an example plugin from WooCommerce
|
|||
|
||||
`WC_EXT=add-navigation-items pnpm example --filter=woocommerce/client/admin`
|
||||
|
||||
This will create a new plugin that covers various features of the navigation and helps to register some intial items and categories within the new navigation menu. After running the command above, you can make edits directly to the files at `docs/examples/extensions/add-navigation-items` and they will be built and copied to your `wp-content/add-navigation-items` folder on save.
|
||||
This will create a new plugin that covers various features of the navigation and helps to register some initial items and categories within the new navigation menu. After running the command above, you can make edits directly to the files at `docs/examples/extensions/add-navigation-items` and they will be built and copied to your `wp-content/add-navigation-items` folder on save.
|
||||
|
||||
If you need to enable the WP Toolbar for debugging purposes in the new navigation, you can add the following filter to do so:
|
||||
|
||||
|
|
|
@ -32,8 +32,8 @@ $args = array(
|
|||
'can_view' => 'US:CA' === wc_get_base_location(),
|
||||
'level' => 3, // Priority level shown for extended tasks.
|
||||
'time' => __( '2 minutes', 'plugin-text-domain' ), // Time string for time to complete the task.
|
||||
'is_dismissable' => false, // Determine if the taks is dismissable.
|
||||
'is_snoozeable' => true, // Determine if the taks is snoozeable.
|
||||
'is_dismissable' => false, // Determine if the task is dismissable.
|
||||
'is_snoozeable' => true, // Determine if the task is snoozeable.
|
||||
'additional_info' => array( 'apples', 'oranges', 'bananas' ), // Additional info passed to the task.
|
||||
)
|
||||
$task = new Task( $args );
|
||||
|
|
|
@ -70,7 +70,7 @@ To disconnect from WooCommerce.com, go to `WooCommerce > Extensions > WooCommerc
|
|||
|
||||
## Jetpack Connection
|
||||
|
||||
Using Jetpack & WooCommerce Shipping & Tax allows us to offer additional features to new WooCommerce users as well as simplify parts of the setup process. For example, we can do automated tax calculations for certain countries, significantly simplifying the tax task. To make this work, the user needs to be connected to a WordPress.com account. This also means development and testing of these features needs to be done on a Jetpack connected site. Search the MGS & the Feld Guide for additional resources on testing Jetpack with local setups.
|
||||
Using Jetpack & WooCommerce Shipping & Tax allows us to offer additional features to new WooCommerce users as well as simplify parts of the setup process. For example, we can do automated tax calculations for certain countries, significantly simplifying the tax task. To make this work, the user needs to be connected to a WordPress.com account. This also means development and testing of these features needs to be done on a Jetpack connected site. Search the MGS & the Field Guide for additional resources on testing Jetpack with local setups.
|
||||
|
||||
We have a special Jetpack connection flow designed specifically for WooCommerce onboarding, so that the user feels that they are connecting as part of a cohesive experience. To access this flow, we have a custom Jetpack connection endpoint [/wc-admin/plugins/connect-jetpack](https://github.com/woocommerce/woocommerce/blob/feba6a8dcd55d4f5c7edc05478369c76df082293/plugins/woocommerce/src/Admin/API/Plugins.php#L395-L417).
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ After merchants click on a recommendation, plugins from this source will then wa
|
|||
|
||||
### Quick start
|
||||
|
||||
Gateway suggestions are retreived from a REST API and can be added via a remote JSON data source or filtered with the `woocommerce_admin_payment_gateway_suggestion_specs` filter.
|
||||
Gateway suggestions are retrieved from a REST API and can be added via a remote JSON data source or filtered with the `woocommerce_admin_payment_gateway_suggestion_specs` filter.
|
||||
|
||||
To quickly get started with an example plugin, run the following:
|
||||
|
||||
|
@ -22,7 +22,7 @@ If a user is not opted into marketplace suggestions or polling fails, the gatewa
|
|||
|
||||
## Remote Data Source Schema
|
||||
|
||||
The data source schema defines the recommended payment gateways and required plugins to kick of the setup process. The goal of this config is to provide the mininum amount of information possible to show a list of gateways and allow the gateways themselves to define specifics around configuration.
|
||||
The data source schema defines the recommended payment gateways and required plugins to kick of the setup process. The goal of this config is to provide the minimum amount of information possible to show a list of gateways and allow the gateways themselves to define specifics around configuration.
|
||||
|
||||
```json
|
||||
[
|
||||
|
|
|
@ -10,7 +10,7 @@ Currently, development efforts have been focused on two primary areas:
|
|||
|
||||
## Analytics
|
||||
|
||||
With WooCommerce installed, a new Analytics menu item is created in the wp-admin menu system. This menu item, and the reports contained insde of it are available to all wp-admin users that have the `view_woocommerce_reports` capability, so per a standard WooCommerce install this would give `shop_manager`s and `administrator`s access to the reports.
|
||||
With WooCommerce installed, a new Analytics menu item is created in the wp-admin menu system. This menu item, and the reports contained inside of it are available to all wp-admin users that have the `view_woocommerce_reports` capability, so per a standard WooCommerce install this would give `shop_manager`s and `administrator`s access to the reports.
|
||||
|
||||
Each report is quite unique with its own set of filtering options and chart types. To learn more about each individual report, please view the pages below:
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
The Activity Panel aims to be a "one stop shop" for managing your store - fulfill new orders, manage product inventory, moderate reviews, and get information about running your store.
|
||||
|
||||
Activity Panels can be accessed wherever the WooCommerce Admin nagivation bar is shown.
|
||||
Activity Panels can be accessed wherever the WooCommerce Admin navigation bar is shown.
|
||||
|
||||
![Activity Panels Tabs Overview](images/activity-panels-tabs.png)
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ The _Summary Number_ tab gives you a quick view at the total figure for that met
|
|||
### Chart
|
||||
![Analytics Chart](analytics-basics-chart.png)
|
||||
|
||||
The charts on report pages offer quite a few options to customize the visualiztion of data. The data legend ( labeled A ) allows you to toggle the visiblity of the different data set periods. The _Interval Selector_ ( labeled B ) allows you to adjust the interval displayed in the chart. The options available here are dependent upon the length of the date range selected:
|
||||
The charts on report pages offer quite a few options to customize the visualiztion of data. The data legend ( labeled A ) allows you to toggle the visibility of the different data set periods. The _Interval Selector_ ( labeled B ) allows you to adjust the interval displayed in the chart. The options available here are dependent upon the length of the date range selected:
|
||||
|
||||
| Length of Date Range | Interval Options |
|
||||
|---|---|
|
||||
|
@ -69,10 +69,10 @@ The table which displays the detailed data on Analytics reports also has a numbe
|
|||
|
||||
Many columns in reports will allow you to click on the column header to sort the tabular data by that value, and to either sort by that value in ascending or descending order. Simply click the column header to sort by that value, and click it again to change between ascending and descending sort.
|
||||
|
||||
### Toggle Column Visiblity
|
||||
### Toggle Column Visibility
|
||||
![Analytics Table Column Sorting](analytics-table-column-visbility.png)
|
||||
|
||||
If a report contains a data column that you don't need to be displayed, you can adjust the visiblity of it by using the visibility menu on the right side of the table header. Click the column name in the menu to change the visibility of the column. Your visibility selections are persisted to your user preferences for each report, so on subsequent visits to that report, the columns you have previously toggled off will not be displayed.
|
||||
If a report contains a data column that you don't need to be displayed, you can adjust the visibility of it by using the visibility menu on the right side of the table header. Click the column name in the menu to change the visibility of the column. Your visibility selections are persisted to your user preferences for each report, so on subsequent visits to that report, the columns you have previously toggled off will not be displayed.
|
||||
|
||||
### CSV Download
|
||||
![Analytics Table csv Download](analytics-table-download-button.png)
|
||||
|
@ -89,4 +89,4 @@ If your selected date range results in a data set that spans more then one page
|
|||
When the data displayed in the table is larger than the default single page size of 25, some pagination options will appear in the table footer area. Directional buttons, labeled `<` and `>` enable you to move backwards and forwards between pages, and a text field will allow you to jump to a specific page number. Furthermore you can change the number of rows to display per page.
|
||||
|
||||
### Table Search Box
|
||||
On some reports, a search box is displayed in the table header area as well. For details on what the search box does on a given report, please refer to the associated documentaiton page for that report.
|
||||
On some reports, a search box is displayed in the table header area as well. For details on what the search box does on a given report, please refer to the associated documentation page for that report.
|
|
@ -12,7 +12,7 @@ WooCommerce Admin is pre-configured with default settings for WooCommerce Analyt
|
|||
|
||||
![Excluded statuses settings](images/settings-excluded-statuses.png)
|
||||
|
||||
In this section, statuses that are **unchecked** are **included** in anayltics reports. **Checked** statuses are **excluded**. If your store uses custom order statuses, those statuses are included in the reports by default. They will be listed in this section under `Custom Statuses` and can be excluded via the status checkbox.
|
||||
In this section, statuses that are **unchecked** are **included** in analytics reports. **Checked** statuses are **excluded**. If your store uses custom order statuses, those statuses are included in the reports by default. They will be listed in this section under `Custom Statuses` and can be excluded via the status checkbox.
|
||||
|
||||
### Default Date Range
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
Correct spelling errors
|
|
@ -44,7 +44,7 @@ function _wc_beta_tester_load_textdomain() {
|
|||
add_action( 'plugins_loaded', '_wc_beta_tester_load_textdomain' );
|
||||
|
||||
/**
|
||||
* Boostrap plugin.
|
||||
* Bootstrap plugin.
|
||||
*/
|
||||
function _wc_beta_tester_bootstrap() {
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
Makes more information available to handlers for the `checkout_place_order` (and related) events.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add Sale price validation#37985
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Modify 'WC_Settings_Tracking' to allow dropdown options recording for WooCommerce Settings
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add e2e test for Merchant > Posts > Can create a new post
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
Correct spelling errors
|
|
@ -1,4 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
skip k6 api order RUD tests on non-existant order when C test fails
|
||||
skip k6 api order RUD tests on non-existent order when C test fails
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Fix loading sample product's progress message is misaligned if Gutenberg plugin is enabled
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
fix logout vs log out typo
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue