Update ProductControl to use HOCs (https://github.com/woocommerce/woocommerce-blocks/pull/974)
* Update ProductControl to use HOCs * Only show variations in certain ProductControls * Cleanup * Refactor withSingleSelected selected prop logic * Fix selected values not being respected * Fix wrong propTypes notation * Set parent: 0 in getProducts util func * Use static properties in withProductVariations * Move messages outside of the functional component * Check that variations is an array * Fix debounced search * Fix broken tests * Rename withSingleSelected to withTransformSingleSelectToMultipleSelect
This commit is contained in:
parent
c86114d2cb
commit
01412e6af0
|
@ -97,6 +97,7 @@ const FeaturedProduct = ( {
|
|||
<div className="wc-block-featured-product__selection">
|
||||
<ProductControl
|
||||
selected={ attributes.productId || 0 }
|
||||
showVariations
|
||||
onChange={ ( value = [] ) => {
|
||||
const id = value[ 0 ] ? value[ 0 ].id : 0;
|
||||
setAttributes( {
|
||||
|
|
|
@ -2,21 +2,22 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { addQueryArgs } from '@wordpress/url';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { debounce, find, escapeRegExp, isEmpty } from 'lodash';
|
||||
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 { ENDPOINTS, IS_LARGE_CATALOG } from '@woocommerce/block-settings';
|
||||
import {
|
||||
withProductVariations,
|
||||
withSearchedProducts,
|
||||
withTransformSingleSelectToMultipleSelect,
|
||||
} from '../../hocs';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getProducts } from '../utils';
|
||||
import { IconRadioSelected, IconRadioUnselected } from '../icons';
|
||||
import ErrorMessage from '../error-placeholder/error-message.js';
|
||||
import './style.scss';
|
||||
|
||||
function getHighlightedName( name, search ) {
|
||||
|
@ -31,133 +32,41 @@ const getInteractionIcon = ( isSelected = false ) => {
|
|||
return isSelected ? <IconRadioSelected /> : <IconRadioUnselected />;
|
||||
};
|
||||
|
||||
class ProductControl extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
this.state = {
|
||||
products: [],
|
||||
product: 0,
|
||||
variationsList: {},
|
||||
variationsLoading: false,
|
||||
loading: true,
|
||||
};
|
||||
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'
|
||||
),
|
||||
};
|
||||
|
||||
this.debouncedOnSearch = debounce( this.onSearch.bind( this ), 400 );
|
||||
this.debouncedGetVariations = debounce(
|
||||
this.getVariations.bind( this ),
|
||||
200
|
||||
);
|
||||
this.renderItem = this.renderItem.bind( this );
|
||||
this.onProductSelect = this.onProductSelect.bind( this );
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.debouncedOnSearch.cancel();
|
||||
this.debouncedGetVariations.cancel();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { selected, queryArgs } = this.props;
|
||||
|
||||
getProducts( { selected, queryArgs } )
|
||||
.then( ( products ) => {
|
||||
products = products.map( ( product ) => {
|
||||
const count = product.variations
|
||||
? product.variations.length
|
||||
: 0;
|
||||
return {
|
||||
...product,
|
||||
parent: 0,
|
||||
count,
|
||||
};
|
||||
} );
|
||||
this.setState( { products, loading: false } );
|
||||
} )
|
||||
.catch( () => {
|
||||
this.setState( { products: [], loading: false } );
|
||||
} );
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps, prevState ) {
|
||||
if ( prevState.product !== this.state.product ) {
|
||||
this.debouncedGetVariations();
|
||||
}
|
||||
}
|
||||
|
||||
getVariations() {
|
||||
const { product, products, variationsList } = this.state;
|
||||
|
||||
if ( ! product ) {
|
||||
this.setState( {
|
||||
variationsList: {},
|
||||
variationsLoading: false,
|
||||
} );
|
||||
return;
|
||||
}
|
||||
|
||||
const productDetails = products.find(
|
||||
( findProduct ) => findProduct.id === product
|
||||
);
|
||||
|
||||
if (
|
||||
! productDetails.variations ||
|
||||
productDetails.variations.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! variationsList[ product ] ) {
|
||||
this.setState( { variationsLoading: true } );
|
||||
}
|
||||
|
||||
apiFetch( {
|
||||
path: addQueryArgs(
|
||||
`${ ENDPOINTS.products }/${ product }/variations`,
|
||||
{
|
||||
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 } );
|
||||
} );
|
||||
}
|
||||
|
||||
onSearch( search ) {
|
||||
const { selected, queryArgs } = this.props;
|
||||
getProducts( { selected, search, queryArgs } )
|
||||
.then( ( products ) => {
|
||||
this.setState( { products, loading: false } );
|
||||
} )
|
||||
.catch( () => {
|
||||
this.setState( { products: [], loading: false } );
|
||||
} );
|
||||
}
|
||||
|
||||
onProductSelect( item, isSelected ) {
|
||||
return () => {
|
||||
this.setState( {
|
||||
product: isSelected ? 0 : item.id,
|
||||
} );
|
||||
};
|
||||
}
|
||||
|
||||
renderItem( args ) {
|
||||
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 { product, variationsLoading } = this.state;
|
||||
const variationsCount =
|
||||
item.variations && Array.isArray( item.variations )
|
||||
? item.variations.length
|
||||
: 0;
|
||||
const classes = classnames(
|
||||
'woocommerce-search-product__item',
|
||||
'woocommerce-search-list__item',
|
||||
|
@ -165,7 +74,7 @@ class ProductControl extends Component {
|
|||
{
|
||||
'is-searching': search.length > 0,
|
||||
'is-skip-level': depth === 0 && item.parent !== 0,
|
||||
'is-variable': item.count > 0,
|
||||
'is-variable': variationsCount > 0,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -182,8 +91,8 @@ class ProductControl extends Component {
|
|||
}`;
|
||||
}
|
||||
|
||||
if ( item.count ) {
|
||||
a11yProps[ 'aria-expanded' ] = item.id === product;
|
||||
if ( variationsCount ) {
|
||||
a11yProps[ 'aria-expanded' ] = item.id === expandedProduct;
|
||||
}
|
||||
|
||||
// Top level items custom rendering based on SearchListItem.
|
||||
|
@ -197,7 +106,6 @@ class ProductControl extends Component {
|
|||
className={ classes }
|
||||
onClick={ () => {
|
||||
onSelect( item )();
|
||||
this.onProductSelect( item, isSelected )();
|
||||
} }
|
||||
>
|
||||
<span className="woocommerce-search-list__item-state">
|
||||
|
@ -213,31 +121,33 @@ class ProductControl extends Component {
|
|||
/>
|
||||
</span>
|
||||
|
||||
{ item.count ? (
|
||||
{ variationsCount ? (
|
||||
<span className="woocommerce-search-list__item-variation-count">
|
||||
{ sprintf(
|
||||
_n(
|
||||
'%d variation',
|
||||
'%d variations',
|
||||
item.count,
|
||||
variationsCount,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
item.count
|
||||
variationsCount
|
||||
) }
|
||||
</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>
|
||||
),
|
||||
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>
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -252,52 +162,44 @@ class ProductControl extends Component {
|
|||
{ ...a11yProps }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getRenderItemFunc = () => {
|
||||
if ( renderItem ) {
|
||||
return renderItem;
|
||||
} else if ( showVariations ) {
|
||||
return renderItemWithVariations;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
if ( error ) {
|
||||
return <ErrorMessage error={ error } />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { products, loading, product, variationsList } = this.state;
|
||||
const { onChange, renderItem, selected } = this.props;
|
||||
const currentVariations = variationsList[ product ] || [];
|
||||
const currentList = [ ...products, ...currentVariations ];
|
||||
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'
|
||||
),
|
||||
};
|
||||
const selectedListItems = selected
|
||||
? [ find( currentList, { id: selected } ) ]
|
||||
const currentVariations =
|
||||
variations && variations[ expandedProduct ]
|
||||
? variations[ expandedProduct ]
|
||||
: [];
|
||||
const currentList = [ ...products, ...currentVariations ];
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SearchListControl
|
||||
className="woocommerce-products"
|
||||
list={ currentList }
|
||||
isLoading={ loading }
|
||||
isSingle
|
||||
selected={ selectedListItems }
|
||||
onChange={ onChange }
|
||||
renderItem={ renderItem }
|
||||
onSearch={
|
||||
IS_LARGE_CATALOG ? this.debouncedOnSearch : null
|
||||
}
|
||||
messages={ messages }
|
||||
isHierarchical
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
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 = {
|
||||
/**
|
||||
|
@ -305,17 +207,37 @@ ProductControl.propTypes = {
|
|||
*/
|
||||
onChange: PropTypes.func.isRequired,
|
||||
/**
|
||||
* Callback to render each item in the selection list, allows any custom object-type rendering.
|
||||
* The ID of the currently expanded product.
|
||||
*/
|
||||
renderItem: PropTypes.func.isRequired,
|
||||
expandedProduct: PropTypes.number,
|
||||
/**
|
||||
* The ID of the currently selected product.
|
||||
* Callback to search products by their name.
|
||||
*/
|
||||
selected: PropTypes.number.isRequired,
|
||||
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,
|
||||
};
|
||||
|
||||
export default ProductControl;
|
||||
ProductControl.defaultProps = {
|
||||
expandedProduct: null,
|
||||
selected: [],
|
||||
showVariations: false,
|
||||
};
|
||||
|
||||
export default withTransformSingleSelectToMultipleSelect(
|
||||
withSearchedProducts( withProductVariations( ProductControl ) )
|
||||
);
|
||||
|
|
|
@ -68,7 +68,9 @@ const ProductsControl = ( {
|
|||
className="woocommerce-products"
|
||||
list={ products }
|
||||
isLoading={ isLoading }
|
||||
selected={ selected }
|
||||
selected={ products.filter( ( { id } ) =>
|
||||
selected.includes( id )
|
||||
) }
|
||||
onSearch={ onSearch }
|
||||
onChange={ onChange }
|
||||
messages={ messages }
|
||||
|
|
|
@ -54,7 +54,14 @@ export const getProducts = ( {
|
|||
const requests = getProductsRequests( { selected, search, queryArgs } );
|
||||
|
||||
return Promise.all( requests.map( ( path ) => apiFetch( { path } ) ) )
|
||||
.then( ( data ) => uniqBy( flatten( data ), 'id' ) )
|
||||
.then( ( data ) => {
|
||||
const products = uniqBy( flatten( data ), 'id' );
|
||||
const list = products.map( ( product ) => ( {
|
||||
...product,
|
||||
parent: 0,
|
||||
} ) );
|
||||
return list;
|
||||
} )
|
||||
.catch( ( e ) => {
|
||||
throw e;
|
||||
} );
|
||||
|
@ -130,6 +137,14 @@ export const getCategories = () => {
|
|||
} );
|
||||
};
|
||||
|
||||
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`, {
|
||||
|
|
|
@ -2,4 +2,8 @@ export { default as withAttributes } from './with-attributes';
|
|||
export { default as withCategories } from './with-categories';
|
||||
export { default as withCategory } from './with-category';
|
||||
export { default as withProduct } from './with-product';
|
||||
export { default as withProductVariations } from './with-product-variations';
|
||||
export { default as withSearchedProducts } from './with-searched-products';
|
||||
export {
|
||||
default as withTransformSingleSelectToMultipleSelect,
|
||||
} from './with-transform-single-select-to-multiple-select';
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import withProductVariations from '../with-product-variations';
|
||||
import * as mockUtils from '../../components/utils';
|
||||
import * as mockBaseUtils from '../../base/utils/errors';
|
||||
|
||||
jest.mock( '../../components/utils', () => ( {
|
||||
getProductVariations: jest.fn(),
|
||||
} ) );
|
||||
|
||||
jest.mock( '../../base/utils/errors', () => ( {
|
||||
formatError: jest.fn(),
|
||||
} ) );
|
||||
|
||||
const mockProducts = [
|
||||
{ id: 1, name: 'Hoodie', variations: [ 3, 4 ] },
|
||||
{ id: 2, name: 'Backpack' },
|
||||
];
|
||||
const mockVariations = [ { id: 3, name: 'Blue' }, { id: 4, name: 'Red' } ];
|
||||
const TestComponent = withProductVariations( ( props ) => {
|
||||
return (
|
||||
<div
|
||||
error={ props.error }
|
||||
expandedProduct={ props.expandedProduct }
|
||||
isLoading={ props.isLoading }
|
||||
variations={ props.variations }
|
||||
variationsLoading={ props.variationsLoading }
|
||||
/>
|
||||
);
|
||||
} );
|
||||
const render = () => {
|
||||
return TestRenderer.create(
|
||||
<TestComponent
|
||||
error={ null }
|
||||
isLoading={ false }
|
||||
products={ mockProducts }
|
||||
selected={ [ 1 ] }
|
||||
showVariations={ true }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe( 'withProductVariations Component', () => {
|
||||
let renderer;
|
||||
afterEach( () => {
|
||||
mockUtils.getProductVariations.mockReset();
|
||||
} );
|
||||
|
||||
describe( 'lifecycle events', () => {
|
||||
beforeEach( () => {
|
||||
mockUtils.getProductVariations.mockImplementation( () =>
|
||||
Promise.resolve( mockVariations )
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'getProductVariations is called on mount', () => {
|
||||
renderer = render();
|
||||
const { getProductVariations } = mockUtils;
|
||||
|
||||
expect( getProductVariations ).toHaveBeenCalledWith( 1 );
|
||||
expect( getProductVariations ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
|
||||
it( 'getProductVariations is called on component update', () => {
|
||||
renderer = TestRenderer.create(
|
||||
<TestComponent
|
||||
error={ null }
|
||||
isLoading={ false }
|
||||
products={ mockProducts }
|
||||
/>
|
||||
);
|
||||
const { getProductVariations } = mockUtils;
|
||||
|
||||
expect( getProductVariations ).toHaveBeenCalledTimes( 0 );
|
||||
|
||||
renderer.update(
|
||||
<TestComponent
|
||||
error={ null }
|
||||
isLoading={ false }
|
||||
products={ mockProducts }
|
||||
selected={ [ 1 ] }
|
||||
showVariations={ true }
|
||||
/>
|
||||
);
|
||||
|
||||
expect( getProductVariations ).toHaveBeenCalledWith( 1 );
|
||||
expect( getProductVariations ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
|
||||
it( 'getProductVariations is not called if selected product has no variations', () => {
|
||||
TestRenderer.create(
|
||||
<TestComponent
|
||||
error={ null }
|
||||
isLoading={ false }
|
||||
products={ mockProducts }
|
||||
selected={ [ 2 ] }
|
||||
showVariations={ true }
|
||||
/>
|
||||
);
|
||||
const { getProductVariations } = mockUtils;
|
||||
|
||||
expect( getProductVariations ).toHaveBeenCalledTimes( 0 );
|
||||
} );
|
||||
|
||||
it( 'getProductVariations is called if selected product is a variation', () => {
|
||||
TestRenderer.create(
|
||||
<TestComponent
|
||||
error={ null }
|
||||
isLoading={ false }
|
||||
products={ mockProducts }
|
||||
selected={ [ 3 ] }
|
||||
showVariations={ true }
|
||||
/>
|
||||
);
|
||||
const { getProductVariations } = mockUtils;
|
||||
|
||||
expect( getProductVariations ).toHaveBeenCalledWith( 1 );
|
||||
expect( getProductVariations ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'when the API returns variations data', () => {
|
||||
beforeEach( () => {
|
||||
mockUtils.getProductVariations.mockImplementation( () =>
|
||||
Promise.resolve( mockVariations )
|
||||
);
|
||||
renderer = render();
|
||||
} );
|
||||
|
||||
it( 'sets the variations props', () => {
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
const expectedVariations = {
|
||||
1: [
|
||||
{ id: 3, name: 'Blue', parent: 1 },
|
||||
{ id: 4, name: 'Red', parent: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
expect( props.error ).toBeNull();
|
||||
expect( props.isLoading ).toBe( false );
|
||||
expect( props.variations ).toEqual( expectedVariations );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'when the API returns an error', () => {
|
||||
const error = { message: 'There was an error.' };
|
||||
const getProductVariationsPromise = Promise.reject( error );
|
||||
const formattedError = { message: 'There was an error.', type: 'api' };
|
||||
|
||||
beforeEach( () => {
|
||||
mockUtils.getProductVariations.mockImplementation(
|
||||
() => getProductVariationsPromise
|
||||
);
|
||||
mockBaseUtils.formatError.mockImplementation(
|
||||
() => formattedError
|
||||
);
|
||||
renderer = render();
|
||||
} );
|
||||
|
||||
it( 'sets the error prop', ( done ) => {
|
||||
const { formatError } = mockBaseUtils;
|
||||
getProductVariationsPromise.catch( () => {
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
|
||||
expect( formatError ).toHaveBeenCalledWith( error );
|
||||
expect( formatError ).toHaveBeenCalledTimes( 1 );
|
||||
expect( props.error ).toEqual( formattedError );
|
||||
expect( props.isLoading ).toBe( false );
|
||||
expect( props.variations ).toEqual( { 1: null } );
|
||||
|
||||
done();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -18,7 +18,10 @@ jest.mock( '@woocommerce/block-settings', () => ( {
|
|||
mockedUtils.getProducts = jest
|
||||
.fn()
|
||||
.mockImplementation( () =>
|
||||
Promise.resolve( [ { id: 10, name: 'foo' }, { id: 20, name: 'bar' } ] )
|
||||
Promise.resolve( [
|
||||
{ id: 10, name: 'foo', parent: 0 },
|
||||
{ id: 20, name: 'bar', parent: 0 },
|
||||
] )
|
||||
);
|
||||
|
||||
// Add a mock implementation of debounce for testing so we can spy on
|
||||
|
@ -68,10 +71,10 @@ describe( 'withSearchedProducts Component', () => {
|
|||
);
|
||||
it( 'has expected values for props', () => {
|
||||
props = renderer.root.findByType( 'div' ).props;
|
||||
expect( props.selected ).toEqual( [ { id: 10, name: 'foo' } ] );
|
||||
expect( props.selected ).toEqual( selected );
|
||||
expect( props.products ).toEqual( [
|
||||
{ id: 10, name: 'foo' },
|
||||
{ id: 20, name: 'bar' },
|
||||
{ id: 10, name: 'foo', parent: 0 },
|
||||
{ id: 20, name: 'bar', parent: 0 },
|
||||
] );
|
||||
} );
|
||||
it( 'debounce and getProducts is called on search event', () => {
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import withTransformSingleSelectToMultipleSelect from '../with-transform-single-select-to-multiple-select';
|
||||
|
||||
const TestComponent = withTransformSingleSelectToMultipleSelect( ( props ) => {
|
||||
return <div selected={ props.selected } />;
|
||||
} );
|
||||
|
||||
describe( 'withTransformSingleSelectToMultipleSelect Component', () => {
|
||||
describe( 'when the API returns an error', () => {
|
||||
it( 'converts the selected value into an array', () => {
|
||||
const selected = 123;
|
||||
const renderer = TestRenderer.create(
|
||||
<TestComponent selected={ selected } />
|
||||
);
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
expect( props.selected ).toEqual( [ selected ] );
|
||||
} );
|
||||
|
||||
it( 'passes an empty array as the selected prop if selected was null', () => {
|
||||
const renderer = TestRenderer.create(
|
||||
<TestComponent selected={ null } />
|
||||
);
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
expect( props.selected ).toEqual( [] );
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,188 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Component } from '@wordpress/element';
|
||||
import { createHigherOrderComponent } from '@wordpress/compose';
|
||||
import PropTypes from 'prop-types';
|
||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getProductVariations } from '../components/utils';
|
||||
import { formatError } from '../base/utils/errors.js';
|
||||
|
||||
const withProductVariations = createHigherOrderComponent(
|
||||
( OriginalComponent ) => {
|
||||
class WrappedComponent extends Component {
|
||||
state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
variations: {},
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { selected, showVariations } = this.props;
|
||||
|
||||
if ( selected && showVariations ) {
|
||||
this.loadVariations();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
const { isLoading, selected, showVariations } = this.props;
|
||||
|
||||
if (
|
||||
showVariations &&
|
||||
( ! isShallowEqual( prevProps.selected, selected ) ||
|
||||
( prevProps.isLoading && ! isLoading ) )
|
||||
) {
|
||||
this.loadVariations();
|
||||
}
|
||||
}
|
||||
|
||||
loadVariations = () => {
|
||||
const { products } = this.props;
|
||||
const { loading, variations } = this.state;
|
||||
|
||||
if ( loading ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expandedProduct = this.getExpandedProduct();
|
||||
|
||||
if ( ! expandedProduct || variations[ expandedProduct ] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const productDetails = products.find(
|
||||
( findProduct ) => findProduct.id === expandedProduct
|
||||
);
|
||||
|
||||
if (
|
||||
! productDetails.variations ||
|
||||
productDetails.variations.length === 0
|
||||
) {
|
||||
this.setState( {
|
||||
variations: {
|
||||
...this.state.variations,
|
||||
[ expandedProduct ]: null,
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
} );
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState( { loading: true } );
|
||||
|
||||
getProductVariations( expandedProduct )
|
||||
.then( ( expandedProductVariations ) => {
|
||||
const newVariations = expandedProductVariations.map(
|
||||
( variation ) => ( {
|
||||
...variation,
|
||||
parent: expandedProduct,
|
||||
} )
|
||||
);
|
||||
this.setState( {
|
||||
variations: {
|
||||
...this.state.variations,
|
||||
[ expandedProduct ]: newVariations,
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
} );
|
||||
} )
|
||||
.catch( async ( e ) => {
|
||||
const error = await formatError( e );
|
||||
|
||||
this.setState( {
|
||||
variations: {
|
||||
...this.state.variations,
|
||||
[ expandedProduct ]: null,
|
||||
},
|
||||
loading: false,
|
||||
error,
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
isProductId( itemId ) {
|
||||
const { products } = this.props;
|
||||
return products.some( ( p ) => p.id === itemId );
|
||||
}
|
||||
|
||||
findParentProduct( variationId ) {
|
||||
const { products } = this.props;
|
||||
const parentProduct = products.filter(
|
||||
( p ) =>
|
||||
p.variations && p.variations.includes( variationId )
|
||||
);
|
||||
return parentProduct[ 0 ].id;
|
||||
}
|
||||
|
||||
getExpandedProduct() {
|
||||
const { isLoading, selected, showVariations } = this.props;
|
||||
|
||||
if ( ! showVariations ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let selectedItem =
|
||||
selected && selected.length ? selected[ 0 ] : null;
|
||||
|
||||
// If there is no selected item, check if there was one in the past, so we
|
||||
// can keep the same product expanded.
|
||||
if ( selectedItem ) {
|
||||
this.prevSelectedItem = selectedItem;
|
||||
} else if ( this.prevSelectedItem ) {
|
||||
// If previous selected item was a variation
|
||||
if (
|
||||
! isLoading &&
|
||||
! this.isProductId( this.prevSelectedItem )
|
||||
) {
|
||||
selectedItem = this.prevSelectedItem;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! isLoading && selectedItem ) {
|
||||
return this.isProductId( selectedItem )
|
||||
? selectedItem
|
||||
: this.findParentProduct( selectedItem );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error: propsError, isLoading } = this.props;
|
||||
const { error, loading, variations } = this.state;
|
||||
|
||||
return (
|
||||
<OriginalComponent
|
||||
{ ...this.props }
|
||||
error={ error || propsError }
|
||||
expandedProduct={ this.getExpandedProduct() }
|
||||
isLoading={ isLoading }
|
||||
variations={ variations }
|
||||
variationsLoading={ loading }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
selected: PropTypes.array,
|
||||
showVariations: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
selected: [],
|
||||
showVariations: false,
|
||||
};
|
||||
}
|
||||
return WrappedComponent;
|
||||
},
|
||||
'withProductVariations'
|
||||
);
|
||||
|
||||
export default withProductVariations;
|
|
@ -69,16 +69,13 @@ const withSearchedProducts = createHigherOrderComponent(
|
|||
|
||||
render() {
|
||||
const { error, list, loading } = this.state;
|
||||
const { selected } = this.props;
|
||||
|
||||
return (
|
||||
<OriginalComponent
|
||||
{ ...this.props }
|
||||
error={ error }
|
||||
products={ list }
|
||||
isLoading={ loading }
|
||||
selected={ list.filter( ( { id } ) =>
|
||||
selected.includes( id )
|
||||
) }
|
||||
onSearch={
|
||||
IS_LARGE_CATALOG ? this.debouncedOnSearch : null
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Component } from '@wordpress/element';
|
||||
import { createHigherOrderComponent } from '@wordpress/compose';
|
||||
import PropTypes from 'prop-types';
|
||||
import { isNil } from 'lodash';
|
||||
|
||||
const withTransformSingleSelectToMultipleSelect = createHigherOrderComponent(
|
||||
( OriginalComponent ) => {
|
||||
class WrappedComponent extends Component {
|
||||
render() {
|
||||
const { selected } = this.props;
|
||||
|
||||
return (
|
||||
<OriginalComponent
|
||||
{ ...this.props }
|
||||
selected={ isNil( selected ) ? [] : [ selected ] }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
WrappedComponent.propTypes = {
|
||||
selected: PropTypes.oneOfType( [
|
||||
PropTypes.number,
|
||||
PropTypes.string,
|
||||
] ),
|
||||
};
|
||||
WrappedComponent.defaultProps = {
|
||||
selected: null,
|
||||
};
|
||||
return WrappedComponent;
|
||||
},
|
||||
'withTransformSingleSelectToMultipleSelect'
|
||||
);
|
||||
|
||||
export default withTransformSingleSelectToMultipleSelect;
|
Loading…
Reference in New Issue