/** * External dependencies */ import { __, _n, sprintf } from '@wordpress/i18n'; import { Component } 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 { getSetting } from '@woocommerce/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( /* translators: %1$d is the count of products, %2$s is the name of the tag. */ _n( '%1$d product tagged as %2$s', '%1$d products tagged as %2$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( /* translators: %d is the count of selected tags. */ _n( '%d tag selected', '%d tags selected', n, 'woocommerce' ), n ), updated: __( 'Tag search results updated.', 'woocommerce' ), }; const limitTags = getSetting( 'limitTags', false ); return ( <> <SearchListControl className="woocommerce-product-tags" list={ list } isLoading={ loading } selected={ selected .map( ( id ) => find( list, { id } ) ) .filter( Boolean ) } onChange={ onChange } onSearch={ limitTags ? 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> ) } </> ); } } 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;