2019-01-10 19:01:49 +00:00
|
|
|
/**
|
|
|
|
* External dependencies
|
|
|
|
*/
|
2019-07-09 09:48:31 +00:00
|
|
|
import { __, _n, sprintf } from '@wordpress/i18n';
|
2019-01-10 19:01:49 +00:00
|
|
|
import { Component, Fragment } from '@wordpress/element';
|
2019-07-09 09:48:31 +00:00
|
|
|
import { addQueryArgs } from '@wordpress/url';
|
|
|
|
import apiFetch from '@wordpress/api-fetch';
|
|
|
|
import { debounce, find, escapeRegExp, isEmpty } from 'lodash';
|
2019-01-10 19:01:49 +00:00
|
|
|
import PropTypes from 'prop-types';
|
2019-07-09 09:48:31 +00:00
|
|
|
import {
|
|
|
|
SearchListControl,
|
|
|
|
SearchListItem,
|
|
|
|
} from '@woocommerce/components';
|
|
|
|
import { Spinner, MenuItem } from '@wordpress/components';
|
|
|
|
import classnames from 'classnames';
|
2019-08-27 15:25:32 +00:00
|
|
|
import { ENDPOINTS, IS_LARGE_CATALOG } from '@woocommerce/block-settings';
|
2019-01-10 19:01:49 +00:00
|
|
|
|
2019-05-03 14:38:13 +00:00
|
|
|
/**
|
|
|
|
* Internal dependencies
|
|
|
|
*/
|
2019-08-17 09:14:11 +00:00
|
|
|
import { getProducts } from '../utils';
|
2019-07-09 09:48:31 +00:00
|
|
|
import {
|
|
|
|
IconRadioSelected,
|
|
|
|
IconRadioUnselected,
|
|
|
|
} from '../icons';
|
|
|
|
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 />;
|
|
|
|
};
|
2019-05-03 14:38:13 +00:00
|
|
|
|
2019-01-10 19:01:49 +00:00
|
|
|
class ProductControl extends Component {
|
|
|
|
constructor() {
|
|
|
|
super( ...arguments );
|
|
|
|
this.state = {
|
2019-07-09 09:48:31 +00:00
|
|
|
products: [],
|
|
|
|
product: 0,
|
|
|
|
variationsList: {},
|
|
|
|
variationsLoading: false,
|
2019-01-10 19:01:49 +00:00
|
|
|
loading: true,
|
|
|
|
};
|
2019-05-03 14:38:13 +00:00
|
|
|
|
|
|
|
this.debouncedOnSearch = debounce( this.onSearch.bind( this ), 400 );
|
2019-07-09 09:48:31 +00:00
|
|
|
this.debouncedGetVariations = debounce( this.getVariations.bind( this ), 200 );
|
|
|
|
this.renderItem = this.renderItem.bind( this );
|
|
|
|
this.onProductSelect = this.onProductSelect.bind( this );
|
2019-01-10 19:01:49 +00:00
|
|
|
}
|
|
|
|
|
2019-08-15 14:55:57 +00:00
|
|
|
componentWillUnmount() {
|
|
|
|
this.debouncedOnSearch.cancel();
|
|
|
|
this.debouncedGetVariations.cancel();
|
|
|
|
}
|
|
|
|
|
2019-01-10 19:01:49 +00:00
|
|
|
componentDidMount() {
|
2019-08-15 14:55:57 +00:00
|
|
|
const { selected, queryArgs } = this.props;
|
2019-05-03 14:38:13 +00:00
|
|
|
|
2019-08-15 14:55:57 +00:00
|
|
|
getProducts( { selected, queryArgs } )
|
2019-07-09 09:48:31 +00:00
|
|
|
.then( ( products ) => {
|
|
|
|
products = products.map( ( product ) => {
|
|
|
|
const count = product.variations ? product.variations.length : 0;
|
|
|
|
return {
|
|
|
|
...product,
|
|
|
|
parent: 0,
|
2019-08-09 16:18:46 +00:00
|
|
|
count,
|
2019-07-09 09:48:31 +00:00
|
|
|
};
|
|
|
|
} );
|
|
|
|
this.setState( { products, loading: false } );
|
2019-05-03 14:38:13 +00:00
|
|
|
} )
|
|
|
|
.catch( () => {
|
2019-07-09 09:48:31 +00:00
|
|
|
this.setState( { products: [], loading: false } );
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidUpdate( prevProps, prevState ) {
|
|
|
|
if ( prevState.product !== this.state.product ) {
|
|
|
|
this.debouncedGetVariations();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getVariations() {
|
2019-08-15 14:55:57 +00:00
|
|
|
const { product, products, variationsList } = this.state;
|
2019-07-09 09:48:31 +00:00
|
|
|
|
|
|
|
if ( ! product ) {
|
|
|
|
this.setState( {
|
|
|
|
variationsList: {},
|
|
|
|
variationsLoading: false,
|
|
|
|
} );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-08-15 14:55:57 +00:00
|
|
|
const productDetails = products.find( ( findProduct ) => findProduct.id === product );
|
2019-07-09 09:48:31 +00:00
|
|
|
|
|
|
|
if ( ! productDetails.variations || productDetails.variations.length === 0 ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( ! variationsList[ product ] ) {
|
|
|
|
this.setState( { variationsLoading: true } );
|
|
|
|
}
|
|
|
|
|
|
|
|
apiFetch( {
|
2019-08-07 14:47:01 +00:00
|
|
|
path: addQueryArgs( `${ ENDPOINTS.products }/${ product }/variations`, {
|
2019-07-09 09:48:31 +00:00
|
|
|
per_page: -1,
|
|
|
|
} ),
|
|
|
|
} )
|
|
|
|
.then( ( variations ) => {
|
|
|
|
variations = variations.map( ( variation ) => ( { ...variation, parent: product } ) );
|
|
|
|
this.setState( ( prevState ) => ( {
|
|
|
|
variationsList: { ...prevState.variationsList, [ product ]: variations },
|
|
|
|
variationsLoading: false,
|
|
|
|
} ) );
|
|
|
|
} )
|
|
|
|
.catch( () => {
|
|
|
|
this.setState( { termsLoading: false } );
|
2019-05-03 14:38:13 +00:00
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
onSearch( search ) {
|
2019-08-15 14:55:57 +00:00
|
|
|
const { selected, queryArgs } = this.props;
|
|
|
|
getProducts( { selected, search, queryArgs } )
|
2019-07-09 09:48:31 +00:00
|
|
|
.then( ( products ) => {
|
|
|
|
this.setState( { products, loading: false } );
|
2019-01-10 19:01:49 +00:00
|
|
|
} )
|
|
|
|
.catch( () => {
|
2019-07-09 09:48:31 +00:00
|
|
|
this.setState( { products: [], loading: false } );
|
2019-01-10 19:01:49 +00:00
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2019-07-09 09:48:31 +00:00
|
|
|
onProductSelect( item, isSelected ) {
|
|
|
|
return () => {
|
|
|
|
this.setState( {
|
|
|
|
product: isSelected ? 0 : item.id,
|
|
|
|
} );
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
renderItem( args ) {
|
|
|
|
const { item, search, depth = 0, isSelected, onSelect } = args;
|
|
|
|
const { product, variationsLoading } = this.state;
|
|
|
|
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': item.count > 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 ( item.count ) {
|
|
|
|
a11yProps[ 'aria-expanded' ] = item.id === product;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 )();
|
|
|
|
this.onProductSelect( item, isSelected )();
|
|
|
|
} }
|
|
|
|
>
|
|
|
|
<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>
|
|
|
|
|
|
|
|
{ item.count ? (
|
|
|
|
<span
|
|
|
|
className="woocommerce-search-list__item-variation-count"
|
|
|
|
>
|
|
|
|
{ sprintf(
|
|
|
|
_n(
|
|
|
|
'%d variation',
|
|
|
|
'%d variations',
|
|
|
|
item.count,
|
|
|
|
'woo-gutenberg-products-block'
|
|
|
|
),
|
|
|
|
item.count
|
|
|
|
) }
|
|
|
|
</span>
|
|
|
|
) : null }
|
|
|
|
</MenuItem>,
|
|
|
|
product === item.id && item.count > 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 }
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-01-10 19:01:49 +00:00
|
|
|
render() {
|
2019-07-09 09:48:31 +00:00
|
|
|
const { products, loading, product, variationsList } = this.state;
|
2019-08-15 14:55:57 +00:00
|
|
|
const { onChange, renderItem, selected } = this.props;
|
2019-07-09 09:48:31 +00:00
|
|
|
const currentVariations = variationsList[ product ] || [];
|
|
|
|
const currentList = [ ...products, ...currentVariations ];
|
2019-01-10 19:01:49 +00:00
|
|
|
const messages = {
|
|
|
|
list: __( 'Products', 'woo-gutenberg-products-block' ),
|
|
|
|
noItems: __(
|
|
|
|
"Your store doesn't have any products.",
|
|
|
|
'woo-gutenberg-products-block'
|
|
|
|
),
|
|
|
|
search: __(
|
|
|
|
'Search for a product to display',
|
|
|
|
'woo-gutenberg-products-block'
|
|
|
|
),
|
|
|
|
updated: __(
|
|
|
|
'Product search results updated.',
|
|
|
|
'woo-gutenberg-products-block'
|
|
|
|
),
|
|
|
|
};
|
2019-07-09 09:48:31 +00:00
|
|
|
const selectedListItems = selected ? [ find( currentList, { id: selected } ) ] : [];
|
2019-01-10 19:01:49 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Fragment>
|
|
|
|
<SearchListControl
|
|
|
|
className="woocommerce-products"
|
2019-07-09 09:48:31 +00:00
|
|
|
list={ currentList }
|
2019-01-10 19:01:49 +00:00
|
|
|
isLoading={ loading }
|
|
|
|
isSingle
|
2019-07-09 09:48:31 +00:00
|
|
|
selected={ selectedListItems }
|
2019-01-10 19:01:49 +00:00
|
|
|
onChange={ onChange }
|
2019-08-15 14:55:57 +00:00
|
|
|
renderItem={ renderItem }
|
2019-08-17 09:14:11 +00:00
|
|
|
onSearch={ IS_LARGE_CATALOG ? this.debouncedOnSearch : null }
|
2019-01-10 19:01:49 +00:00
|
|
|
messages={ messages }
|
2019-07-09 09:48:31 +00:00
|
|
|
isHierarchical
|
2019-01-10 19:01:49 +00:00
|
|
|
/>
|
|
|
|
</Fragment>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ProductControl.propTypes = {
|
|
|
|
/**
|
|
|
|
* Callback to update the selected products.
|
|
|
|
*/
|
|
|
|
onChange: PropTypes.func.isRequired,
|
2019-08-15 14:55:57 +00:00
|
|
|
/**
|
|
|
|
* Callback to render each item in the selection list, allows any custom object-type rendering.
|
|
|
|
*/
|
|
|
|
renderItem: PropTypes.func.isRequired,
|
2019-01-10 19:01:49 +00:00
|
|
|
/**
|
|
|
|
* The ID of the currently selected product.
|
|
|
|
*/
|
|
|
|
selected: PropTypes.number.isRequired,
|
2019-08-15 14:55:57 +00:00
|
|
|
/**
|
|
|
|
* Query args to pass to getProducts.
|
|
|
|
*/
|
|
|
|
queryArgs: PropTypes.object,
|
2019-01-10 19:01:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
export default ProductControl;
|