Create a reusable search + list control for category selection (https://github.com/woocommerce/woocommerce-blocks/pull/166)
* Add new components for ProductCategoryControl, using a reusable SearchListControl * Add sass variables from wc-admin * Finish styling * Save selected categories as selected for the block * Style sidebar version of control * Filter the categories list, highlight search term in result * Filter out selected items in the filter list function * Add spacing in the placeholder, remove unnecessary stylesheet * Add a more descriptive label for screen readers * Remove category references from list item options * Switch to a configurable object of messages, so SearchListControl can be more customizable * Add screen-reader message for toggling “done” and moving into preview * Remove call to getProductCategoryControl The component is simple enough to just drop in now :) * Add documentation for all props * Add padding to placeholder * Rename fallbackrenderItem to defaultRenderItem * Add a variable to save the selected count * Add `isDestructive` to make link red * Update item style * Add a hover/focus background color
This commit is contained in:
parent
fbdc17d532
commit
4ac52b3cf3
|
@ -34,3 +34,16 @@
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide an element from sighted users, but availble to screen reader users.
|
||||||
|
@mixin visually-hidden() {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(1px, 1px, 1px, 1px);
|
||||||
|
clip-path: inset(50%);
|
||||||
|
height: 1px;
|
||||||
|
width: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
/* Many screen reader and browser combinations announce broken words as they would appear visually. */
|
||||||
|
word-wrap: normal !important;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
$gap-largest: 40px;
|
||||||
|
$gap-larger: 36px;
|
||||||
|
$gap-large: 24px;
|
||||||
|
$gap: 16px;
|
||||||
|
$gap-small: 12px;
|
||||||
|
$gap-smaller: 8px;
|
||||||
|
$gap-smallest: 4px;
|
|
@ -4,15 +4,28 @@
|
||||||
|
|
||||||
.wc-block-products-category {
|
.wc-block-products-category {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.components-placeholder {
|
||||||
|
padding: 2em 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wc-block-products-category__selection {
|
.wc-block-products-category__selection {
|
||||||
margin-top: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-top: 1px solid $core-grey-light-500;
|
}
|
||||||
|
|
||||||
.components-spinner {
|
.components-panel {
|
||||||
float: none;
|
.woocommerce-search-list {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.woocommerce-search-list__selected {
|
||||||
|
margin: 0 0 $gap;
|
||||||
|
padding: 0;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
.woocommerce-search-list__search {
|
||||||
|
margin: 0 0 $gap;
|
||||||
|
padding: 0;
|
||||||
|
border-top: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||||
|
import { addQueryArgs } from '@wordpress/url';
|
||||||
|
import apiFetch from '@wordpress/api-fetch';
|
||||||
|
import { Component } from '@wordpress/element';
|
||||||
|
import { find } from 'lodash';
|
||||||
|
import { MenuItem } from '@wordpress/components';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './style.scss';
|
||||||
|
import SearchListControl from '../search-list-control';
|
||||||
|
|
||||||
|
class ProductCategoryControl extends Component {
|
||||||
|
constructor() {
|
||||||
|
super( ...arguments );
|
||||||
|
this.state = {
|
||||||
|
list: [],
|
||||||
|
};
|
||||||
|
this.renderItem = this.renderItem.bind( this );
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
apiFetch( {
|
||||||
|
path: addQueryArgs( '/wc/v3/products/categories', { per_page: -1 } ),
|
||||||
|
} )
|
||||||
|
.then( ( list ) => {
|
||||||
|
this.setState( { list } );
|
||||||
|
} )
|
||||||
|
.catch( () => {
|
||||||
|
this.setState( { list: [] } );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
renderItem( { getHighlightedName, item, onSelect, search } ) {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={ item.id }
|
||||||
|
className="woocommerce-product-categories__item woocommerce-search-list__item"
|
||||||
|
onClick={ onSelect( item ) }
|
||||||
|
aria-label={ sprintf(
|
||||||
|
_n(
|
||||||
|
'%s, has %d product',
|
||||||
|
'%s, has %d products',
|
||||||
|
item.count,
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
item.name,
|
||||||
|
item.count
|
||||||
|
) }
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="woocommerce-product-categories__item-name"
|
||||||
|
dangerouslySetInnerHTML={ {
|
||||||
|
__html: getHighlightedName( item.name, search ),
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
<span className="woocommerce-product-categories__item-count">
|
||||||
|
{ item.count }
|
||||||
|
</span>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { list } = this.state;
|
||||||
|
const { selected, onChange } = this.props;
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
clear: __( 'Clear all product categories', 'woocommerce' ),
|
||||||
|
list: __( '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' ),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SearchListControl
|
||||||
|
className="woocommerce-product-categories"
|
||||||
|
list={ list }
|
||||||
|
selected={ selected.map( ( id ) => find( list, { id } ) ).filter( Boolean ) }
|
||||||
|
onChange={ onChange }
|
||||||
|
renderItem={ this.renderItem }
|
||||||
|
messages={ messages }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProductCategoryControl.propTypes = {
|
||||||
|
/**
|
||||||
|
* Callback to update the selected product categories.
|
||||||
|
*/
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
/**
|
||||||
|
* The list of currently selected category IDs.
|
||||||
|
*/
|
||||||
|
selected: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductCategoryControl;
|
|
@ -0,0 +1,21 @@
|
||||||
|
.woocommerce-product-categories {
|
||||||
|
.woocommerce-product-categories__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-product-categories__item-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-product-categories__item-count {
|
||||||
|
flex: 0;
|
||||||
|
padding: $gap-smallest/2 $gap-smaller;
|
||||||
|
border: 1px solid $core-grey-light-500;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: $core-grey-dark-300;
|
||||||
|
background: $white;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,240 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
MenuItem,
|
||||||
|
MenuGroup,
|
||||||
|
TextControl,
|
||||||
|
withSpokenMessages,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
import { Component } from '@wordpress/element';
|
||||||
|
import { compose, withInstanceId, withState } from '@wordpress/compose';
|
||||||
|
import { escapeRegExp, findIndex } from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Tag } from '@woocommerce/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
|
const defaultMessages = {
|
||||||
|
clear: __( 'Clear all selected items', 'woocommerce' ),
|
||||||
|
list: __( 'Results', 'woocommerce' ),
|
||||||
|
noResults: __( 'No results for %s', 'woocommerce' ),
|
||||||
|
search: __( 'Search for items', 'woocommerce' ),
|
||||||
|
selected: ( n ) =>
|
||||||
|
sprintf( _n( '%d item selected', '%d items selected', n, 'woocommerce' ), n ),
|
||||||
|
updated: __( 'Search results updated.', 'woocommerce' ),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to display a searchable, selectable list of items.
|
||||||
|
*/
|
||||||
|
export class SearchListControl extends Component {
|
||||||
|
constructor() {
|
||||||
|
super( ...arguments );
|
||||||
|
|
||||||
|
this.onSelect = this.onSelect.bind( this );
|
||||||
|
this.onRemove = this.onRemove.bind( this );
|
||||||
|
this.onClear = this.onClear.bind( this );
|
||||||
|
this.defaultRenderItem = this.defaultRenderItem.bind( this );
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove( id ) {
|
||||||
|
const { selected, onChange } = this.props;
|
||||||
|
return () => {
|
||||||
|
const i = findIndex( selected, { id } );
|
||||||
|
onChange( [ ...selected.slice( 0, i ), ...selected.slice( i + 1 ) ] );
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect( item ) {
|
||||||
|
const { selected, onChange } = this.props;
|
||||||
|
return () => {
|
||||||
|
onChange( [ ...selected, item ] );
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onClear() {
|
||||||
|
this.props.onChange( [] );
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelected( item ) {
|
||||||
|
return -1 !== findIndex( this.props.selected, { id: item.id } );
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilteredList( list, search ) {
|
||||||
|
if ( ! search ) {
|
||||||
|
return list.filter( ( item ) => item && ! this.isSelected( item ) );
|
||||||
|
}
|
||||||
|
const messages = { ...defaultMessages, ...this.props.messages };
|
||||||
|
const re = new RegExp( escapeRegExp( search ), 'i' );
|
||||||
|
this.props.debouncedSpeak( messages.updated );
|
||||||
|
return list
|
||||||
|
.map( ( item ) => ( re.test( item.name ) ? item : false ) )
|
||||||
|
.filter( ( item ) => item && ! this.isSelected( item ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
getHighlightedName( name, search ) {
|
||||||
|
if ( ! search ) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
const re = new RegExp( escapeRegExp( search ), 'ig' );
|
||||||
|
return name.replace( re, '<strong>$&</strong>' );
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultRenderItem( { getHighlightedName, item, onSelect, search } ) {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={ item.id }
|
||||||
|
className="woocommerce-search-list__item"
|
||||||
|
onClick={ onSelect( item ) }
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="woocommerce-search-list__item-name"
|
||||||
|
dangerouslySetInnerHTML={ {
|
||||||
|
__html: getHighlightedName( item.name, search ),
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { className, search, selected, setState } = this.props;
|
||||||
|
const messages = { ...defaultMessages, ...this.props.messages };
|
||||||
|
const list = this.getFilteredList( this.props.list, search );
|
||||||
|
const noResults = search ? sprintf( messages.noResults, search ) : null;
|
||||||
|
const renderItem = this.props.renderItem || this.defaultRenderItem;
|
||||||
|
const selectedCount = selected.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={ `woocommerce-search-list ${ className }` }>
|
||||||
|
{ selectedCount > 0 ? (
|
||||||
|
<div className="woocommerce-search-list__selected">
|
||||||
|
<div className="woocommerce-search-list__selected-header">
|
||||||
|
<strong>{ messages.selected( selectedCount ) }</strong>
|
||||||
|
<Button isLink isDestructive onClick={ this.onClear } aria-label={ messages.clear }>
|
||||||
|
{ __( 'Clear all', 'woocommerce' ) }
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{ selected.map( ( item, i ) => (
|
||||||
|
<Tag
|
||||||
|
key={ i }
|
||||||
|
label={ item.name }
|
||||||
|
id={ item.id }
|
||||||
|
remove={ this.onRemove }
|
||||||
|
/>
|
||||||
|
) ) }
|
||||||
|
</div>
|
||||||
|
) : null }
|
||||||
|
|
||||||
|
<div className="woocommerce-search-list__search">
|
||||||
|
<TextControl
|
||||||
|
label={ messages.search }
|
||||||
|
type="search"
|
||||||
|
value={ search }
|
||||||
|
onChange={ ( value ) => setState( { search: value } ) }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ ! list.length ? (
|
||||||
|
noResults
|
||||||
|
) : (
|
||||||
|
<MenuGroup
|
||||||
|
label={ messages.list }
|
||||||
|
className="woocommerce-search-list__list"
|
||||||
|
>
|
||||||
|
{ list.map( ( item ) =>
|
||||||
|
renderItem( {
|
||||||
|
getHighlightedName: this.getHighlightedName,
|
||||||
|
item,
|
||||||
|
onSelect: this.onSelect,
|
||||||
|
search,
|
||||||
|
} )
|
||||||
|
) }
|
||||||
|
</MenuGroup>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchListControl.propTypes = {
|
||||||
|
/**
|
||||||
|
* Additional CSS classes.
|
||||||
|
*/
|
||||||
|
className: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* A complete list of item objects, each with id, name properties. This is displayed as a
|
||||||
|
* clickable/keyboard-able list, and possibly filtered by the search term (searches name).
|
||||||
|
*/
|
||||||
|
list: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape( {
|
||||||
|
id: PropTypes.number,
|
||||||
|
name: PropTypes.string,
|
||||||
|
} )
|
||||||
|
),
|
||||||
|
/**
|
||||||
|
* Messages displayed or read to the user. Configure these to reflect your object type.
|
||||||
|
* See `defaultMessages` above for examples.
|
||||||
|
*/
|
||||||
|
messages: PropTypes.shape( {
|
||||||
|
/**
|
||||||
|
* A more detailed label for the "Clear all" button, read to screen reader users.
|
||||||
|
*/
|
||||||
|
clear: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Label for the list of selectable items, only read to screen reader users.
|
||||||
|
*/
|
||||||
|
list: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Message to display when no matching results are found. %s is the search term.
|
||||||
|
*/
|
||||||
|
noResults: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Label for the search input
|
||||||
|
*/
|
||||||
|
search: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Label for the selected items. This is actually a function, so that we can pass
|
||||||
|
* through the count of currently selected items.
|
||||||
|
*/
|
||||||
|
selected: PropTypes.func,
|
||||||
|
/**
|
||||||
|
* Label indicating that search results have changed, read to screen reader users.
|
||||||
|
*/
|
||||||
|
updated: PropTypes.string,
|
||||||
|
} ),
|
||||||
|
/**
|
||||||
|
* Callback fired when selected items change, whether added, cleared, or removed.
|
||||||
|
* Passed an array of item objects (as passed in via props.list).
|
||||||
|
*/
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
/**
|
||||||
|
* Callback to render each item in the selection list, allows any custom object-type rendering.
|
||||||
|
*/
|
||||||
|
renderItem: PropTypes.func,
|
||||||
|
/**
|
||||||
|
* The list of currently selected items.
|
||||||
|
*/
|
||||||
|
selected: PropTypes.array.isRequired,
|
||||||
|
// from withState
|
||||||
|
search: PropTypes.string,
|
||||||
|
setState: PropTypes.func,
|
||||||
|
// from withSpokenMessages
|
||||||
|
debouncedSpeak: PropTypes.func,
|
||||||
|
// from withInstanceId
|
||||||
|
instanceId: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default compose( [
|
||||||
|
withState( {
|
||||||
|
search: '',
|
||||||
|
} ),
|
||||||
|
withSpokenMessages,
|
||||||
|
withInstanceId,
|
||||||
|
] )( SearchListControl );
|
|
@ -0,0 +1,67 @@
|
||||||
|
.woocommerce-search-list {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 0 $gap;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-search-list__selected {
|
||||||
|
margin: $gap 0;
|
||||||
|
padding: $gap 0 0;
|
||||||
|
border-top: 1px solid $core-grey-light-500;
|
||||||
|
|
||||||
|
.woocommerce-search-list__selected-header {
|
||||||
|
margin-bottom: $gap-smaller;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-left: $gap-small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-tag__text {
|
||||||
|
max-width: 13em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-search-list__search {
|
||||||
|
margin: $gap 0;
|
||||||
|
padding: $gap 0 0;
|
||||||
|
border-top: 1px solid $core-grey-light-500;
|
||||||
|
|
||||||
|
.components-base-control__field {
|
||||||
|
margin-bottom: $gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-search-list__list {
|
||||||
|
padding: 0;
|
||||||
|
max-height: 20em;
|
||||||
|
overflow-x: scroll;
|
||||||
|
border-top: 1px solid $core-grey-light-500;
|
||||||
|
border-bottom: 1px solid $core-grey-light-500;
|
||||||
|
|
||||||
|
.components-menu-group__label {
|
||||||
|
@include visually-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > [role="menu"] {
|
||||||
|
border: 1px solid $core-grey-light-500;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-search-list__item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: $gap;
|
||||||
|
background: $white;
|
||||||
|
// !important to keep the border around on hover
|
||||||
|
border-bottom: 1px solid $core-grey-light-500 !important;
|
||||||
|
color: $core-grey-dark-500;
|
||||||
|
|
||||||
|
@include hover-state {
|
||||||
|
background: $core-grey-light-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,8 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import apiFetch from '@wordpress/api-fetch';
|
|
||||||
import { addQueryArgs } from '@wordpress/url';
|
import { addQueryArgs } from '@wordpress/url';
|
||||||
|
import apiFetch from '@wordpress/api-fetch';
|
||||||
import { Component, Fragment, RawHTML } from '@wordpress/element';
|
import { Component, Fragment, RawHTML } from '@wordpress/element';
|
||||||
import {
|
import {
|
||||||
BlockAlignmentToolbar,
|
BlockAlignmentToolbar,
|
||||||
|
@ -18,7 +18,7 @@ import {
|
||||||
SelectControl,
|
SelectControl,
|
||||||
Spinner,
|
Spinner,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
TreeSelect,
|
withSpokenMessages,
|
||||||
} from '@wordpress/components';
|
} from '@wordpress/components';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { registerBlockType } from '@wordpress/blocks';
|
import { registerBlockType } from '@wordpress/blocks';
|
||||||
|
@ -29,6 +29,7 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||||
import '../css/product-category-block.scss';
|
import '../css/product-category-block.scss';
|
||||||
import getQuery from './utils/get-query';
|
import getQuery from './utils/get-query';
|
||||||
import getShortcode from './utils/get-shortcode';
|
import getShortcode from './utils/get-shortcode';
|
||||||
|
import ProductCategoryControl from './components/product-category-control';
|
||||||
import ProductPreview from './components/product-preview';
|
import ProductPreview from './components/product-preview';
|
||||||
import sharedAttributes from './utils/shared-attributes';
|
import sharedAttributes from './utils/shared-attributes';
|
||||||
|
|
||||||
|
@ -38,26 +39,16 @@ const validAlignments = [ 'center', 'wide', 'full' ];
|
||||||
/**
|
/**
|
||||||
* Component to handle edit mode of "Products by Category".
|
* Component to handle edit mode of "Products by Category".
|
||||||
*/
|
*/
|
||||||
class ProductByCategoryBlock extends Component {
|
export default class ProductByCategoryBlock extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super( ...arguments );
|
super( ...arguments );
|
||||||
this.state = {
|
this.state = {
|
||||||
categoriesList: [],
|
|
||||||
products: [],
|
products: [],
|
||||||
loaded: false,
|
loaded: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
apiFetch( {
|
|
||||||
path: addQueryArgs( '/wc/v3/products/categories', { per_page: -1 } ),
|
|
||||||
} )
|
|
||||||
.then( ( categoriesList ) => {
|
|
||||||
this.setState( { categoriesList } );
|
|
||||||
} )
|
|
||||||
.catch( () => {
|
|
||||||
this.setState( { categoriesList: [] } );
|
|
||||||
} );
|
|
||||||
if ( this.props.attributes.categories ) {
|
if ( this.props.attributes.categories ) {
|
||||||
this.getProducts();
|
this.getProducts();
|
||||||
}
|
}
|
||||||
|
@ -90,19 +81,16 @@ class ProductByCategoryBlock extends Component {
|
||||||
|
|
||||||
getInspectorControls() {
|
getInspectorControls() {
|
||||||
const { attributes, setAttributes } = this.props;
|
const { attributes, setAttributes } = this.props;
|
||||||
const { columns, orderby, rows, categories } = attributes;
|
const { columns, orderby, rows } = attributes;
|
||||||
const { categoriesList } = this.state;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InspectorControls key="inspector">
|
<InspectorControls key="inspector">
|
||||||
<PanelBody title={ __( 'Product Category', 'woocommerce' ) } initialOpen>
|
<PanelBody title={ __( 'Product Category', 'woocommerce' ) } initialOpen>
|
||||||
<TreeSelect
|
<ProductCategoryControl
|
||||||
label={ __( 'Product Category', 'woocommerce' ) }
|
selected={ attributes.categories }
|
||||||
tree={ categoriesList }
|
onChange={ ( value = [] ) => {
|
||||||
selectedId={ categories }
|
const ids = value.map( ( { id } ) => id );
|
||||||
multiple
|
setAttributes( { categories: ids } );
|
||||||
onChange={ ( value ) => {
|
|
||||||
setAttributes( { categories: value ? value : [] } );
|
|
||||||
} }
|
} }
|
||||||
/>
|
/>
|
||||||
</PanelBody>
|
</PanelBody>
|
||||||
|
@ -164,42 +152,37 @@ class ProductByCategoryBlock extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEditMode() {
|
renderEditMode() {
|
||||||
const { setAttributes } = this.props;
|
const { attributes, debouncedSpeak, setAttributes } = this.props;
|
||||||
const { categories } = this.props.attributes;
|
const onDone = () => {
|
||||||
const { categoriesList } = this.state;
|
setAttributes( { editMode: false } );
|
||||||
|
debouncedSpeak( __( 'Showing product block preview.', 'woocommerce' ) );
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Placeholder
|
<Placeholder
|
||||||
icon="category"
|
icon="category"
|
||||||
label={ __( 'Products by Category', 'woocommerce' ) }
|
label={ __( 'Products by Category', 'woocommerce' ) }
|
||||||
|
className="wc-block-products-category"
|
||||||
>
|
>
|
||||||
{ __(
|
{ __(
|
||||||
'Display a grid of products from your selected categories',
|
'Display a grid of products from your selected categories',
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
) }
|
) }
|
||||||
{ categoriesList.length ? (
|
|
||||||
<div className="wc-block-products-category__selection">
|
<div className="wc-block-products-category__selection">
|
||||||
<TreeSelect
|
<ProductCategoryControl
|
||||||
label={ __( 'Product Category', 'woocommerce' ) }
|
selected={ attributes.categories }
|
||||||
tree={ categoriesList }
|
onChange={ ( value = [] ) => {
|
||||||
selectedId={ categories }
|
const ids = value.map( ( { id } ) => id );
|
||||||
multiple
|
setAttributes( { categories: ids } );
|
||||||
onChange={ ( value ) => {
|
|
||||||
setAttributes( { categories: value ? value : [] } );
|
|
||||||
} }
|
} }
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
isDefault
|
isDefault
|
||||||
onClick={ () => setAttributes( { editMode: false } ) }
|
onClick={ onDone }
|
||||||
>
|
>
|
||||||
{ __( 'Done', 'woocommerce' ) }
|
{ __( 'Done', 'woocommerce' ) }
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="wc-block-products-category__selection">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
) }
|
|
||||||
</Placeholder>
|
</Placeholder>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -265,9 +248,11 @@ ProductByCategoryBlock.propTypes = {
|
||||||
* A callback to update attributes
|
* A callback to update attributes
|
||||||
*/
|
*/
|
||||||
setAttributes: PropTypes.func.isRequired,
|
setAttributes: PropTypes.func.isRequired,
|
||||||
|
// from withSpokenMessages
|
||||||
|
debouncedSpeak: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProductByCategoryBlock;
|
const WrappedProductByCategoryBlock = withSpokenMessages( ProductByCategoryBlock );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register and run the "Products by Category" block.
|
* Register and run the "Products by Category" block.
|
||||||
|
@ -303,7 +288,7 @@ registerBlockType( 'woocommerce/product-category', {
|
||||||
* Renders and manages the block.
|
* Renders and manages the block.
|
||||||
*/
|
*/
|
||||||
edit( props ) {
|
edit( props ) {
|
||||||
return <ProductByCategoryBlock { ...props } />;
|
return <WrappedProductByCategoryBlock { ...props } />;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -58,6 +58,7 @@ const GutenbergBlocksConfig = {
|
||||||
includePaths: [ 'assets/css/abstracts' ],
|
includePaths: [ 'assets/css/abstracts' ],
|
||||||
data:
|
data:
|
||||||
'@import "_colors"; ' +
|
'@import "_colors"; ' +
|
||||||
|
'@import "_variables"; ' +
|
||||||
'@import "_breakpoints"; ' +
|
'@import "_breakpoints"; ' +
|
||||||
'@import "_mixins"; ',
|
'@import "_mixins"; ',
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue