diff --git a/plugins/woocommerce-blocks/assets/js/components/product-attribute-control/index.js b/plugins/woocommerce-blocks/assets/js/components/product-attribute-control/index.js new file mode 100644 index 00000000000..77d8000ded5 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/components/product-attribute-control/index.js @@ -0,0 +1,147 @@ +/** + * 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 PropTypes from 'prop-types'; + +/** + * Internal dependencies + */ +import './style.scss'; +import SearchListControl from '../search-list-control'; +import SearchListItem from '../search-list-control/item'; + +class ProductAttributeControl extends Component { + constructor() { + super( ...arguments ); + this.state = { + list: [], + loading: true, + }; + } + + componentDidMount() { + const getTermsInAttribute = ( { id } ) => { + return apiFetch( { + path: addQueryArgs( `/wc-pb/v3/products/attributes/${ id }/terms`, { + per_page: -1, + } ), + } ).then( ( terms ) => terms.map( ( t ) => ( { ...t, parent: id } ) ) ); + }; + + apiFetch( { + path: addQueryArgs( '/wc-pb/v3/products/attributes', { per_page: -1 } ), + } ) + .then( ( attributes ) => { + // Fetch the terms list for each attribute group, then flatten them into one list. + Promise.all( attributes.map( getTermsInAttribute ) ).then( ( results ) => { + const list = attributes.map( ( a ) => ( { ...a, parent: 0 } ) ); + results.forEach( ( terms ) => { + list.push( ...terms ); + } ); + this.setState( { list, loading: false } ); + } ); + } ) + .catch( () => { + this.setState( { list: [], loading: false } ); + } ); + } + + 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 !== 0 ) { + classes.push( 'is-skip-level' ); + } + + if ( ! item.breadcrumbs.length ) { + classes.push( 'is-not-active' ); + return ( +
+ + + { item.name } + + +
+ ); + } + + return ( + + ); + } + + render() { + const { list, loading } = this.state; + const { selected, onChange } = this.props; + + const messages = { + clear: __( 'Clear all product attributes', 'woo-gutenberg-products-block' ), + list: __( 'Product Attributes', 'woo-gutenberg-products-block' ), + noItems: __( + "Your store doesn't have any product attributes.", + 'woo-gutenberg-products-block' + ), + search: __( + 'Search for product attributes', + 'woo-gutenberg-products-block' + ), + selected: ( n ) => + sprintf( + _n( + '%d attribute selected', + '%d attributes selected', + n, + 'woo-gutenberg-products-block' + ), + n + ), + updated: __( + 'Product attribute search results updated.', + 'woo-gutenberg-products-block' + ), + }; + + return ( + find( list, { id } ) ).filter( Boolean ) } + onChange={ onChange } + renderItem={ this.renderItem } + messages={ messages } + isHierarchical + /> + ); + } +} + +ProductAttributeControl.propTypes = { + /** + * Callback to update the selected product attributes. + */ + onChange: PropTypes.func.isRequired, + /** + * The list of currently selected attribute slug/ID pairs. + */ + selected: PropTypes.array.isRequired, +}; + +export default ProductAttributeControl; diff --git a/plugins/woocommerce-blocks/assets/js/components/product-attribute-control/style.scss b/plugins/woocommerce-blocks/assets/js/components/product-attribute-control/style.scss new file mode 100644 index 00000000000..58306776cc9 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/components/product-attribute-control/style.scss @@ -0,0 +1,14 @@ +.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: transparent; + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/components/search-list-control/style.scss b/plugins/woocommerce-blocks/assets/js/components/search-list-control/style.scss index a022ad3e7f0..2f3039b6dbc 100644 --- a/plugins/woocommerce-blocks/assets/js/components/search-list-control/style.scss +++ b/plugins/woocommerce-blocks/assets/js/components/search-list-control/style.scss @@ -95,7 +95,7 @@ background: $core-grey-light-100; } - &:last-of-type { + &:last-child { border-bottom: none !important; } diff --git a/plugins/woocommerce-blocks/assets/js/product-category-block.js b/plugins/woocommerce-blocks/assets/js/product-category-block.js index cb22de7047f..9adc50e60ff 100644 --- a/plugins/woocommerce-blocks/assets/js/product-category-block.js +++ b/plugins/woocommerce-blocks/assets/js/product-category-block.js @@ -26,6 +26,7 @@ import PropTypes from 'prop-types'; * Internal dependencies */ import getQuery from './utils/get-query'; +import ProductAttributeControl from './components/product-attribute-control'; import ProductCategoryControl from './components/product-category-control'; import ProductOrderbyControl from './components/product-orderby-control'; import ProductPreview from './components/product-preview'; @@ -130,6 +131,18 @@ class ProductByCategoryBlock extends Component { value={ orderby } /> + + { + const selected = value.map( ( { id, attr_slug } ) => ( { id, attr_slug } ) ); // eslint-disable-line camelcase + setAttributes( { attributes: selected } ); + } } + /> + ); } diff --git a/plugins/woocommerce-blocks/assets/js/utils/shared-attributes.js b/plugins/woocommerce-blocks/assets/js/utils/shared-attributes.js index 3891403eed7..17c5261909e 100644 --- a/plugins/woocommerce-blocks/assets/js/utils/shared-attributes.js +++ b/plugins/woocommerce-blocks/assets/js/utils/shared-attributes.js @@ -37,4 +37,12 @@ export default { type: 'string', default: 'any', }, + + /** + * Product attributes, used to display only products with the given attributes. + */ + attributes: { + type: 'array', + default: [], + }, };