khaihihi
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
// Ensure textarea bg color is transparent for block titles.
|
||||
// Some themes (e.g. Twenty Twenty) set a non-white background for the editor, and Gutenberg sets white background for text inputs, creating this issue.
|
||||
// https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/1204
|
||||
.wc-block-component-title {
|
||||
background-color: transparent;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import { PlainText } from '@wordpress/block-editor';
|
||||
import classnames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './editor.scss';
|
||||
|
||||
const BlockTitle = ( { className, headingLevel, onChange, heading } ) => {
|
||||
const TagName = `h${ headingLevel }`;
|
||||
return (
|
||||
<TagName>
|
||||
<PlainText
|
||||
className={ classnames(
|
||||
'wc-block-component-title',
|
||||
className
|
||||
) }
|
||||
value={ heading }
|
||||
onChange={ onChange }
|
||||
/>
|
||||
</TagName>
|
||||
);
|
||||
};
|
||||
|
||||
BlockTitle.propTypes = {
|
||||
/**
|
||||
* Classname to add to title in addition to the defaults.
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
/**
|
||||
* The value of the heading.
|
||||
*/
|
||||
value: PropTypes.string,
|
||||
/**
|
||||
* Callback to update the attribute when text is changed.
|
||||
*/
|
||||
onChange: PropTypes.func,
|
||||
/**
|
||||
* Level of the heading tag (1, 2, 3... will render <h1>, <h2>, <h3>... elements).
|
||||
*/
|
||||
headingLevel: PropTypes.number,
|
||||
};
|
||||
|
||||
export default BlockTitle;
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { escapeHTML } from '@wordpress/escape-html';
|
||||
|
||||
const getErrorMessage = ( { message, type } ) => {
|
||||
if ( ! message ) {
|
||||
return __(
|
||||
'An unknown error occurred which prevented the block from being updated.',
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
|
||||
if ( type === 'general' ) {
|
||||
return (
|
||||
<span>
|
||||
{ __(
|
||||
'The following error was returned',
|
||||
'woocommerce'
|
||||
) }
|
||||
<br />
|
||||
<code>{ escapeHTML( message ) }</code>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if ( type === 'api' ) {
|
||||
return (
|
||||
<span>
|
||||
{ __(
|
||||
'The following error was returned from the API',
|
||||
'woocommerce'
|
||||
) }
|
||||
<br />
|
||||
<code>{ escapeHTML( message ) }</code>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
const ErrorMessage = ( { error } ) => (
|
||||
<div className="wc-block-error-message">{ getErrorMessage( error ) }</div>
|
||||
);
|
||||
|
||||
ErrorMessage.propTypes = {
|
||||
/**
|
||||
* The error object.
|
||||
*/
|
||||
error: PropTypes.shape( {
|
||||
/**
|
||||
* Human-readable error message to display.
|
||||
*/
|
||||
message: PropTypes.node,
|
||||
/**
|
||||
* Context in which the error was triggered. That will determine how the error is displayed to the user.
|
||||
*/
|
||||
type: PropTypes.oneOf( [ 'api', 'general' ] ),
|
||||
} ),
|
||||
};
|
||||
|
||||
export default ErrorMessage;
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Fragment } from '@wordpress/element';
|
||||
import PropTypes from 'prop-types';
|
||||
import Gridicon from 'gridicons';
|
||||
import classNames from 'classnames';
|
||||
import { Button, Placeholder, Spinner } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import ErrorMessage from './error-message.js';
|
||||
import './style.scss';
|
||||
|
||||
const ErrorPlaceholder = ( { className, error, isLoading, onRetry } ) => (
|
||||
<Placeholder
|
||||
icon={ <Gridicon icon="notice" /> }
|
||||
label={ __(
|
||||
'Sorry, an error occurred',
|
||||
'woocommerce'
|
||||
) }
|
||||
className={ classNames( 'wc-block-api-error', className ) }
|
||||
>
|
||||
<ErrorMessage error={ error } />
|
||||
{ onRetry && (
|
||||
<Fragment>
|
||||
{ isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Button isDefault onClick={ onRetry }>
|
||||
{ __( 'Retry', 'woocommerce' ) }
|
||||
</Button>
|
||||
) }
|
||||
</Fragment>
|
||||
) }
|
||||
</Placeholder>
|
||||
);
|
||||
|
||||
ErrorPlaceholder.propTypes = {
|
||||
/**
|
||||
* Classname to add to placeholder in addition to the defaults.
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
/**
|
||||
* The error object.
|
||||
*/
|
||||
error: PropTypes.shape( {
|
||||
/**
|
||||
* Human-readable error message to display.
|
||||
*/
|
||||
message: PropTypes.node,
|
||||
/**
|
||||
* Context in which the error was triggered. That will determine how the error is displayed to the user.
|
||||
*/
|
||||
type: PropTypes.oneOf( [ 'api', 'general' ] ),
|
||||
} ),
|
||||
/**
|
||||
* Whether there is a request running, so the 'Retry' button is hidden and
|
||||
* a spinner is shown instead.
|
||||
*/
|
||||
isLoading: PropTypes.bool,
|
||||
/**
|
||||
* Callback to retry an action.
|
||||
*/
|
||||
onRetry: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ErrorPlaceholder;
|
||||
@@ -0,0 +1,18 @@
|
||||
.wc-block-error-message {
|
||||
margin-bottom: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.wc-block-api-error {
|
||||
.components-placeholder__fieldset {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wc-block-error-message {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.components-spinner {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Fragment } from '@wordpress/element';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ToggleControl } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* A combination of toggle controls for content visibility in product grids.
|
||||
*/
|
||||
const GridContentControl = ( { onChange, settings } ) => {
|
||||
const { button, price, rating, title } = settings;
|
||||
return (
|
||||
<Fragment>
|
||||
<ToggleControl
|
||||
label={ __( 'Product title', 'woocommerce' ) }
|
||||
help={
|
||||
title
|
||||
? __(
|
||||
'Product title is visible.',
|
||||
'woocommerce'
|
||||
)
|
||||
: __(
|
||||
'Product title is hidden.',
|
||||
'woocommerce'
|
||||
)
|
||||
}
|
||||
checked={ title }
|
||||
onChange={ () => onChange( { ...settings, title: ! title } ) }
|
||||
/>
|
||||
<ToggleControl
|
||||
label={ __( 'Product price', 'woocommerce' ) }
|
||||
help={
|
||||
price
|
||||
? __(
|
||||
'Product price is visible.',
|
||||
'woocommerce'
|
||||
)
|
||||
: __(
|
||||
'Product price is hidden.',
|
||||
'woocommerce'
|
||||
)
|
||||
}
|
||||
checked={ price }
|
||||
onChange={ () => onChange( { ...settings, price: ! price } ) }
|
||||
/>
|
||||
<ToggleControl
|
||||
label={ __( 'Product rating', 'woocommerce' ) }
|
||||
help={
|
||||
rating
|
||||
? __(
|
||||
'Product rating is visible.',
|
||||
'woocommerce'
|
||||
)
|
||||
: __(
|
||||
'Product rating is hidden.',
|
||||
'woocommerce'
|
||||
)
|
||||
}
|
||||
checked={ rating }
|
||||
onChange={ () => onChange( { ...settings, rating: ! rating } ) }
|
||||
/>
|
||||
<ToggleControl
|
||||
label={ __(
|
||||
'Add to Cart button',
|
||||
'woocommerce'
|
||||
) }
|
||||
help={
|
||||
button
|
||||
? __(
|
||||
'Add to Cart button is visible.',
|
||||
'woocommerce'
|
||||
)
|
||||
: __(
|
||||
'Add to Cart button is hidden.',
|
||||
'woocommerce'
|
||||
)
|
||||
}
|
||||
checked={ button }
|
||||
onChange={ () => onChange( { ...settings, button: ! button } ) }
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
GridContentControl.propTypes = {
|
||||
/**
|
||||
* The current title visibility.
|
||||
*/
|
||||
settings: PropTypes.shape( {
|
||||
button: PropTypes.bool.isRequired,
|
||||
price: PropTypes.bool.isRequired,
|
||||
rating: PropTypes.bool.isRequired,
|
||||
title: PropTypes.bool.isRequired,
|
||||
} ).isRequired,
|
||||
/**
|
||||
* Callback to update the layout settings.
|
||||
*/
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default GridContentControl;
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { clamp, isNaN } from 'lodash';
|
||||
import { Fragment } from '@wordpress/element';
|
||||
import PropTypes from 'prop-types';
|
||||
import { RangeControl, ToggleControl } from '@wordpress/components';
|
||||
import {
|
||||
MAX_COLUMNS,
|
||||
MIN_COLUMNS,
|
||||
MAX_ROWS,
|
||||
MIN_ROWS,
|
||||
} from '@woocommerce/block-settings';
|
||||
|
||||
/**
|
||||
* A combination of range controls for product grid layout settings.
|
||||
*/
|
||||
const GridLayoutControl = ( {
|
||||
columns,
|
||||
rows,
|
||||
setAttributes,
|
||||
alignButtons,
|
||||
} ) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<RangeControl
|
||||
label={ __( 'Columns', 'woocommerce' ) }
|
||||
value={ columns }
|
||||
onChange={ ( value ) => {
|
||||
const newValue = clamp( value, MIN_COLUMNS, MAX_COLUMNS );
|
||||
setAttributes( {
|
||||
columns: isNaN( newValue ) ? '' : newValue,
|
||||
} );
|
||||
} }
|
||||
min={ MIN_COLUMNS }
|
||||
max={ MAX_COLUMNS }
|
||||
/>
|
||||
<RangeControl
|
||||
label={ __( 'Rows', 'woocommerce' ) }
|
||||
value={ rows }
|
||||
onChange={ ( value ) => {
|
||||
const newValue = clamp( value, MIN_ROWS, MAX_ROWS );
|
||||
setAttributes( {
|
||||
rows: isNaN( newValue ) ? '' : newValue,
|
||||
} );
|
||||
} }
|
||||
min={ MIN_ROWS }
|
||||
max={ MAX_ROWS }
|
||||
/>
|
||||
<ToggleControl
|
||||
label={ __( 'Align Buttons', 'woocommerce' ) }
|
||||
help={
|
||||
alignButtons
|
||||
? __(
|
||||
'Buttons are aligned vertically.',
|
||||
'woocommerce'
|
||||
)
|
||||
: __(
|
||||
'Buttons follow content.',
|
||||
'woocommerce'
|
||||
)
|
||||
}
|
||||
checked={ alignButtons }
|
||||
onChange={ () =>
|
||||
setAttributes( { alignButtons: ! alignButtons } )
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
GridLayoutControl.propTypes = {
|
||||
/**
|
||||
* The current columns count.
|
||||
*/
|
||||
columns: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] )
|
||||
.isRequired,
|
||||
/**
|
||||
* The current rows count.
|
||||
*/
|
||||
rows: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] )
|
||||
.isRequired,
|
||||
/**
|
||||
* Whether or not buttons are aligned horizontally across items.
|
||||
*/
|
||||
alignButtons: PropTypes.bool.isRequired,
|
||||
/**
|
||||
* Callback to update the layout settings.
|
||||
*/
|
||||
setAttributes: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default GridLayoutControl;
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Path, SVG } from '@wordpress/components';
|
||||
|
||||
export default function HeadingLevelIcon( { level } ) {
|
||||
const levelToPath = {
|
||||
1: 'M9 5h2v10H9v-4H5v4H3V5h2v4h4V5zm6.6 0c-.6.9-1.5 1.7-2.6 2v1h2v7h2V5h-1.4z',
|
||||
2: 'M7 5h2v10H7v-4H3v4H1V5h2v4h4V5zm8 8c.5-.4.6-.6 1.1-1.1.4-.4.8-.8 1.2-1.3.3-.4.6-.8.9-1.3.2-.4.3-.8.3-1.3 0-.4-.1-.9-.3-1.3-.2-.4-.4-.7-.8-1-.3-.3-.7-.5-1.2-.6-.5-.2-1-.2-1.5-.2-.4 0-.7 0-1.1.1-.3.1-.7.2-1 .3-.3.1-.6.3-.9.5-.3.2-.6.4-.8.7l1.2 1.2c.3-.3.6-.5 1-.7.4-.2.7-.3 1.2-.3s.9.1 1.3.4c.3.3.5.7.5 1.1 0 .4-.1.8-.4 1.1-.3.5-.6.9-1 1.2-.4.4-1 .9-1.6 1.4-.6.5-1.4 1.1-2.2 1.6V15h8v-2H15z',
|
||||
3: 'M12.1 12.2c.4.3.8.5 1.2.7.4.2.9.3 1.4.3.5 0 1-.1 1.4-.3.3-.1.5-.5.5-.8 0-.2 0-.4-.1-.6-.1-.2-.3-.3-.5-.4-.3-.1-.7-.2-1-.3-.5-.1-1-.1-1.5-.1V9.1c.7.1 1.5-.1 2.2-.4.4-.2.6-.5.6-.9 0-.3-.1-.6-.4-.8-.3-.2-.7-.3-1.1-.3-.4 0-.8.1-1.1.3-.4.2-.7.4-1.1.6l-1.2-1.4c.5-.4 1.1-.7 1.6-.9.5-.2 1.2-.3 1.8-.3.5 0 1 .1 1.6.2.4.1.8.3 1.2.5.3.2.6.5.8.8.2.3.3.7.3 1.1 0 .5-.2.9-.5 1.3-.4.4-.9.7-1.5.9v.1c.6.1 1.2.4 1.6.8.4.4.7.9.7 1.5 0 .4-.1.8-.3 1.2-.2.4-.5.7-.9.9-.4.3-.9.4-1.3.5-.5.1-1 .2-1.6.2-.8 0-1.6-.1-2.3-.4-.6-.2-1.1-.6-1.6-1l1.1-1.4zM7 9H3V5H1v10h2v-4h4v4h2V5H7v4z',
|
||||
4: 'M9 15H7v-4H3v4H1V5h2v4h4V5h2v10zm10-2h-1v2h-2v-2h-5v-2l4-6h3v6h1v2zm-3-2V7l-2.8 4H16z',
|
||||
5: 'M12.1 12.2c.4.3.7.5 1.1.7.4.2.9.3 1.3.3.5 0 1-.1 1.4-.4.4-.3.6-.7.6-1.1 0-.4-.2-.9-.6-1.1-.4-.3-.9-.4-1.4-.4H14c-.1 0-.3 0-.4.1l-.4.1-.5.2-1-.6.3-5h6.4v1.9h-4.3L14 8.8c.2-.1.5-.1.7-.2.2 0 .5-.1.7-.1.5 0 .9.1 1.4.2.4.1.8.3 1.1.6.3.2.6.6.8.9.2.4.3.9.3 1.4 0 .5-.1 1-.3 1.4-.2.4-.5.8-.9 1.1-.4.3-.8.5-1.3.7-.5.2-1 .3-1.5.3-.8 0-1.6-.1-2.3-.4-.6-.2-1.1-.6-1.6-1-.1-.1 1-1.5 1-1.5zM9 15H7v-4H3v4H1V5h2v4h4V5h2v10z',
|
||||
6: 'M9 15H7v-4H3v4H1V5h2v4h4V5h2v10zm8.6-7.5c-.2-.2-.5-.4-.8-.5-.6-.2-1.3-.2-1.9 0-.3.1-.6.3-.8.5l-.6.9c-.2.5-.2.9-.2 1.4.4-.3.8-.6 1.2-.8.4-.2.8-.3 1.3-.3.4 0 .8 0 1.2.2.4.1.7.3 1 .6.3.3.5.6.7.9.2.4.3.8.3 1.3s-.1.9-.3 1.4c-.2.4-.5.7-.8 1-.4.3-.8.5-1.2.6-1 .3-2 .3-3 0-.5-.2-1-.5-1.4-.9-.4-.4-.8-.9-1-1.5-.2-.6-.3-1.3-.3-2.1s.1-1.6.4-2.3c.2-.6.6-1.2 1-1.6.4-.4.9-.7 1.4-.9.6-.3 1.1-.4 1.7-.4.7 0 1.4.1 2 .3.5.2 1 .5 1.4.8 0 .1-1.3 1.4-1.3 1.4zm-2.4 5.8c.2 0 .4 0 .6-.1.2 0 .4-.1.5-.2.1-.1.3-.3.4-.5.1-.2.1-.5.1-.7 0-.4-.1-.8-.4-1.1-.3-.2-.7-.3-1.1-.3-.3 0-.7.1-1 .2-.4.2-.7.4-1 .7 0 .3.1.7.3 1 .1.2.3.4.4.6.2.1.3.3.5.3.2.1.5.2.7.1z',
|
||||
};
|
||||
if ( ! levelToPath.hasOwnProperty( level ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SVG
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<Path d={ levelToPath[ level ] } />
|
||||
</SVG>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { range } from 'lodash';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { Toolbar } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import HeadingLevelIcon from './heading-level-icon';
|
||||
|
||||
/**
|
||||
* HeadingToolbar component.
|
||||
*
|
||||
* Allows the heading level to be chosen for a title block.
|
||||
*/
|
||||
class HeadingToolbar extends Component {
|
||||
createLevelControl( targetLevel, selectedLevel, onChange ) {
|
||||
const isActive = targetLevel === selectedLevel;
|
||||
return {
|
||||
icon: <HeadingLevelIcon level={ targetLevel } />,
|
||||
// translators: %s: heading level e.g: "2", "3", "4"
|
||||
title: sprintf( __( 'Heading %d' ), targetLevel ),
|
||||
isActive,
|
||||
onClick: () => onChange( targetLevel ),
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isCollapsed = true,
|
||||
minLevel,
|
||||
maxLevel,
|
||||
selectedLevel,
|
||||
onChange,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Toolbar
|
||||
isCollapsed={ isCollapsed }
|
||||
icon={ <HeadingLevelIcon level={ selectedLevel } /> }
|
||||
controls={ range( minLevel, maxLevel ).map( ( index ) =>
|
||||
this.createLevelControl( index, selectedLevel, onChange )
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default HeadingToolbar;
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default ( { className } ) => (
|
||||
<Icon
|
||||
className={ className }
|
||||
icon={
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 22.41">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M16.55 0H1.75C.56 0 0 .52 0 1.68v9.24c0 1.15.56 1.92 1.75 1.92H2V4.76a2.51 2.51 0 0 1 2.74-2.68H18v-.4A1.51 1.51 0 0 0 16.55 0z" />
|
||||
<path d="M19.54 3.11H4.74C3.55 3.11 3 3.64 3 4.79V14c0 1.19.55 2 1.74 2H5V7.88A2.51 2.51 0 0 1 7.73 5.2H21v-.41a1.51 1.51 0 0 0-1.46-1.68z" />
|
||||
<path d="M22.53 6.23H7.73C6.54 6.23 6 6.75 6 7.91v9.24c0 1.15.56 1.92 1.75 1.92h7a.59.59 0 0 1 .32.11l1.61 1.15 2.83 2a.31.31 0 0 0 .18.05.3.3 0 0 0 .31-.3v-2.73a.3.3 0 0 1 .28-.28h2.31A1.47 1.47 0 0 0 24 17.6V7.91a1.52 1.52 0 0 0-1.47-1.68z" />
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M10.2 10.92l1.06-2.14 1.05 2.14 2.37.35-1.71 1.66.4 2.36-2.11-1.11-.17.09-.52.27-.51.26-.92.49.18-1.02.09-.57.1-.56.03-.21-1.71-1.66 2.37-.35zM21.46 14.25h-4.4a.69.69 0 0 1-.69-.69.68.68 0 0 1 .69-.68h4.4a.68.68 0 0 1 .69.68.69.69 0 0 1-.69.69zM21.46 11.19h-4.4a.69.69 0 1 1 0-1.37h4.4a.69.69 0 1 1 0 1.37z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default () => (
|
||||
<Icon
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="#1E8CBE"
|
||||
d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default () => (
|
||||
<Icon
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="#6C7781"
|
||||
d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default () => (
|
||||
<Icon
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<mask id="external-mask" width="24" height="24" x="0" y="0" maskUnits="userSpaceOnUse">
|
||||
<path fill="#fff" d="M6.3431 6.3431v1.994l7.8984.0072-8.6055 8.6054 1.4142 1.4143 8.6055-8.6055.0071 7.8984h1.994V6.3431H6.3431z" />
|
||||
</mask>
|
||||
<g mask="url(#external-mask)">
|
||||
<path d="M0 0h24v24H0z" />
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default () => (
|
||||
<Icon
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M22 7.5H2c-1.2 0-2.1 1-2 2.2l.7 11.1c.1 1.1 1 1.9 2 1.9h18.5c1.1 0 2-.8 2-1.9L24 9.6c.1-1.1-.9-2.1-2-2.1zM13.1 2.8v-.4c0-1.1-.9-2-2-2H4.8c-1.1 0-2 .9-2 2v3.4h18.4v-1c0-1.1-.9-2-2-2h-6.1z" />
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M14.4 18.7L12 17.4l-2.4 1.3.5-2.6-1.9-1.9 2.6-.4 1.2-2.4 1.2 2.4 2.6.4-1.9 1.9z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default () => (
|
||||
<Icon
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fillRule="nonzero"
|
||||
d="M21.913 7.0946H2.0254c-1.1708 0-2.0984.9908-2.0205 2.16l.741 11.0724c.0714 1.0638.9552 1.8892 2.0206 1.8892h18.4054c1.0654 0 1.9492-.8254 2.0205-1.8892l.7411-11.0724c.0779-1.1692-.8497-2.16-2.0205-2.16zm-8.8006-4.6573h5.987c1.119 0 2.0255.9065 2.0255 2.0254v.934H2.8103V2.0255C2.8103.9064 3.7168 0 4.8357 0h6.2513c1.119 0 2.0254.9065 2.0254 2.0254v.4119zm-7.0589 11.619a.926.926 0 1 1 0-1.852h11.8297a.926.926 0 1 1 0 1.852H6.0535z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
// Export each icon as a named component.
|
||||
export { default as IconAllReviews } from './all-reviews';
|
||||
export { default as IconCheckChecked } from './checkbox-checked';
|
||||
export { default as IconCheckUnchecked } from './checkbox-unchecked';
|
||||
export { default as IconFolder } from './folder';
|
||||
export { default as IconFolderStar } from './folder-star';
|
||||
export { default as IconProductOnSale } from './product-on-sale';
|
||||
export { default as IconProductSearch } from './product-search';
|
||||
export { default as IconProductTag } from './product-tag';
|
||||
export { default as IconNewReleases } from './new-releases';
|
||||
export { default as IconRadioSelected } from './radio-selected';
|
||||
export { default as IconRadioUnselected } from './radio-unselected';
|
||||
export { default as IconReviewsByProduct } from './reviews-by-product';
|
||||
export { default as IconReviewsByCategory } from './reviews-by-category';
|
||||
export { default as IconWidgets } from './widgets';
|
||||
export { default as IconWoo } from './woo';
|
||||
export { default as IconMoney } from './money';
|
||||
export { default as IconExternal } from './external';
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default () => (
|
||||
<Icon
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<mask id="money-mask" width="20" height="14" x="2" y="5" maskUnits="userSpaceOnUse">
|
||||
<path fill="#fff" fillRule="evenodd" d="M2 5v14h20V5H2zm5 12c0-1.657-1.343-3-3-3v-4c1.657 0 3-1.343 3-3h10c0 1.657 1.343 3 3 3v4c-1.657 0-3 1.343-3 3H7zm7-5c0-1.7-.9-3-2-3s-2 1.3-2 3 .9 3 2 3 2-1.3 2-3z" clipRule="evenodd" />
|
||||
</mask>
|
||||
<g mask="url(#money-mask)">
|
||||
<path d="M0 0h24v24H0z" />
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default () => (
|
||||
<Icon
|
||||
className="material-icon"
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M23 12l-2.44-2.78.34-3.68-3.61-.82-1.89-3.18L12 3 8.6 1.54 6.71 4.72l-3.61.81.34 3.68L1 12l2.44 2.78-.34 3.69 3.61.82 1.89 3.18L12 21l3.4 1.46 1.89-3.18 3.61-.82-.34-3.68L23 12zm-10 5h-2v-2h2v2zm0-4h-2V7h2v6z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default ( { className } ) => (
|
||||
<Icon
|
||||
className={ className }
|
||||
icon={
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.5,0h-9.3L0.8,11.4c-1,1-1,2.6,0,3.6L9,23.2c1,1,2.6,1,3.6,0c0,0,0,0,0,0L24,11.9V2.5C24,1.1,22.9,0,21.5,0z" />
|
||||
<circle fill="#fff" cx="18.1" cy="5.8" r="2.5" />
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M5.8,10.5C5.8,10,6,9.6,6.3,9.2s0.8-0.5,1.3-0.5c0.6,0,1,0.2,1.3,0.5s0.5,0.8,0.5,1.3V11 c0,0.5-0.2,1-0.5,1.3s-0.8,0.5-1.3,0.5c-0.6,0-1-0.2-1.3-0.5s-0.5-0.8-0.5-1.3V10.5z M7,11c0,0.2,0.1,0.4,0.2,0.6 s0.3,0.2,0.5,0.2s0.4-0.1,0.5-0.2s0.2-0.3,0.2-0.6v-0.4c0-0.2-0.1-0.4-0.2-0.6S7.9,9.7,7.7,9.7c-0.2,0-0.4,0.1-0.5,0.2 S7,10.3,7,10.5V11z M8,16.8l-0.8-0.4l4.2-6.7l0.8,0.4L8,16.8z M9.9,15.3c0-0.5,0.2-1,0.5-1.3s0.8-0.5,1.3-0.5 c0.6,0,1,0.2,1.3,0.5s0.5,0.8,0.5,1.3v0.4c0,0.5-0.2,0.9-0.5,1.3s-0.8,0.5-1.3,0.5c-0.6,0-1-0.2-1.4-0.5s-0.5-0.8-0.5-1.3V15.3z M11,15.8c0,0.2,0.1,0.4,0.2,0.6s0.3,0.2,0.5,0.2c0.5,0,0.7-0.3,0.7-0.8v-0.4c0-0.2-0.1-0.4-0.2-0.6s-0.3-0.2-0.5-0.2 s-0.4,0.1-0.5,0.2S11,15.1,11,15.3V15.8z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default ( { className } ) => (
|
||||
<Icon
|
||||
className={ className }
|
||||
icon={
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 23">
|
||||
<path d="M13.0993 2.41207V2.01231C13.0993 0.912976 12.1999 0.0135193 11.1005 0.0135193H4.80435C3.70501 0.0135193 2.80556 0.912976 2.80556 2.01231V5.41026H21.1944V4.5108C21.1944 3.41147 20.295 2.51201 19.1957 2.51201H13.0993V2.41207Z" />
|
||||
<path d="M8.60759 11.3092C7.50759 12.4092 7.50759 14.2092 8.60759 15.3092C9.70759 16.4092 11.5076 16.4092 12.6076 15.3092C13.7076 14.2092 13.7076 12.4092 12.6076 11.3092C11.5076 10.2092 9.70759 10.2092 8.60759 11.3092Z" />
|
||||
<path d="M22.0076 7.10919H2.00759C0.80759 7.10919 -0.0924101 8.10919 0.00758988 9.30919L0.70759 20.4092C0.80759 21.5092 1.70759 22.3092 2.70759 22.3092H21.2076C22.3076 22.3092 23.2076 21.5092 23.2076 20.4092L24.0076 9.30919C24.1076 8.10919 23.1076 7.10919 22.0076 7.10919ZM16.5076 20.2092L13.4076 17.1092V16.5092L13.3076 16.4092C11.7076 17.8092 9.30759 17.7092 7.80759 16.2092C6.20759 14.6092 6.20759 12.0092 7.80759 10.4092C9.40759 8.80919 12.0076 8.80919 13.6076 10.4092C15.1076 11.9092 15.2076 14.3092 13.8076 15.9092L13.9076 16.0092H14.4076L17.5076 19.1092L16.5076 20.2092Z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default ( { className } ) => (
|
||||
<Icon
|
||||
className={ className }
|
||||
icon={
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.45,0H12.14L.75,11.4A2.55,2.55,0,0,0,.75,15L9,23.25a2.55,2.55,0,0,0,3.61,0L24,11.86V2.55A2.55,2.55,0,0,0,21.45,0Z" />
|
||||
<circle fill="#fff" cx="18.07" cy="5.75" r="2.47" />
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M9.27,9.53c-.14-.53.19-.85.72-.72l3.17.82a1.83,1.83,0,0,1,1.21,1.21L15.19,14c.13.53-.19.86-.72.72l-3.17-.81a1.9,1.9,0,0,1-1.22-1.22Z"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M14.14,15.71a.52.52,0,0,1,.26,1L12.09,19a1.94,1.94,0,0,1-1.68.46l-3.16-.81a.52.52,0,0,1-.26-1L9.3,15.36A1.93,1.93,0,0,1,11,14.9Z"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M8.29,9.86a.52.52,0,0,0-1-.26L5,11.91a1.94,1.94,0,0,0-.46,1.68l.81,3.16a.52.52,0,0,0,1,.26L8.64,14.7A1.93,1.93,0,0,0,9.1,13Z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default () => (
|
||||
<Icon
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="#1E8CBE"
|
||||
d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default () => (
|
||||
<Icon
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="#6C7781"
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default ( { className } ) => (
|
||||
<Icon
|
||||
className={ className }
|
||||
icon={
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 23">
|
||||
<path d="M21.9943 7.0957H2.00638C0.807102 7.0957 -0.0923543 8.0951 0.0075853 9.29437L0.707162 20.3877C0.807102 21.487 1.70656 22.2865 2.70595 22.2865H21.1948C22.2941 22.2865 23.1936 21.487 23.1936 20.3877L23.9931 9.29437C24.093 8.0951 23.0936 7.0957 21.9943 7.0957Z" />
|
||||
<path d="M13.0921 2.39855V1.99879C13.0921 0.899456 12.1926 0 11.0933 0H4.7971C3.69777 0 2.79831 0.899456 2.79831 1.99879V5.39674H21.1872V4.49728C21.1872 3.39795 20.2877 2.49849 19.1884 2.49849H13.0921V2.39855Z" />
|
||||
<path
|
||||
fill="white"
|
||||
d="M8.63115 17.126L6.5415 15.9449L4.361 17.126L4.72442 14.6729L2.99819 13.0376L5.45125 12.6742L6.5415 10.4937L7.5409 12.6742L9.99396 13.0376L8.26773 14.6729L8.63115 17.126Z"
|
||||
/>
|
||||
<path
|
||||
fill="white"
|
||||
d="M19.1907 13.5918H12.9899C12.547 13.5918 12.1926 13.1921 12.1926 12.6924C12.1926 12.1927 12.547 11.7929 12.9899 11.7929H19.1907C19.6336 11.7929 19.9879 12.1927 19.9879 12.6924C19.9879 13.1921 19.6336 13.5918 19.1907 13.5918Z"
|
||||
/>
|
||||
<path
|
||||
fill="white"
|
||||
d="M19.1907 17.1897H12.9899C12.547 17.1897 12.1926 16.7899 12.1926 16.2902C12.1926 15.7905 12.547 15.3907 12.9899 15.3907H19.1907C19.6336 15.3907 19.9879 15.7905 19.9879 16.2902C19.9879 16.7899 19.6336 17.1897 19.1907 17.1897Z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default ( { className } ) => (
|
||||
<Icon
|
||||
className={ className }
|
||||
icon={
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.3,17.3h9.3c0.1,0,0.3,0,0.4,0.1l5.9,4.2c0.3,0.2,0.7,0,0.7-0.3v-3.7c0-0.2,0.2-0.4,0.4-0.4H22 c1.1,0,2-0.9,2-2V2.5c0-1.2-0.7-2.2-2-2.2H2.3C0.7,0.2,0,0.9,0,2.5v12.3C0,16.3,0.7,17.3,2.3,17.3z" />
|
||||
<polygon
|
||||
fill="#ffffff"
|
||||
points="8.8,12.1 6.5,10.9 4.1,12.1 4.5,9.5 2.6,7.6 5.3,7.2 6.5,4.8 7.6,7.2 10.3,7.6 8.4,9.5"
|
||||
/>
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M20.7,7.9h-7c-0.5,0-0.9-0.4-0.9-0.9S13.2,6,13.7,6h7c0.5,0,0.9,0.4,0.9,0.9S21.2,7.9,20.7,7.9z"
|
||||
/>
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M20.7,11.5h-7c-0.5,0-0.9-0.4-0.9-0.9s0.4-0.9,0.9-0.9h7c0.5,0,0.9,0.4,0.9,0.9S21.2,11.5,20.7,11.5z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
|
||||
export default () => (
|
||||
<Icon
|
||||
className="material-icon"
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M13 13v8h8v-8h-8zM3 21h8v-8H3v8zM3 3v8h8V3H3zm13.66-1.31L11 7.34 16.66 13l5.66-5.66-5.66-5.65z" />
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Icon } from '@wordpress/components';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const IconWoo = ( { size = 20, className } ) => (
|
||||
<Icon
|
||||
className={ classNames( 'woo-icon', className ) }
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={ size }
|
||||
width={ Math.floor( size * 1.67 ) }
|
||||
viewBox="0 0 245 145"
|
||||
>
|
||||
<path
|
||||
fill="#96588A"
|
||||
d="M22.76 0h199.1a22.77 22.77 0 0 1 22.8 22.76v75.92a22.77 22.77 0 0 1-22.78 22.78h-71.41l9.77 24-43.13-24H22.76C10.1946 121.449.011 111.2654 0 98.7V22.76C.011 10.1946 10.1946.011 22.76 0z"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
fillRule="nonzero"
|
||||
d="M12.32 20.63a8.16 8.16 0 0 1 6.25-3.09c5.0733-.36 7.9867 2.0333 8.74 7.18 3.14 20.86 6.4933 38.5633 10.06 53.11l21.92-41.69c2-3.76 4.49-5.76 7.52-6 4.3867-.3067 7.13 2.49 8.23 8.39a186 186 0 0 0 9.47 34.23c2.62-25.38 7.0133-43.7133 13.18-55a7.73 7.73 0 0 1 6.6-4.43 8.8 8.8 0 0 1 6.32 2 7.75 7.75 0 0 1 3.05 5.74 8.52 8.52 0 0 1-1 4.77c-3.9 7.24-7.12 19.28-9.66 36.12-2.48 16.2533-3.3967 28.9633-2.75 38.13a12.25 12.25 0 0 1-1.16 6.56 6.32 6.32 0 0 1-5.33 3.53c-2.6667.18-5.3033-1.0567-7.91-3.71-9.2933-9.54-16.6567-23.7267-22.09-42.56C57.3 76.7167 52.48 86.3333 49.3 92.76c-5.86 11.3333-10.89 17.13-15.09 17.39-2.7333.1933-5.0667-2.0933-7-6.86-5.1-13.0933-10.5833-38.4033-16.45-75.93a8.56 8.56 0 0 1 1.56-6.73zM227.23 36.34a23.17 23.17 0 0 0-16-11.59 26.25 26.25 0 0 0-5.42-.58c-9.5867 0-17.41 5-23.47 15a53 53 0 0 0-7.78 28.16c0 7.7333 1.6167 14.3333 4.85 19.8a23.15 23.15 0 0 0 16 11.59 26.28 26.28 0 0 0 5.42.59c9.68 0 17.5033-5 23.47-15A53.89 53.89 0 0 0 232.08 56c0-7.7333-1.6167-14.2867-4.85-19.66zm-12.61 27.72c-1.3733 6.56-3.9167 11.5-7.63 14.82-2.9267 2.6667-5.6133 3.7433-8.06 3.23-2.4467-.5133-4.4033-2.6667-5.87-6.46a25.68 25.68 0 0 1-1.76-9.09 35.9 35.9 0 0 1 .73-7.34 33 33 0 0 1 5.28-11.88c3.3333-4.8933 6.8067-6.9467 10.42-6.16 2.44.4867 4.3933 2.64 5.86 6.46a25.68 25.68 0 0 1 1.76 9.12 35.73 35.73 0 0 1-.73 7.33v-.03zM164.57 36.34a23.2 23.2 0 0 0-16-11.59 26.42 26.42 0 0 0-5.43-.58c-9.5933 0-17.4167 5-23.47 15a53 53 0 0 0-7.78 28.16c0 7.7333 1.6133 14.3333 4.84 19.8a23.19 23.19 0 0 0 16 11.59 26.44 26.44 0 0 0 5.43.59c9.68 0 17.5033-5 23.47-15A53.88 53.88 0 0 0 169.4 56c0-7.7333-1.6133-14.2867-4.84-19.66h.01zm-12.61 27.72c-1.3667 6.56-3.91 11.5-7.63 14.82-2.93 2.64-5.63 3.72-8.07 3.23-2.44-.49-4.4-2.64-5.86-6.46a25.7 25.7 0 0 1-1.77-9.09 35.9 35.9 0 0 1 .74-7.34 33 33 0 0 1 5.28-11.88c3.3333-4.8933 6.8067-6.9467 10.42-6.16 2.44.4867 4.3933 2.64 5.86 6.46a25.91 25.91 0 0 1 1.76 9.12 35.73 35.73 0 0 1-.73 7.33v-.03z"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
IconWoo.propTypes = {
|
||||
size: PropTypes.number,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default IconWoo;
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import { Fragment } from '@wordpress/element';
|
||||
import { find } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SearchListControl, SearchListItem } from '@woocommerce/components';
|
||||
import { SelectControl, Spinner } from '@wordpress/components';
|
||||
import { withAttributes } from '@woocommerce/block-hocs';
|
||||
import ErrorMessage from '@woocommerce/block-components/error-placeholder/error-message.js';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
const ProductAttributeTermControl = ( {
|
||||
attributes,
|
||||
error,
|
||||
expandedAttribute,
|
||||
onChange,
|
||||
onExpandAttribute,
|
||||
onOperatorChange,
|
||||
isLoading,
|
||||
operator,
|
||||
selected,
|
||||
termsAreLoading,
|
||||
termsList,
|
||||
} ) => {
|
||||
const onSelectAttribute = ( item ) => {
|
||||
return () => {
|
||||
onChange( [] );
|
||||
onExpandAttribute( item.id );
|
||||
};
|
||||
};
|
||||
|
||||
const renderItem = ( args ) => {
|
||||
const { item, search, depth = 0 } = args;
|
||||
const classes = [
|
||||
'woocommerce-product-attributes__item',
|
||||
'woocommerce-search-list__item',
|
||||
];
|
||||
if ( search.length ) {
|
||||
classes.push( 'is-searching' );
|
||||
}
|
||||
if ( depth === 0 && item.parent ) {
|
||||
classes.push( 'is-skip-level' );
|
||||
}
|
||||
|
||||
if ( ! item.breadcrumbs.length ) {
|
||||
return [
|
||||
<SearchListItem
|
||||
key={ `attr-${ item.id }` }
|
||||
{ ...args }
|
||||
className={ classes.join( ' ' ) }
|
||||
isSelected={ expandedAttribute === item.id }
|
||||
onSelect={ onSelectAttribute }
|
||||
isSingle
|
||||
disabled={ item.count === '0' }
|
||||
aria-expanded={ expandedAttribute === item.id }
|
||||
aria-label={ sprintf(
|
||||
_n(
|
||||
'%s, has %d term',
|
||||
'%s, has %d terms',
|
||||
item.count,
|
||||
'woocommerce'
|
||||
),
|
||||
item.name,
|
||||
item.count
|
||||
) }
|
||||
/>,
|
||||
expandedAttribute === item.id && termsAreLoading && (
|
||||
<div
|
||||
key="loading"
|
||||
className={
|
||||
'woocommerce-search-list__item woocommerce-product-attributes__item' +
|
||||
'depth-1 is-loading is-not-active'
|
||||
}
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchListItem
|
||||
className={ classes.join( ' ' ) }
|
||||
{ ...args }
|
||||
showCount
|
||||
aria-label={ `${ item.breadcrumbs[ 0 ] }: ${ item.name }` }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const currentTerms = termsList[ expandedAttribute ] || [];
|
||||
const currentList = [ ...attributes, ...currentTerms ];
|
||||
|
||||
const messages = {
|
||||
clear: __(
|
||||
'Clear all product attributes',
|
||||
'woocommerce'
|
||||
),
|
||||
list: __( 'Product Attributes', 'woocommerce' ),
|
||||
noItems: __(
|
||||
"Your store doesn't have any product attributes.",
|
||||
'woocommerce'
|
||||
),
|
||||
search: __(
|
||||
'Search for product attributes',
|
||||
'woocommerce'
|
||||
),
|
||||
selected: ( n ) =>
|
||||
sprintf(
|
||||
_n(
|
||||
'%d attribute selected',
|
||||
'%d attributes selected',
|
||||
n,
|
||||
'woocommerce'
|
||||
),
|
||||
n
|
||||
),
|
||||
updated: __(
|
||||
'Product attribute search results updated.',
|
||||
'woocommerce'
|
||||
),
|
||||
};
|
||||
|
||||
if ( error ) {
|
||||
return <ErrorMessage error={ error } />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SearchListControl
|
||||
className="woocommerce-product-attributes"
|
||||
list={ currentList }
|
||||
isLoading={ isLoading }
|
||||
selected={ selected
|
||||
.map( ( { id } ) => find( currentList, { id } ) )
|
||||
.filter( Boolean ) }
|
||||
onChange={ onChange }
|
||||
renderItem={ renderItem }
|
||||
messages={ messages }
|
||||
isHierarchical
|
||||
/>
|
||||
{ !! onOperatorChange && (
|
||||
<div
|
||||
className={
|
||||
selected.length < 2 ? 'screen-reader-text' : ''
|
||||
}
|
||||
>
|
||||
<SelectControl
|
||||
className="woocommerce-product-attributes__operator"
|
||||
label={ __(
|
||||
'Display products matching',
|
||||
'woocommerce'
|
||||
) }
|
||||
help={ __(
|
||||
'Pick at least two attributes to use this setting.',
|
||||
'woocommerce'
|
||||
) }
|
||||
value={ operator }
|
||||
onChange={ onOperatorChange }
|
||||
options={ [
|
||||
{
|
||||
label: __(
|
||||
'Any selected attributes',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'any',
|
||||
},
|
||||
{
|
||||
label: __(
|
||||
'All selected attributes',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'all',
|
||||
},
|
||||
] }
|
||||
/>
|
||||
</div>
|
||||
) }
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
ProductAttributeTermControl.propTypes = {
|
||||
/**
|
||||
* Callback to update the selected product attributes.
|
||||
*/
|
||||
onChange: PropTypes.func.isRequired,
|
||||
/**
|
||||
* Callback to update the category operator. If not passed in, setting is not used.
|
||||
*/
|
||||
onOperatorChange: PropTypes.func,
|
||||
/**
|
||||
* Setting for whether products should match all or any selected categories.
|
||||
*/
|
||||
operator: PropTypes.oneOf( [ 'all', 'any' ] ),
|
||||
/**
|
||||
* The list of currently selected attribute slug/ID pairs.
|
||||
*/
|
||||
selected: PropTypes.array.isRequired,
|
||||
// from withAttributes
|
||||
attributes: PropTypes.array,
|
||||
error: PropTypes.object,
|
||||
expandedAttribute: PropTypes.number,
|
||||
onExpandAttribute: PropTypes.func,
|
||||
isLoading: PropTypes.bool,
|
||||
termsAreLoading: PropTypes.bool,
|
||||
termsList: PropTypes.object,
|
||||
};
|
||||
|
||||
ProductAttributeTermControl.defaultProps = {
|
||||
operator: 'any',
|
||||
};
|
||||
|
||||
export default withAttributes( ProductAttributeTermControl );
|
||||
@@ -0,0 +1,60 @@
|
||||
.woocommerce-product-attributes__operator {
|
||||
.components-base-control__help {
|
||||
@include visually-hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.components-panel {
|
||||
.woocommerce-product-attributes__operator.components-base-control {
|
||||
margin-top: $gap;
|
||||
|
||||
.components-select-control__input {
|
||||
margin-left: 0;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-search-list__item.woocommerce-product-attributes__item {
|
||||
&.is-searching,
|
||||
&.is-skip-level {
|
||||
.woocommerce-search-list__item-prefix::after {
|
||||
content: ":";
|
||||
}
|
||||
}
|
||||
|
||||
&.is-not-active {
|
||||
@include hover-state {
|
||||
background: $white;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
justify-content: center;
|
||||
|
||||
.components-spinner {
|
||||
margin-bottom: $gap-small;
|
||||
}
|
||||
}
|
||||
|
||||
&.depth-0::after {
|
||||
margin-left: $gap-smaller;
|
||||
content: "";
|
||||
height: $gap-large;
|
||||
width: $gap-large;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z" fill="{$core-grey-dark-300}" /></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center right;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
&.depth-0[aria-expanded="true"]::after {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z" fill="{$core-grey-dark-300}" /></svg>');
|
||||
}
|
||||
|
||||
&[disabled].depth-0::after {
|
||||
margin-left: 0;
|
||||
width: auto;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import { Fragment } from '@wordpress/element';
|
||||
import { find } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SearchListControl, SearchListItem } from '@woocommerce/components';
|
||||
import { SelectControl } from '@wordpress/components';
|
||||
import { withCategories } from '@woocommerce/block-hocs';
|
||||
import ErrorMessage from '@woocommerce/block-components/error-placeholder/error-message.js';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
const ProductCategoryControl = ( {
|
||||
categories,
|
||||
error,
|
||||
isLoading,
|
||||
onChange,
|
||||
onOperatorChange,
|
||||
operator,
|
||||
selected,
|
||||
isSingle,
|
||||
showReviewCount,
|
||||
} ) => {
|
||||
const renderItem = ( args ) => {
|
||||
const { item, search, depth = 0 } = args;
|
||||
const classes = [ 'woocommerce-product-categories__item' ];
|
||||
if ( search.length ) {
|
||||
classes.push( 'is-searching' );
|
||||
}
|
||||
if ( depth === 0 && item.parent !== 0 ) {
|
||||
classes.push( 'is-skip-level' );
|
||||
}
|
||||
|
||||
const accessibleName = ! item.breadcrumbs.length
|
||||
? item.name
|
||||
: `${ item.breadcrumbs.join( ', ' ) }, ${ item.name }`;
|
||||
|
||||
const listItemAriaLabel = showReviewCount
|
||||
? sprintf(
|
||||
_n(
|
||||
'%s, has %d review',
|
||||
'%s, has %d reviews',
|
||||
item.review_count,
|
||||
'woocommerce'
|
||||
),
|
||||
accessibleName,
|
||||
item.review_count
|
||||
)
|
||||
: sprintf(
|
||||
_n(
|
||||
'%s, has %d product',
|
||||
'%s, has %d products',
|
||||
item.count,
|
||||
'woocommerce'
|
||||
),
|
||||
accessibleName,
|
||||
item.count
|
||||
);
|
||||
|
||||
const listItemCountLabel = showReviewCount
|
||||
? sprintf(
|
||||
_n(
|
||||
'%d Review',
|
||||
'%d Reviews',
|
||||
item.review_count,
|
||||
'woocommerce'
|
||||
),
|
||||
item.review_count
|
||||
)
|
||||
: sprintf(
|
||||
_n(
|
||||
'%d Product',
|
||||
'%d Products',
|
||||
item.count,
|
||||
'woocommerce'
|
||||
),
|
||||
item.count
|
||||
);
|
||||
return (
|
||||
<SearchListItem
|
||||
className={ classes.join( ' ' ) }
|
||||
{ ...args }
|
||||
showCount
|
||||
countLabel={ listItemCountLabel }
|
||||
aria-label={ listItemAriaLabel }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const messages = {
|
||||
clear: __(
|
||||
'Clear all product categories',
|
||||
'woocommerce'
|
||||
),
|
||||
list: __( 'Product Categories', 'woocommerce' ),
|
||||
noItems: __(
|
||||
"Your store doesn't have any product categories.",
|
||||
'woocommerce'
|
||||
),
|
||||
search: __(
|
||||
'Search for product categories',
|
||||
'woocommerce'
|
||||
),
|
||||
selected: ( n ) =>
|
||||
sprintf(
|
||||
_n(
|
||||
'%d category selected',
|
||||
'%d categories selected',
|
||||
n,
|
||||
'woocommerce'
|
||||
),
|
||||
n
|
||||
),
|
||||
updated: __(
|
||||
'Category search results updated.',
|
||||
'woocommerce'
|
||||
),
|
||||
};
|
||||
|
||||
if ( error ) {
|
||||
return <ErrorMessage error={ error } />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SearchListControl
|
||||
className="woocommerce-product-categories"
|
||||
list={ categories }
|
||||
isLoading={ isLoading }
|
||||
selected={ selected
|
||||
.map( ( id ) => find( categories, { id } ) )
|
||||
.filter( Boolean ) }
|
||||
onChange={ onChange }
|
||||
renderItem={ renderItem }
|
||||
messages={ messages }
|
||||
isHierarchical
|
||||
isSingle={ isSingle }
|
||||
/>
|
||||
{ !! onOperatorChange && (
|
||||
<div
|
||||
className={
|
||||
selected.length < 2 ? 'screen-reader-text' : ''
|
||||
}
|
||||
>
|
||||
<SelectControl
|
||||
className="woocommerce-product-categories__operator"
|
||||
label={ __(
|
||||
'Display products matching',
|
||||
'woocommerce'
|
||||
) }
|
||||
help={ __(
|
||||
'Pick at least two categories to use this setting.',
|
||||
'woocommerce'
|
||||
) }
|
||||
value={ operator }
|
||||
onChange={ onOperatorChange }
|
||||
options={ [
|
||||
{
|
||||
label: __(
|
||||
'Any selected categories',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'any',
|
||||
},
|
||||
{
|
||||
label: __(
|
||||
'All selected categories',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'all',
|
||||
},
|
||||
] }
|
||||
/>
|
||||
</div>
|
||||
) }
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
ProductCategoryControl.propTypes = {
|
||||
/**
|
||||
* Callback to update the selected product categories.
|
||||
*/
|
||||
onChange: PropTypes.func.isRequired,
|
||||
/**
|
||||
* Callback to update the category operator. If not passed in, setting is not used.
|
||||
*/
|
||||
onOperatorChange: PropTypes.func,
|
||||
/**
|
||||
* Setting for whether products should match all or any selected categories.
|
||||
*/
|
||||
operator: PropTypes.oneOf( [ 'all', 'any' ] ),
|
||||
/**
|
||||
* The list of currently selected category IDs.
|
||||
*/
|
||||
selected: PropTypes.array.isRequired,
|
||||
/**
|
||||
* Allow only a single selection. Defaults to false.
|
||||
*/
|
||||
isSingle: PropTypes.bool,
|
||||
};
|
||||
|
||||
ProductCategoryControl.defaultProps = {
|
||||
operator: 'any',
|
||||
isSingle: false,
|
||||
};
|
||||
|
||||
export default withCategories( ProductCategoryControl );
|
||||
@@ -0,0 +1,16 @@
|
||||
.woocommerce-product-categories__operator {
|
||||
.components-base-control__help {
|
||||
@include visually-hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.components-panel {
|
||||
.woocommerce-product-categories__operator.components-base-control {
|
||||
margin-top: $gap;
|
||||
|
||||
.components-select-control__input {
|
||||
margin-left: 0;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import { escapeRegExp, isEmpty } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SearchListControl, SearchListItem } from '@woocommerce/components';
|
||||
import { Spinner, MenuItem } from '@wordpress/components';
|
||||
import classnames from 'classnames';
|
||||
import {
|
||||
withProductVariations,
|
||||
withSearchedProducts,
|
||||
withTransformSingleSelectToMultipleSelect,
|
||||
} from '@woocommerce/block-hocs';
|
||||
import {
|
||||
IconRadioSelected,
|
||||
IconRadioUnselected,
|
||||
} from '@woocommerce/block-components/icons';
|
||||
import ErrorMessage from '@woocommerce/block-components/error-placeholder/error-message.js';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
function getHighlightedName( name, search ) {
|
||||
if ( ! search ) {
|
||||
return name;
|
||||
}
|
||||
const re = new RegExp( escapeRegExp( search ), 'ig' );
|
||||
return name.replace( re, '<strong>$&</strong>' );
|
||||
}
|
||||
|
||||
const getInteractionIcon = ( isSelected = false ) => {
|
||||
return isSelected ? <IconRadioSelected /> : <IconRadioUnselected />;
|
||||
};
|
||||
|
||||
const messages = {
|
||||
list: __( 'Products', 'woocommerce' ),
|
||||
noItems: __(
|
||||
"Your store doesn't have any products.",
|
||||
'woocommerce'
|
||||
),
|
||||
search: __(
|
||||
'Search for a product to display',
|
||||
'woocommerce'
|
||||
),
|
||||
updated: __(
|
||||
'Product search results updated.',
|
||||
'woocommerce'
|
||||
),
|
||||
};
|
||||
|
||||
const ProductControl = ( {
|
||||
expandedProduct,
|
||||
error,
|
||||
isLoading,
|
||||
onChange,
|
||||
onSearch,
|
||||
products,
|
||||
renderItem,
|
||||
selected,
|
||||
showVariations,
|
||||
variations,
|
||||
variationsLoading,
|
||||
} ) => {
|
||||
const renderItemWithVariations = ( args ) => {
|
||||
const { item, search, depth = 0, isSelected, onSelect } = args;
|
||||
const variationsCount =
|
||||
item.variations && Array.isArray( item.variations )
|
||||
? item.variations.length
|
||||
: 0;
|
||||
const classes = classnames(
|
||||
'woocommerce-search-product__item',
|
||||
'woocommerce-search-list__item',
|
||||
`depth-${ depth }`,
|
||||
{
|
||||
'is-searching': search.length > 0,
|
||||
'is-skip-level': depth === 0 && item.parent !== 0,
|
||||
'is-variable': variationsCount > 0,
|
||||
}
|
||||
);
|
||||
|
||||
const itemArgs = Object.assign( {}, args );
|
||||
delete itemArgs.isSingle;
|
||||
|
||||
const a11yProps = {
|
||||
role: 'menuitemradio',
|
||||
};
|
||||
|
||||
if ( item.breadcrumbs.length ) {
|
||||
a11yProps[ 'aria-label' ] = `${ item.breadcrumbs[ 0 ] }: ${
|
||||
item.name
|
||||
}`;
|
||||
}
|
||||
|
||||
if ( variationsCount ) {
|
||||
a11yProps[ 'aria-expanded' ] = item.id === expandedProduct;
|
||||
}
|
||||
|
||||
// Top level items custom rendering based on SearchListItem.
|
||||
if ( ! item.breadcrumbs.length ) {
|
||||
return [
|
||||
<MenuItem
|
||||
key={ `product-${ item.id }` }
|
||||
isSelected={ isSelected }
|
||||
{ ...itemArgs }
|
||||
{ ...a11yProps }
|
||||
className={ classes }
|
||||
onClick={ () => {
|
||||
onSelect( item )();
|
||||
} }
|
||||
>
|
||||
<span className="woocommerce-search-list__item-state">
|
||||
{ getInteractionIcon( isSelected ) }
|
||||
</span>
|
||||
|
||||
<span className="woocommerce-search-list__item-label">
|
||||
<span
|
||||
className="woocommerce-search-list__item-name"
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: getHighlightedName( item.name, search ),
|
||||
} }
|
||||
/>
|
||||
</span>
|
||||
|
||||
{ variationsCount ? (
|
||||
<span className="woocommerce-search-list__item-variation-count">
|
||||
{ sprintf(
|
||||
_n(
|
||||
'%d variation',
|
||||
'%d variations',
|
||||
variationsCount,
|
||||
'woocommerce'
|
||||
),
|
||||
variationsCount
|
||||
) }
|
||||
</span>
|
||||
) : null }
|
||||
</MenuItem>,
|
||||
expandedProduct === item.id &&
|
||||
variationsCount > 0 &&
|
||||
variationsLoading && (
|
||||
<div
|
||||
key="loading"
|
||||
className={
|
||||
'woocommerce-search-list__item woocommerce-search-product__item' +
|
||||
'depth-1 is-loading is-not-active'
|
||||
}
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if ( ! isEmpty( item.variation ) ) {
|
||||
item.name = item.variation;
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchListItem
|
||||
className={ classes }
|
||||
{ ...args }
|
||||
{ ...a11yProps }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getRenderItemFunc = () => {
|
||||
if ( renderItem ) {
|
||||
return renderItem;
|
||||
} else if ( showVariations ) {
|
||||
return renderItemWithVariations;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
if ( error ) {
|
||||
return <ErrorMessage error={ error } />;
|
||||
}
|
||||
|
||||
const currentVariations =
|
||||
variations && variations[ expandedProduct ]
|
||||
? variations[ expandedProduct ]
|
||||
: [];
|
||||
const currentList = [ ...products, ...currentVariations ];
|
||||
|
||||
return (
|
||||
<SearchListControl
|
||||
className="woocommerce-products"
|
||||
list={ currentList }
|
||||
isLoading={ isLoading }
|
||||
isSingle
|
||||
selected={ currentList.filter( ( { id } ) =>
|
||||
selected.includes( id )
|
||||
) }
|
||||
onChange={ onChange }
|
||||
renderItem={ getRenderItemFunc() }
|
||||
onSearch={ onSearch }
|
||||
messages={ messages }
|
||||
isHierarchical
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ProductControl.propTypes = {
|
||||
/**
|
||||
* Callback to update the selected products.
|
||||
*/
|
||||
onChange: PropTypes.func.isRequired,
|
||||
/**
|
||||
* The ID of the currently expanded product.
|
||||
*/
|
||||
expandedProduct: PropTypes.number,
|
||||
/**
|
||||
* Callback to search products by their name.
|
||||
*/
|
||||
onSearch: PropTypes.func,
|
||||
/**
|
||||
* Query args to pass to getProducts.
|
||||
*/
|
||||
queryArgs: PropTypes.object,
|
||||
/**
|
||||
* Callback to render each item in the selection list, allows any custom object-type rendering.
|
||||
*/
|
||||
renderItem: PropTypes.func,
|
||||
/**
|
||||
* The ID of the currently selected item (product or variation).
|
||||
*/
|
||||
selected: PropTypes.arrayOf( PropTypes.number ),
|
||||
/**
|
||||
* Whether to show variations in the list of items available.
|
||||
*/
|
||||
showVariations: PropTypes.bool,
|
||||
};
|
||||
|
||||
ProductControl.defaultProps = {
|
||||
expandedProduct: null,
|
||||
selected: [],
|
||||
showVariations: false,
|
||||
};
|
||||
|
||||
export default withTransformSingleSelectToMultipleSelect(
|
||||
withSearchedProducts( withProductVariations( ProductControl ) )
|
||||
);
|
||||
@@ -0,0 +1,43 @@
|
||||
.woocommerce-search-product__item {
|
||||
.woocommerce-search-list__item-name {
|
||||
.description {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-searching,
|
||||
&.is-skip-level {
|
||||
.woocommerce-search-list__item-prefix::after {
|
||||
content: ":";
|
||||
}
|
||||
}
|
||||
|
||||
&.is-not-active {
|
||||
@include hover-state {
|
||||
background: $white;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
justify-content: center;
|
||||
|
||||
.components-spinner {
|
||||
margin-bottom: $gap-small;
|
||||
}
|
||||
}
|
||||
|
||||
&.depth-0.is-variable::after {
|
||||
margin-left: $gap-smaller;
|
||||
content: "";
|
||||
height: $gap-large;
|
||||
width: $gap-large;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z" fill="{$core-grey-dark-300}" /></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center right;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
&.depth-0.is-variable[aria-expanded="true"]::after {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z" fill="{$core-grey-dark-300}" /></svg>');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { SelectControl } from '@wordpress/components';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* A pre-configured SelectControl for product orderby settings.
|
||||
*/
|
||||
const ProductOrderbyControl = ( { value, setAttributes } ) => {
|
||||
return (
|
||||
<SelectControl
|
||||
label={ __( 'Order products by', 'woocommerce' ) }
|
||||
value={ value }
|
||||
options={ [
|
||||
{
|
||||
label: __(
|
||||
'Newness - newest first',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'date',
|
||||
},
|
||||
{
|
||||
label: __(
|
||||
'Price - low to high',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'price_asc',
|
||||
},
|
||||
{
|
||||
label: __(
|
||||
'Price - high to low',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'price_desc',
|
||||
},
|
||||
{
|
||||
label: __(
|
||||
'Rating - highest first',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'rating',
|
||||
},
|
||||
{
|
||||
label: __(
|
||||
'Sales - most first',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'popularity',
|
||||
},
|
||||
{
|
||||
label: __(
|
||||
'Title - alphabetical',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'title',
|
||||
},
|
||||
{
|
||||
label: __( 'Menu Order', 'woocommerce' ),
|
||||
value: 'menu_order',
|
||||
},
|
||||
] }
|
||||
onChange={ ( orderby ) => setAttributes( { orderby } ) }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ProductOrderbyControl.propTypes = {
|
||||
/**
|
||||
* Callback to update the order setting.
|
||||
*/
|
||||
setAttributes: PropTypes.func.isRequired,
|
||||
/**
|
||||
* The selected order setting.
|
||||
*/
|
||||
value: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ProductOrderbyControl;
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
PLACEHOLDER_IMG_SRC,
|
||||
THUMBNAIL_SIZE,
|
||||
} from '@woocommerce/block-settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
/**
|
||||
* Display a preview for a given product.
|
||||
*/
|
||||
const ProductPreview = ( { product } ) => {
|
||||
let image = null;
|
||||
if ( product.images.length ) {
|
||||
image = (
|
||||
<img
|
||||
className="wc-product-preview__image"
|
||||
src={ product.images[ 0 ].src }
|
||||
alt=""
|
||||
style={ { width: `${ THUMBNAIL_SIZE }px` } }
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
image = (
|
||||
<img
|
||||
className="wc-product-preview__image"
|
||||
src={ PLACEHOLDER_IMG_SRC }
|
||||
alt=""
|
||||
style={ { width: `${ THUMBNAIL_SIZE }px` } }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const rating = Number( product.average_rating );
|
||||
let displayRating = false;
|
||||
if ( rating > 0 ) {
|
||||
displayRating = ( rating / 5 ) * 100;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="wc-product-preview wc-block-grid__product">
|
||||
<div className="wc-product-preview__image wc-block-grid__product-image">
|
||||
{ image }
|
||||
</div>
|
||||
<div
|
||||
className="wc-product-preview__title wc-block-grid__product-title"
|
||||
dangerouslySetInnerHTML={ { __html: product.name } }
|
||||
/>
|
||||
<div
|
||||
className="wc-product-preview__price wc-block-grid__product-price"
|
||||
dangerouslySetInnerHTML={ { __html: product.price_html } }
|
||||
/>
|
||||
|
||||
{ displayRating && (
|
||||
<div
|
||||
className="wc-product-preview__rating star-rating wc-block-grid__product-rating"
|
||||
role="img"
|
||||
>
|
||||
<span style={ { width: `${ displayRating }%` } } />
|
||||
</div>
|
||||
) }
|
||||
|
||||
<span className="wp-block-button">
|
||||
<span className="wc-product-preview__add-to-cart wc-block-grid__product-add-to-cart wp-block-button__link">
|
||||
{ __( 'Add to cart', 'woocommerce' ) }
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProductPreview.propTypes = {
|
||||
/**
|
||||
* The product object as returned from the API.
|
||||
*/
|
||||
product: PropTypes.shape( {
|
||||
id: PropTypes.number,
|
||||
average_rating: PropTypes.oneOf( [
|
||||
'PropTypes.number',
|
||||
'PropTypes.string',
|
||||
] ),
|
||||
images: PropTypes.array,
|
||||
name: PropTypes.string,
|
||||
price_html: PropTypes.string,
|
||||
} ).isRequired,
|
||||
};
|
||||
|
||||
export default ProductPreview;
|
||||
@@ -0,0 +1,54 @@
|
||||
.wc-product-preview {
|
||||
|
||||
.wc-product-preview__image {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.wp-block-button {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.wc-product-preview__add-to-cart {
|
||||
cursor: text;
|
||||
margin: $gap-small 0 0;
|
||||
}
|
||||
|
||||
|
||||
.is-hidden-title & {
|
||||
.wc-product-preview__title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.is-hidden-price & {
|
||||
.wc-product-preview__price {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.is-hidden-rating & {
|
||||
.wc-product-preview__rating {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.is-hidden-button & {
|
||||
.wp-block-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-block-preview & {
|
||||
.wc-product-preview__title {
|
||||
font-size: 0.7em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.wc-product-preview__price {
|
||||
font-size: 0.6em;
|
||||
}
|
||||
.wc-product-preview__add-to-cart {
|
||||
font-size: 0.6em !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ProductPreview should render a single product preview with an image 1`] = `
|
||||
<div
|
||||
className="wc-product-preview wc-block-grid__product"
|
||||
>
|
||||
<div
|
||||
className="wc-product-preview__image wc-block-grid__product-image"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
className="wc-product-preview__image"
|
||||
src="https://example.local/product.jpg"
|
||||
style={
|
||||
Object {
|
||||
"width": "300px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="wc-product-preview__title wc-block-grid__product-title"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Winter Jacket",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="wc-product-preview__price wc-block-grid__product-price"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<span class=\\"woocommerce-Price-amount amount\\"><span class=\\"woocommerce-Price-currencySymbol\\">$</span>65.00</span>",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<span
|
||||
className="wp-block-button"
|
||||
>
|
||||
<span
|
||||
className="wc-product-preview__add-to-cart wc-block-grid__product-add-to-cart wp-block-button__link"
|
||||
>
|
||||
Add to cart
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ProductPreview should render a single product preview without an image 1`] = `
|
||||
<div
|
||||
className="wc-product-preview wc-block-grid__product"
|
||||
>
|
||||
<div
|
||||
className="wc-product-preview__image wc-block-grid__product-image"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
className="wc-product-preview__image"
|
||||
src="placeholder.png"
|
||||
style={
|
||||
Object {
|
||||
"width": "300px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="wc-product-preview__title wc-block-grid__product-title"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "Winter Jacket",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="wc-product-preview__price wc-block-grid__product-price"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<span class=\\"woocommerce-Price-amount amount\\"><span class=\\"woocommerce-Price-currencySymbol\\">$</span>65.00</span>",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<span
|
||||
className="wp-block-button"
|
||||
>
|
||||
<span
|
||||
className="wc-product-preview__add-to-cart wc-block-grid__product-add-to-cart wp-block-button__link"
|
||||
>
|
||||
Add to cart
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import ProductPreview from '../';
|
||||
|
||||
jest.mock( '@woocommerce/block-settings', () => ( {
|
||||
PLACEHOLDER_IMG_SRC: 'placeholder.png',
|
||||
THUMBNAIL_SIZE: 300,
|
||||
} ) );
|
||||
|
||||
describe( 'ProductPreview', () => {
|
||||
test( 'should render a single product preview with an image', () => {
|
||||
const product = {
|
||||
id: 1,
|
||||
name: 'Winter Jacket',
|
||||
price_html:
|
||||
'<span class="woocommerce-Price-amount amount"><span class="woocommerce-Price-currencySymbol">$</span>65.00</span>',
|
||||
images: [
|
||||
{
|
||||
src: 'https://example.local/product.jpg',
|
||||
},
|
||||
],
|
||||
};
|
||||
const component = TestRenderer.create(
|
||||
<ProductPreview product={ product } />
|
||||
);
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render a single product preview without an image', () => {
|
||||
const product = {
|
||||
id: 1,
|
||||
name: 'Winter Jacket',
|
||||
price_html:
|
||||
'<span class="woocommerce-Price-amount amount"><span class="woocommerce-Price-currencySymbol">$</span>65.00</span>',
|
||||
images: [],
|
||||
};
|
||||
const component = TestRenderer.create(
|
||||
<ProductPreview product={ product } />
|
||||
);
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { debounce, find } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SearchListControl, SearchListItem } from '@woocommerce/components';
|
||||
import { SelectControl } from '@wordpress/components';
|
||||
import { LIMIT_TAGS } from '@woocommerce/block-settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getProductTags } from '../utils';
|
||||
import './style.scss';
|
||||
|
||||
/**
|
||||
* Component to handle searching and selecting product tags.
|
||||
*/
|
||||
class ProductTagControl extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
this.state = {
|
||||
list: [],
|
||||
loading: true,
|
||||
};
|
||||
this.renderItem = this.renderItem.bind( this );
|
||||
this.debouncedOnSearch = debounce( this.onSearch.bind( this ), 400 );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { selected } = this.props;
|
||||
|
||||
getProductTags( { selected } )
|
||||
.then( ( list ) => {
|
||||
this.setState( { list, loading: false } );
|
||||
} )
|
||||
.catch( () => {
|
||||
this.setState( { list: [], loading: false } );
|
||||
} );
|
||||
}
|
||||
|
||||
onSearch( search ) {
|
||||
const { selected } = this.props;
|
||||
this.setState( { loading: true } );
|
||||
|
||||
getProductTags( { selected, search } )
|
||||
.then( ( list ) => {
|
||||
this.setState( { list, loading: false } );
|
||||
} )
|
||||
.catch( () => {
|
||||
this.setState( { list: [], loading: false } );
|
||||
} );
|
||||
}
|
||||
|
||||
renderItem( args ) {
|
||||
const { item, search, depth = 0 } = args;
|
||||
const classes = [ 'woocommerce-product-tags__item' ];
|
||||
if ( search.length ) {
|
||||
classes.push( 'is-searching' );
|
||||
}
|
||||
if ( depth === 0 && item.parent !== 0 ) {
|
||||
classes.push( 'is-skip-level' );
|
||||
}
|
||||
|
||||
const accessibleName = ! item.breadcrumbs.length
|
||||
? item.name
|
||||
: `${ item.breadcrumbs.join( ', ' ) }, ${ item.name }`;
|
||||
|
||||
return (
|
||||
<SearchListItem
|
||||
className={ classes.join( ' ' ) }
|
||||
{ ...args }
|
||||
showCount
|
||||
aria-label={ sprintf(
|
||||
_n(
|
||||
'%d product tagged as %s',
|
||||
'%d products tagged as %s',
|
||||
item.count,
|
||||
'woocommerce'
|
||||
),
|
||||
item.count,
|
||||
accessibleName
|
||||
) }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { list, loading } = this.state;
|
||||
const { onChange, onOperatorChange, operator, selected } = this.props;
|
||||
|
||||
const messages = {
|
||||
clear: __(
|
||||
'Clear all product tags',
|
||||
'woocommerce'
|
||||
),
|
||||
list: __( 'Product Tags', 'woocommerce' ),
|
||||
noItems: __(
|
||||
"Your store doesn't have any product tags.",
|
||||
'woocommerce'
|
||||
),
|
||||
search: __(
|
||||
'Search for product tags',
|
||||
'woocommerce'
|
||||
),
|
||||
selected: ( n ) =>
|
||||
sprintf(
|
||||
_n(
|
||||
'%d tag selected',
|
||||
'%d tags selected',
|
||||
n,
|
||||
'woocommerce'
|
||||
),
|
||||
n
|
||||
),
|
||||
updated: __(
|
||||
'Tag search results updated.',
|
||||
'woocommerce'
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SearchListControl
|
||||
className="woocommerce-product-tags"
|
||||
list={ list }
|
||||
isLoading={ loading }
|
||||
selected={ selected
|
||||
.map( ( id ) => find( list, { id } ) )
|
||||
.filter( Boolean ) }
|
||||
onChange={ onChange }
|
||||
onSearch={ LIMIT_TAGS ? this.debouncedOnSearch : null }
|
||||
renderItem={ this.renderItem }
|
||||
messages={ messages }
|
||||
isHierarchical
|
||||
/>
|
||||
{ !! onOperatorChange && (
|
||||
<div
|
||||
className={
|
||||
selected.length < 2 ? 'screen-reader-text' : ''
|
||||
}
|
||||
>
|
||||
<SelectControl
|
||||
className="woocommerce-product-tags__operator"
|
||||
label={ __(
|
||||
'Display products matching',
|
||||
'woocommerce'
|
||||
) }
|
||||
help={ __(
|
||||
'Pick at least two tags to use this setting.',
|
||||
'woocommerce'
|
||||
) }
|
||||
value={ operator }
|
||||
onChange={ onOperatorChange }
|
||||
options={ [
|
||||
{
|
||||
label: __(
|
||||
'Any selected tags',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'any',
|
||||
},
|
||||
{
|
||||
label: __(
|
||||
'All selected tags',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'all',
|
||||
},
|
||||
] }
|
||||
/>
|
||||
</div>
|
||||
) }
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ProductTagControl.propTypes = {
|
||||
/**
|
||||
* Callback to update the selected product categories.
|
||||
*/
|
||||
onChange: PropTypes.func.isRequired,
|
||||
/**
|
||||
* Callback to update the category operator. If not passed in, setting is not used.
|
||||
*/
|
||||
onOperatorChange: PropTypes.func,
|
||||
/**
|
||||
* Setting for whether products should match all or any selected categories.
|
||||
*/
|
||||
operator: PropTypes.oneOf( [ 'all', 'any' ] ),
|
||||
/**
|
||||
* The list of currently selected tags.
|
||||
*/
|
||||
selected: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
ProductTagControl.defaultProps = {
|
||||
operator: 'any',
|
||||
};
|
||||
|
||||
export default ProductTagControl;
|
||||
@@ -0,0 +1,16 @@
|
||||
.woocommerce-product-tags__operator {
|
||||
.components-base-control__help {
|
||||
@include visually-hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.components-panel {
|
||||
.woocommerce-product-tags__operator.components-base-control {
|
||||
margin-top: $gap;
|
||||
|
||||
.components-select-control__input {
|
||||
margin-left: 0;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import { SearchListControl } from '@woocommerce/components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withSearchedProducts } from '@woocommerce/block-hocs';
|
||||
import ErrorMessage from '@woocommerce/block-components/error-placeholder/error-message.js';
|
||||
|
||||
/**
|
||||
* The products control exposes a custom selector for searching and selecting
|
||||
* products.
|
||||
*
|
||||
* @param {Object} props Component props.
|
||||
* @param {Function} props.onChange Callback fired when the selected item changes
|
||||
* @param {Function} props.onSearch Callback fired when a search is triggered
|
||||
* @param {Array} props.selected An array of selected products.
|
||||
* @param {Array} props.products An array of products to select from.
|
||||
* @param {boolean} props.isLoading Whether or not the products are being loaded.
|
||||
*
|
||||
* @return {Function} A functional component.
|
||||
*/
|
||||
const ProductsControl = ( {
|
||||
error,
|
||||
onChange,
|
||||
onSearch,
|
||||
selected,
|
||||
products,
|
||||
isLoading,
|
||||
} ) => {
|
||||
const messages = {
|
||||
clear: __( 'Clear all products', 'woocommerce' ),
|
||||
list: __( 'Products', 'woocommerce' ),
|
||||
noItems: __(
|
||||
"Your store doesn't have any products.",
|
||||
'woocommerce'
|
||||
),
|
||||
search: __(
|
||||
'Search for products to display',
|
||||
'woocommerce'
|
||||
),
|
||||
selected: ( n ) =>
|
||||
sprintf(
|
||||
_n(
|
||||
'%d product selected',
|
||||
'%d products selected',
|
||||
n,
|
||||
'woocommerce'
|
||||
),
|
||||
n
|
||||
),
|
||||
updated: __(
|
||||
'Product search results updated.',
|
||||
'woocommerce'
|
||||
),
|
||||
};
|
||||
|
||||
if ( error ) {
|
||||
return <ErrorMessage error={ error } />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchListControl
|
||||
className="woocommerce-products"
|
||||
list={ products }
|
||||
isLoading={ isLoading }
|
||||
selected={ products.filter( ( { id } ) =>
|
||||
selected.includes( id )
|
||||
) }
|
||||
onSearch={ onSearch }
|
||||
onChange={ onChange }
|
||||
messages={ messages }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ProductsControl.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSearch: PropTypes.func,
|
||||
selected: PropTypes.array,
|
||||
products: PropTypes.array,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
ProductsControl.defaultProps = {
|
||||
selected: [],
|
||||
products: [],
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
export default withSearchedProducts( ProductsControl );
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isFunction } from 'lodash';
|
||||
import classnames from 'classnames';
|
||||
import { BaseControl, ButtonGroup, Button } from '@wordpress/components';
|
||||
import { Component } from '@wordpress/element';
|
||||
import { withInstanceId } from '@wordpress/compose';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
class ToggleButtonControl extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
|
||||
this.onClick = this.onClick.bind( this );
|
||||
}
|
||||
|
||||
onClick( event ) {
|
||||
if ( this.props.onChange ) {
|
||||
this.props.onChange( event.target.value );
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
label,
|
||||
checked,
|
||||
instanceId,
|
||||
className,
|
||||
help,
|
||||
options,
|
||||
value,
|
||||
} = this.props;
|
||||
const id = `inspector-toggle-button-control-${ instanceId }`;
|
||||
|
||||
let helpLabel;
|
||||
|
||||
if ( help ) {
|
||||
helpLabel = isFunction( help ) ? help( checked ) : help;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseControl
|
||||
id={ id }
|
||||
help={ helpLabel }
|
||||
className={ classnames(
|
||||
'components-toggle-button-control',
|
||||
className
|
||||
) }
|
||||
>
|
||||
<label
|
||||
id={ id + '__label' }
|
||||
htmlFor={ id }
|
||||
className="components-toggle-button-control__label"
|
||||
>
|
||||
{ label }
|
||||
</label>
|
||||
<ButtonGroup aria-labelledby={ id + '__label' }>
|
||||
{ options.map( ( option, index ) => {
|
||||
const buttonArgs = {};
|
||||
|
||||
// Change button depending on pressed state.
|
||||
if ( value === option.value ) {
|
||||
buttonArgs.isPrimary = true;
|
||||
buttonArgs[ 'aria-pressed' ] = true;
|
||||
} else {
|
||||
buttonArgs.isDefault = true;
|
||||
buttonArgs[ 'aria-pressed' ] = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={ `${ option.label }-${
|
||||
option.value
|
||||
}-${ index }` }
|
||||
value={ option.value }
|
||||
onClick={ this.onClick }
|
||||
aria-label={ label + ': ' + option.label }
|
||||
{ ...buttonArgs }
|
||||
>
|
||||
{ option.label }
|
||||
</Button>
|
||||
);
|
||||
} ) }
|
||||
</ButtonGroup>
|
||||
</BaseControl>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withInstanceId( ToggleButtonControl );
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,13 @@
|
||||
.components-toggle-button-control {
|
||||
.components-base-control__field {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.components-toggle-button-control__label {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
.components-base-control__help {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { addQueryArgs } from '@wordpress/url';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { flatten, uniqBy } from 'lodash';
|
||||
import {
|
||||
ENDPOINTS,
|
||||
IS_LARGE_CATALOG,
|
||||
LIMIT_TAGS,
|
||||
} from '@woocommerce/block-settings';
|
||||
|
||||
const getProductsRequests = ( {
|
||||
selected = [],
|
||||
search = '',
|
||||
queryArgs = [],
|
||||
} ) => {
|
||||
const defaultArgs = {
|
||||
per_page: IS_LARGE_CATALOG ? 100 : -1,
|
||||
catalog_visibility: 'any',
|
||||
status: 'publish',
|
||||
search,
|
||||
orderby: 'title',
|
||||
order: 'asc',
|
||||
};
|
||||
const requests = [
|
||||
addQueryArgs( ENDPOINTS.products, { ...defaultArgs, ...queryArgs } ),
|
||||
];
|
||||
|
||||
// If we have a large catalog, we might not get all selected products in the first page.
|
||||
if ( IS_LARGE_CATALOG && selected.length ) {
|
||||
requests.push(
|
||||
addQueryArgs( ENDPOINTS.products, {
|
||||
catalog_visibility: 'any',
|
||||
status: 'publish',
|
||||
include: selected,
|
||||
} )
|
||||
);
|
||||
}
|
||||
|
||||
return requests;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a promise that resolves to a list of products from the API.
|
||||
*
|
||||
* @param {Object} - A query object with the list of selected products and search term.
|
||||
*/
|
||||
export const getProducts = ( {
|
||||
selected = [],
|
||||
search = '',
|
||||
queryArgs = [],
|
||||
} ) => {
|
||||
const requests = getProductsRequests( { selected, search, queryArgs } );
|
||||
|
||||
return Promise.all( requests.map( ( path ) => apiFetch( { path } ) ) )
|
||||
.then( ( data ) => {
|
||||
const products = uniqBy( flatten( data ), 'id' );
|
||||
const list = products.map( ( product ) => ( {
|
||||
...product,
|
||||
parent: 0,
|
||||
} ) );
|
||||
return list;
|
||||
} )
|
||||
.catch( ( e ) => {
|
||||
throw e;
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a promise that resolves to a product object from the API.
|
||||
*
|
||||
* @param {number} productId Id of the product to retrieve.
|
||||
*/
|
||||
export const getProduct = ( productId ) => {
|
||||
return apiFetch( {
|
||||
path: `${ ENDPOINTS.products }/${ productId }`,
|
||||
} );
|
||||
};
|
||||
|
||||
const getProductTagsRequests = ( { selected = [], search } ) => {
|
||||
const requests = [
|
||||
addQueryArgs( `${ ENDPOINTS.products }/tags`, {
|
||||
per_page: LIMIT_TAGS ? 100 : -1,
|
||||
orderby: LIMIT_TAGS ? 'count' : 'name',
|
||||
order: LIMIT_TAGS ? 'desc' : 'asc',
|
||||
search,
|
||||
} ),
|
||||
];
|
||||
|
||||
// If we have a large catalog, we might not get all selected products in the first page.
|
||||
if ( LIMIT_TAGS && selected.length ) {
|
||||
requests.push(
|
||||
addQueryArgs( `${ ENDPOINTS.products }/tags`, {
|
||||
include: selected,
|
||||
} )
|
||||
);
|
||||
}
|
||||
|
||||
return requests;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a promise that resolves to a list of tags from the API.
|
||||
*
|
||||
* @param {Object} - A query object with the list of selected products and search term.
|
||||
*/
|
||||
export const getProductTags = ( { selected = [], search } ) => {
|
||||
const requests = getProductTagsRequests( { selected, search } );
|
||||
|
||||
return Promise.all( requests.map( ( path ) => apiFetch( { path } ) ) ).then(
|
||||
( data ) => {
|
||||
return uniqBy( flatten( data ), 'id' );
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a promise that resolves to a category object from the API.
|
||||
*
|
||||
* @param {number} categoryId Id of the product to retrieve.
|
||||
*/
|
||||
export const getCategory = ( categoryId ) => {
|
||||
return apiFetch( {
|
||||
path: `${ ENDPOINTS.categories }/${ categoryId }`,
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a promise that resolves to an array of category objects from the API.
|
||||
*/
|
||||
export const getCategories = ( queryArgs ) => {
|
||||
return apiFetch( {
|
||||
path: addQueryArgs( `${ ENDPOINTS.products }/categories`, {
|
||||
per_page: -1,
|
||||
...queryArgs,
|
||||
} ),
|
||||
} );
|
||||
};
|
||||
|
||||
export const getProductVariations = ( product ) => {
|
||||
return apiFetch( {
|
||||
path: addQueryArgs( `${ ENDPOINTS.products }/${ product }/variations`, {
|
||||
per_page: -1,
|
||||
} ),
|
||||
} );
|
||||
};
|
||||
|
||||
export const getAttributes = () => {
|
||||
return apiFetch( {
|
||||
path: addQueryArgs( `${ ENDPOINTS.products }/attributes`, {
|
||||
per_page: -1,
|
||||
} ),
|
||||
} );
|
||||
};
|
||||
|
||||
export const getTerms = ( attribute ) => {
|
||||
return apiFetch( {
|
||||
path: addQueryArgs(
|
||||
`${ ENDPOINTS.products }/attributes/${ attribute }/terms`,
|
||||
{
|
||||
per_page: -1,
|
||||
}
|
||||
),
|
||||
} );
|
||||
};
|
||||
Reference in New Issue
Block a user