khaihihi
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
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';
|
||||
export { default as withRestApiHydration } from './with-rest-api-hydration';
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import withAttributes from '../with-attributes';
|
||||
import * as mockUtils from '../../components/utils';
|
||||
import * as mockBaseUtils from '../../base/utils/errors';
|
||||
|
||||
jest.mock( '../../components/utils', () => ( {
|
||||
getAttributes: jest.fn(),
|
||||
getTerms: jest.fn(),
|
||||
} ) );
|
||||
|
||||
jest.mock( '../../base/utils/errors', () => ( {
|
||||
formatError: jest.fn(),
|
||||
} ) );
|
||||
|
||||
jest.mock( 'lodash', () => ( {
|
||||
...jest.requireActual( 'lodash' ),
|
||||
debounce: ( func ) => func,
|
||||
} ) );
|
||||
|
||||
const mockAttributes = [
|
||||
{ id: 1, name: 'Color', slug: 'color' },
|
||||
{ id: 2, name: 'Size', slug: 'size' },
|
||||
];
|
||||
const mockAttributesWithParent = [
|
||||
{ id: 1, name: 'Color', slug: 'color', parent: 0 },
|
||||
{ id: 2, name: 'Size', slug: 'size', parent: 0 },
|
||||
];
|
||||
const selected = [ { id: 11, attr_slug: 'color' } ];
|
||||
const TestComponent = withAttributes( ( props ) => {
|
||||
return (
|
||||
<div
|
||||
attributes={ props.attributes }
|
||||
error={ props.error }
|
||||
expandedAttribute={ props.expandedAttribute }
|
||||
onExpandAttribute={ props.onExpandAttribute }
|
||||
isLoading={ props.isLoading }
|
||||
termsAreLoading={ props.termsAreLoading }
|
||||
termsList={ props.termsList }
|
||||
/>
|
||||
);
|
||||
} );
|
||||
|
||||
describe( 'withAttributes Component', () => {
|
||||
afterEach( () => {
|
||||
mockUtils.getAttributes.mockReset();
|
||||
mockUtils.getTerms.mockReset();
|
||||
mockBaseUtils.formatError.mockReset();
|
||||
} );
|
||||
|
||||
describe( 'lifecycle events', () => {
|
||||
let getAttributesPromise;
|
||||
|
||||
beforeEach( () => {
|
||||
getAttributesPromise = Promise.resolve( mockAttributes );
|
||||
mockUtils.getAttributes.mockImplementation(
|
||||
() => getAttributesPromise
|
||||
);
|
||||
mockUtils.getTerms.mockImplementation( () =>
|
||||
Promise.resolve( [] )
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'getAttributes is called on mount', () => {
|
||||
TestRenderer.create( <TestComponent /> );
|
||||
const { getAttributes } = mockUtils;
|
||||
|
||||
expect( getAttributes ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
|
||||
it( 'getTerms is called on component update', () => {
|
||||
const renderer = TestRenderer.create( <TestComponent /> );
|
||||
let props = renderer.root.findByType( 'div' ).props;
|
||||
|
||||
props.onExpandAttribute( 1 );
|
||||
|
||||
const { getTerms } = mockUtils;
|
||||
props = renderer.root.findByType( 'div' ).props;
|
||||
|
||||
expect( getTerms ).toHaveBeenCalledWith( 1 );
|
||||
expect( getTerms ).toHaveBeenCalledTimes( 1 );
|
||||
expect( props.expandedAttribute ).toBe( 1 );
|
||||
} );
|
||||
|
||||
it( 'getTerms is called on mount if there was an attribute selected', ( done ) => {
|
||||
const renderer = TestRenderer.create(
|
||||
<TestComponent selected={ selected } />
|
||||
);
|
||||
|
||||
getAttributesPromise.then( () => {
|
||||
const { getTerms } = mockUtils;
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
|
||||
expect( getTerms ).toHaveBeenCalledWith( 1 );
|
||||
expect( getTerms ).toHaveBeenCalledTimes( 1 );
|
||||
expect( props.expandedAttribute ).toBe( 1 );
|
||||
done();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'when the API returns attributes data', () => {
|
||||
let renderer;
|
||||
|
||||
beforeEach( () => {
|
||||
mockUtils.getAttributes.mockImplementation( () =>
|
||||
Promise.resolve( mockAttributes )
|
||||
);
|
||||
renderer = TestRenderer.create( <TestComponent /> );
|
||||
} );
|
||||
|
||||
it( 'sets the attributes props', () => {
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
|
||||
expect( props.error ).toBeNull();
|
||||
expect( props.isLoading ).toBe( false );
|
||||
expect( props.attributes ).toEqual( mockAttributesWithParent );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'when the API returns an error', () => {
|
||||
const error = { message: 'There was an error.' };
|
||||
const getAttributesPromise = Promise.reject( error );
|
||||
const formattedError = { message: 'There was an error.', type: 'api' };
|
||||
let renderer;
|
||||
|
||||
beforeEach( () => {
|
||||
mockUtils.getAttributes.mockImplementation(
|
||||
() => getAttributesPromise
|
||||
);
|
||||
mockBaseUtils.formatError.mockImplementation(
|
||||
() => formattedError
|
||||
);
|
||||
renderer = TestRenderer.create( <TestComponent /> );
|
||||
} );
|
||||
|
||||
it( 'sets the error prop', ( done ) => {
|
||||
const { formatError } = mockBaseUtils;
|
||||
getAttributesPromise.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.attributes ).toEqual( [] );
|
||||
done();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import withCategories from '../with-categories';
|
||||
import * as mockUtils from '../../components/utils';
|
||||
import * as mockBaseUtils from '../../base/utils/errors';
|
||||
|
||||
jest.mock( '../../components/utils', () => ( {
|
||||
getCategories: jest.fn(),
|
||||
} ) );
|
||||
|
||||
jest.mock( '../../base/utils/errors', () => ( {
|
||||
formatError: jest.fn(),
|
||||
} ) );
|
||||
|
||||
const mockCategories = [ { id: 1, name: 'Clothing' }, { id: 2, name: 'Food' } ];
|
||||
const TestComponent = withCategories( ( props ) => {
|
||||
return (
|
||||
<div
|
||||
error={ props.error }
|
||||
isLoading={ props.isLoading }
|
||||
categories={ props.categories }
|
||||
/>
|
||||
);
|
||||
} );
|
||||
const render = () => {
|
||||
return TestRenderer.create( <TestComponent /> );
|
||||
};
|
||||
|
||||
describe( 'withCategories Component', () => {
|
||||
let renderer;
|
||||
afterEach( () => {
|
||||
mockUtils.getCategories.mockReset();
|
||||
} );
|
||||
|
||||
describe( 'lifecycle events', () => {
|
||||
beforeEach( () => {
|
||||
mockUtils.getCategories.mockImplementation( () =>
|
||||
Promise.resolve()
|
||||
);
|
||||
renderer = render();
|
||||
} );
|
||||
|
||||
it( 'getCategories is called on mount', () => {
|
||||
const { getCategories } = mockUtils;
|
||||
|
||||
expect( getCategories ).toHaveBeenCalledWith( {
|
||||
show_review_count: false,
|
||||
} );
|
||||
expect( getCategories ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'when the API returns categories data', () => {
|
||||
beforeEach( () => {
|
||||
mockUtils.getCategories.mockImplementation( () =>
|
||||
Promise.resolve( mockCategories )
|
||||
);
|
||||
renderer = render();
|
||||
} );
|
||||
|
||||
it( 'sets the categories props', () => {
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
|
||||
expect( props.error ).toBeNull();
|
||||
expect( props.isLoading ).toBe( false );
|
||||
expect( props.categories ).toEqual( mockCategories );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'when the API returns an error', () => {
|
||||
const error = { message: 'There was an error.' };
|
||||
const getCategoriesPromise = Promise.reject( error );
|
||||
const formattedError = { message: 'There was an error.', type: 'api' };
|
||||
|
||||
beforeEach( () => {
|
||||
mockUtils.getCategories.mockImplementation(
|
||||
() => getCategoriesPromise
|
||||
);
|
||||
mockBaseUtils.formatError.mockImplementation(
|
||||
() => formattedError
|
||||
);
|
||||
renderer = render();
|
||||
} );
|
||||
|
||||
it( 'sets the error prop', ( done ) => {
|
||||
const { formatError } = mockBaseUtils;
|
||||
getCategoriesPromise.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.categories ).toBeNull();
|
||||
|
||||
done();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import withCategory from '../with-category';
|
||||
import * as mockUtils from '../../components/utils';
|
||||
import * as mockBaseUtils from '../../base/utils/errors';
|
||||
|
||||
jest.mock( '../../components/utils', () => ( {
|
||||
getCategory: jest.fn(),
|
||||
} ) );
|
||||
|
||||
jest.mock( '../../base/utils/errors', () => ( {
|
||||
formatError: jest.fn(),
|
||||
} ) );
|
||||
|
||||
const mockCategory = { name: 'Clothing' };
|
||||
const attributes = { categoryId: 1 };
|
||||
const TestComponent = withCategory( ( props ) => {
|
||||
return (
|
||||
<div
|
||||
error={ props.error }
|
||||
getCategory={ props.getCategory }
|
||||
isLoading={ props.isLoading }
|
||||
category={ props.category }
|
||||
/>
|
||||
);
|
||||
} );
|
||||
const render = () => {
|
||||
return TestRenderer.create( <TestComponent attributes={ attributes } /> );
|
||||
};
|
||||
|
||||
describe( 'withCategory Component', () => {
|
||||
let renderer;
|
||||
afterEach( () => {
|
||||
mockUtils.getCategory.mockReset();
|
||||
} );
|
||||
|
||||
describe( 'lifecycle events', () => {
|
||||
beforeEach( () => {
|
||||
mockUtils.getCategory.mockImplementation( () => Promise.resolve() );
|
||||
renderer = render();
|
||||
} );
|
||||
|
||||
it( 'getCategory is called on mount with passed in category id', () => {
|
||||
const { getCategory } = mockUtils;
|
||||
|
||||
expect( getCategory ).toHaveBeenCalledWith( attributes.categoryId );
|
||||
expect( getCategory ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
|
||||
it( 'getCategory is called on component update', () => {
|
||||
const { getCategory } = mockUtils;
|
||||
const newAttributes = { ...attributes, categoryId: 2 };
|
||||
renderer.update( <TestComponent attributes={ newAttributes } /> );
|
||||
|
||||
expect( getCategory ).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
newAttributes.categoryId
|
||||
);
|
||||
expect( getCategory ).toHaveBeenCalledTimes( 2 );
|
||||
} );
|
||||
|
||||
it( 'getCategory is hooked to the prop', () => {
|
||||
const { getCategory } = mockUtils;
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
|
||||
props.getCategory();
|
||||
|
||||
expect( getCategory ).toHaveBeenCalledTimes( 2 );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'when the API returns category data', () => {
|
||||
beforeEach( () => {
|
||||
mockUtils.getCategory.mockImplementation( ( categoryId ) =>
|
||||
Promise.resolve( { ...mockCategory, id: categoryId } )
|
||||
);
|
||||
renderer = render();
|
||||
} );
|
||||
|
||||
it( 'sets the category props', () => {
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
|
||||
expect( props.error ).toBeNull();
|
||||
expect( typeof props.getCategory ).toBe( 'function' );
|
||||
expect( props.isLoading ).toBe( false );
|
||||
expect( props.category ).toEqual( {
|
||||
...mockCategory,
|
||||
id: attributes.categoryId,
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'when the API returns an error', () => {
|
||||
const error = { message: 'There was an error.' };
|
||||
const getCategoryPromise = Promise.reject( error );
|
||||
const formattedError = { message: 'There was an error.', type: 'api' };
|
||||
|
||||
beforeEach( () => {
|
||||
mockUtils.getCategory.mockImplementation(
|
||||
() => getCategoryPromise
|
||||
);
|
||||
mockBaseUtils.formatError.mockImplementation(
|
||||
() => formattedError
|
||||
);
|
||||
renderer = render();
|
||||
} );
|
||||
|
||||
it( 'sets the error prop', ( done ) => {
|
||||
const { formatError } = mockBaseUtils;
|
||||
getCategoryPromise.catch( () => {
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
|
||||
expect( formatError ).toHaveBeenCalledWith( error );
|
||||
expect( formatError ).toHaveBeenCalledTimes( 1 );
|
||||
expect( props.error ).toEqual( formattedError );
|
||||
expect( typeof props.getCategory ).toBe( 'function' );
|
||||
expect( props.isLoading ).toBe( false );
|
||||
expect( props.category ).toBeNull();
|
||||
|
||||
done();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -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();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import withProduct from '../with-product';
|
||||
import * as mockUtils from '../../components/utils';
|
||||
import * as mockBaseUtils from '../../base/utils/errors';
|
||||
|
||||
jest.mock( '../../components/utils', () => ( {
|
||||
getProduct: jest.fn(),
|
||||
} ) );
|
||||
|
||||
jest.mock( '../../base/utils/errors', () => ( {
|
||||
formatError: jest.fn(),
|
||||
} ) );
|
||||
|
||||
const mockProduct = { name: 'T-Shirt' };
|
||||
const attributes = { productId: 1 };
|
||||
const TestComponent = withProduct( ( props ) => {
|
||||
return (
|
||||
<div
|
||||
error={ props.error }
|
||||
getProduct={ props.getProduct }
|
||||
isLoading={ props.isLoading }
|
||||
product={ props.product }
|
||||
/>
|
||||
);
|
||||
} );
|
||||
const render = () => {
|
||||
return TestRenderer.create( <TestComponent attributes={ attributes } /> );
|
||||
};
|
||||
|
||||
describe( 'withProduct Component', () => {
|
||||
let renderer;
|
||||
afterEach( () => {
|
||||
mockUtils.getProduct.mockReset();
|
||||
} );
|
||||
|
||||
describe( 'lifecycle events', () => {
|
||||
beforeEach( () => {
|
||||
mockUtils.getProduct.mockImplementation( () => Promise.resolve() );
|
||||
renderer = render();
|
||||
} );
|
||||
|
||||
it( 'getProduct is called on mount with passed in product id', () => {
|
||||
const { getProduct } = mockUtils;
|
||||
|
||||
expect( getProduct ).toHaveBeenCalledWith( attributes.productId );
|
||||
expect( getProduct ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
|
||||
it( 'getProduct is called on component update', () => {
|
||||
const { getProduct } = mockUtils;
|
||||
const newAttributes = { ...attributes, productId: 2 };
|
||||
renderer.update( <TestComponent attributes={ newAttributes } /> );
|
||||
|
||||
expect( getProduct ).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
newAttributes.productId
|
||||
);
|
||||
expect( getProduct ).toHaveBeenCalledTimes( 2 );
|
||||
} );
|
||||
|
||||
it( 'getProduct is hooked to the prop', () => {
|
||||
const { getProduct } = mockUtils;
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
|
||||
props.getProduct();
|
||||
|
||||
expect( getProduct ).toHaveBeenCalledTimes( 2 );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'when the API returns product data', () => {
|
||||
beforeEach( () => {
|
||||
mockUtils.getProduct.mockImplementation( ( productId ) =>
|
||||
Promise.resolve( { ...mockProduct, id: productId } )
|
||||
);
|
||||
renderer = render();
|
||||
} );
|
||||
|
||||
it( 'sets the product props', () => {
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
|
||||
expect( props.error ).toBeNull();
|
||||
expect( typeof props.getProduct ).toBe( 'function' );
|
||||
expect( props.isLoading ).toBe( false );
|
||||
expect( props.product ).toEqual( {
|
||||
...mockProduct,
|
||||
id: attributes.productId,
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'when the API returns an error', () => {
|
||||
const error = { message: 'There was an error.' };
|
||||
const getProductPromise = Promise.reject( error );
|
||||
const formattedError = { message: 'There was an error.', type: 'api' };
|
||||
|
||||
beforeEach( () => {
|
||||
mockUtils.getProduct.mockImplementation( () => getProductPromise );
|
||||
mockBaseUtils.formatError.mockImplementation(
|
||||
() => formattedError
|
||||
);
|
||||
renderer = render();
|
||||
} );
|
||||
|
||||
it( 'sets the error prop', ( done ) => {
|
||||
const { formatError } = mockBaseUtils;
|
||||
getProductPromise.catch( () => {
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
|
||||
expect( formatError ).toHaveBeenCalledWith( error );
|
||||
expect( formatError ).toHaveBeenCalledTimes( 1 );
|
||||
expect( props.error ).toEqual( formattedError );
|
||||
expect( typeof props.getProduct ).toBe( 'function' );
|
||||
expect( props.isLoading ).toBe( false );
|
||||
expect( props.product ).toBeNull();
|
||||
|
||||
done();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
import _ from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import withSearchedProducts from '../with-searched-products';
|
||||
import * as mockedUtils from '../../components/utils';
|
||||
|
||||
jest.mock( '@woocommerce/block-settings', () => ( {
|
||||
IS_LARGE_CATALOG: true,
|
||||
} ) );
|
||||
|
||||
// Mock the getProducts and isLargeCatalog values for tests.
|
||||
mockedUtils.getProducts = jest
|
||||
.fn()
|
||||
.mockImplementation( () =>
|
||||
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
|
||||
// the onSearch call.
|
||||
const debouncedCancel = jest.fn();
|
||||
const debouncedAction = jest.fn();
|
||||
_.debounce = ( onSearch ) => {
|
||||
const debounced = debouncedAction.mockImplementation( () => {
|
||||
onSearch();
|
||||
} );
|
||||
debounced.cancel = debouncedCancel;
|
||||
return debounced;
|
||||
};
|
||||
|
||||
describe( 'withSearchedProducts Component', () => {
|
||||
const { getProducts } = mockedUtils;
|
||||
afterEach( () => {
|
||||
debouncedCancel.mockClear();
|
||||
debouncedAction.mockClear();
|
||||
mockedUtils.getProducts.mockClear();
|
||||
} );
|
||||
const TestComponent = withSearchedProducts(
|
||||
( { selected, products, isLoading, onSearch } ) => {
|
||||
return (
|
||||
<div
|
||||
products={ products }
|
||||
selected={ selected }
|
||||
isLoading={ isLoading }
|
||||
onSearch={ onSearch }
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
describe( 'lifecycle tests', () => {
|
||||
const selected = [ 10 ];
|
||||
const renderer = TestRenderer.create(
|
||||
<TestComponent selected={ selected } />
|
||||
);
|
||||
let props;
|
||||
it(
|
||||
'getProducts is called on mount with passed in selected ' +
|
||||
'values',
|
||||
() => {
|
||||
expect( getProducts ).toHaveBeenCalledWith( { selected } );
|
||||
expect( getProducts ).toHaveBeenCalledTimes( 1 );
|
||||
}
|
||||
);
|
||||
it( 'has expected values for props', () => {
|
||||
props = renderer.root.findByType( 'div' ).props;
|
||||
expect( props.selected ).toEqual( selected );
|
||||
expect( props.products ).toEqual( [
|
||||
{ id: 10, name: 'foo', parent: 0 },
|
||||
{ id: 20, name: 'bar', parent: 0 },
|
||||
] );
|
||||
} );
|
||||
it( 'debounce and getProducts is called on search event', () => {
|
||||
props = renderer.root.findByType( 'div' ).props;
|
||||
props.onSearch();
|
||||
expect( debouncedAction ).toHaveBeenCalled();
|
||||
expect( getProducts ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
it( 'debounce is cancelled on unmount', () => {
|
||||
renderer.unmount();
|
||||
expect( debouncedCancel ).toHaveBeenCalled();
|
||||
expect( getProducts ).toHaveBeenCalledTimes( 0 );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -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,168 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Component } from '@wordpress/element';
|
||||
import { createHigherOrderComponent } from '@wordpress/compose';
|
||||
import PropTypes from 'prop-types';
|
||||
import { debounce } from 'lodash';
|
||||
import { getAttributes, getTerms } from '@woocommerce/block-components/utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { formatError } from '../base/utils/errors.js';
|
||||
|
||||
const withAttributes = createHigherOrderComponent( ( OriginalComponent ) => {
|
||||
class WrappedComponent extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
this.state = {
|
||||
attributes: [],
|
||||
error: null,
|
||||
expandedAttribute: null,
|
||||
loading: false,
|
||||
termsList: {},
|
||||
termsLoading: false,
|
||||
};
|
||||
|
||||
this.loadAttributes = this.loadAttributes.bind( this );
|
||||
this.onExpandAttribute = this.onExpandAttribute.bind( this );
|
||||
this.debouncedLoadTerms = debounce(
|
||||
this.loadTerms.bind( this ),
|
||||
200
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadAttributes();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.debouncedLoadTerms.cancel();
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps, prevState ) {
|
||||
if (
|
||||
prevState.expandedAttribute !== this.state.expandedAttribute
|
||||
) {
|
||||
this.debouncedLoadTerms();
|
||||
}
|
||||
}
|
||||
|
||||
loadAttributes() {
|
||||
const { selected } = this.props;
|
||||
const { expandedAttribute } = this.state;
|
||||
this.setState( { loading: true } );
|
||||
|
||||
getAttributes()
|
||||
.then( ( attributes ) => {
|
||||
attributes = attributes.map( ( item ) => ( {
|
||||
...item,
|
||||
parent: 0,
|
||||
} ) );
|
||||
let newExpandedAttribute = expandedAttribute;
|
||||
if ( ! expandedAttribute && selected.length > 0 ) {
|
||||
const attr = attributes.find(
|
||||
( item ) => item.slug === selected[ 0 ].attr_slug
|
||||
);
|
||||
if ( attr ) {
|
||||
newExpandedAttribute = attr.id;
|
||||
}
|
||||
}
|
||||
this.setState( {
|
||||
attributes,
|
||||
expandedAttribute: newExpandedAttribute,
|
||||
loading: false,
|
||||
error: null,
|
||||
} );
|
||||
} )
|
||||
.catch( async ( e ) => {
|
||||
const error = await formatError( e );
|
||||
|
||||
this.setState( {
|
||||
attributes: [],
|
||||
expandedAttribute: null,
|
||||
loading: false,
|
||||
error,
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
loadTerms() {
|
||||
const { expandedAttribute, termsList } = this.state;
|
||||
if ( ! expandedAttribute ) {
|
||||
return;
|
||||
}
|
||||
if ( ! termsList[ expandedAttribute ] ) {
|
||||
this.setState( { termsLoading: true } );
|
||||
}
|
||||
|
||||
getTerms( expandedAttribute )
|
||||
.then( ( terms ) => {
|
||||
terms = terms.map( ( term ) => ( {
|
||||
...term,
|
||||
parent: expandedAttribute,
|
||||
attr_slug: term.attribute.slug,
|
||||
} ) );
|
||||
this.setState( ( prevState ) => ( {
|
||||
termsList: {
|
||||
...prevState.termsList,
|
||||
[ expandedAttribute ]: terms,
|
||||
},
|
||||
termsLoading: false,
|
||||
} ) );
|
||||
} )
|
||||
.catch( async ( e ) => {
|
||||
const error = await formatError( e );
|
||||
|
||||
this.setState( {
|
||||
termsList: {},
|
||||
termsLoading: false,
|
||||
error,
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
onExpandAttribute( attributeId ) {
|
||||
const { expandedAttribute } = this.state;
|
||||
|
||||
this.setState( {
|
||||
expandedAttribute:
|
||||
attributeId === expandedAttribute ? null : attributeId,
|
||||
} );
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
error,
|
||||
expandedAttribute,
|
||||
loading,
|
||||
attributes,
|
||||
termsList,
|
||||
termsLoading,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<OriginalComponent
|
||||
{ ...this.props }
|
||||
attributes={ attributes }
|
||||
error={ error }
|
||||
expandedAttribute={ expandedAttribute }
|
||||
onExpandAttribute={ this.onExpandAttribute }
|
||||
isLoading={ loading }
|
||||
termsAreLoading={ termsLoading }
|
||||
termsList={ termsList }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
WrappedComponent.propTypes = {
|
||||
selected: PropTypes.array,
|
||||
};
|
||||
WrappedComponent.defaultProps = {
|
||||
selected: [],
|
||||
};
|
||||
return WrappedComponent;
|
||||
}, 'withAttributes' );
|
||||
|
||||
export default withAttributes;
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Component } from '@wordpress/element';
|
||||
import { createHigherOrderComponent } from '@wordpress/compose';
|
||||
import { getCategories } from '@woocommerce/block-components/utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { formatError } from '../base/utils/errors.js';
|
||||
|
||||
const withCategories = createHigherOrderComponent( ( OriginalComponent ) => {
|
||||
return class WrappedComponent extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
this.state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
categories: null,
|
||||
};
|
||||
this.loadCategories = this.loadCategories.bind( this );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadCategories();
|
||||
}
|
||||
|
||||
loadCategories() {
|
||||
this.setState( { loading: true } );
|
||||
|
||||
getCategories( {
|
||||
show_review_count: this.props.showReviewCount || false,
|
||||
} )
|
||||
.then( ( categories ) => {
|
||||
this.setState( {
|
||||
categories,
|
||||
loading: false,
|
||||
error: null,
|
||||
} );
|
||||
} )
|
||||
.catch( async ( e ) => {
|
||||
const error = await formatError( e );
|
||||
|
||||
this.setState( {
|
||||
categories: null,
|
||||
loading: false,
|
||||
error,
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error, loading, categories } = this.state;
|
||||
|
||||
return (
|
||||
<OriginalComponent
|
||||
{ ...this.props }
|
||||
error={ error }
|
||||
isLoading={ loading }
|
||||
categories={ categories }
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}, 'withCategories' );
|
||||
|
||||
export default withCategories;
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Component } from '@wordpress/element';
|
||||
import { createHigherOrderComponent } from '@wordpress/compose';
|
||||
import { getCategory } from '@woocommerce/block-components/utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { formatError } from '../base/utils/errors.js';
|
||||
|
||||
const withCategory = createHigherOrderComponent( ( OriginalComponent ) => {
|
||||
return class WrappedComponent extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
this.state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
category:
|
||||
this.props.attributes.categoryId === 'preview'
|
||||
? this.props.attributes.previewCategory
|
||||
: null,
|
||||
};
|
||||
this.loadCategory = this.loadCategory.bind( this );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadCategory();
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
if (
|
||||
prevProps.attributes.categoryId !==
|
||||
this.props.attributes.categoryId
|
||||
) {
|
||||
this.loadCategory();
|
||||
}
|
||||
}
|
||||
|
||||
loadCategory() {
|
||||
const { categoryId } = this.props.attributes;
|
||||
|
||||
if ( categoryId === 'preview' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! categoryId ) {
|
||||
this.setState( {
|
||||
category: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
} );
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState( { loading: true } );
|
||||
|
||||
getCategory( categoryId )
|
||||
.then( ( category ) => {
|
||||
this.setState( { category, loading: false, error: null } );
|
||||
} )
|
||||
.catch( async ( e ) => {
|
||||
const error = await formatError( e );
|
||||
|
||||
this.setState( { category: null, loading: false, error } );
|
||||
} );
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error, loading, category } = this.state;
|
||||
|
||||
return (
|
||||
<OriginalComponent
|
||||
{ ...this.props }
|
||||
error={ error }
|
||||
getCategory={ this.loadCategory }
|
||||
isLoading={ loading }
|
||||
category={ category }
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}, 'withCategory' );
|
||||
|
||||
export default withCategory;
|
||||
@@ -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';
|
||||
import { getProductVariations } from '@woocommerce/block-components/utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
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;
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Component } from '@wordpress/element';
|
||||
import { createHigherOrderComponent } from '@wordpress/compose';
|
||||
import { getProduct } from '@woocommerce/block-components/utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { formatError } from '../base/utils/errors.js';
|
||||
|
||||
const withProduct = createHigherOrderComponent( ( OriginalComponent ) => {
|
||||
return class WrappedComponent extends Component {
|
||||
state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
product:
|
||||
this.props.attributes.productId === 'preview'
|
||||
? this.props.attributes.previewProduct
|
||||
: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.loadProduct();
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
if (
|
||||
prevProps.attributes.productId !==
|
||||
this.props.attributes.productId
|
||||
) {
|
||||
this.loadProduct();
|
||||
}
|
||||
}
|
||||
|
||||
loadProduct = () => {
|
||||
const { productId } = this.props.attributes;
|
||||
|
||||
if ( productId === 'preview' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! productId ) {
|
||||
this.setState( { product: null, loading: false, error: null } );
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState( { loading: true } );
|
||||
|
||||
getProduct( productId )
|
||||
.then( ( product ) => {
|
||||
this.setState( { product, loading: false, error: null } );
|
||||
} )
|
||||
.catch( async ( e ) => {
|
||||
const error = await formatError( e );
|
||||
|
||||
this.setState( { product: null, loading: false, error } );
|
||||
} );
|
||||
};
|
||||
|
||||
render() {
|
||||
const { error, loading, product } = this.state;
|
||||
|
||||
return (
|
||||
<OriginalComponent
|
||||
{ ...this.props }
|
||||
error={ error }
|
||||
getProduct={ this.loadProduct }
|
||||
isLoading={ loading }
|
||||
product={ product }
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}, 'withProduct' );
|
||||
|
||||
export default withProduct;
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useRef } from '@wordpress/element';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { SCHEMA_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Hydrate Rest API data from settings to reduce the number of API requests needed.
|
||||
*/
|
||||
const useRestApiHydration = () => {
|
||||
const restApiRoutes = useRef( getSetting( 'restApiRoutes' ) );
|
||||
|
||||
useSelect( ( select, registry ) => {
|
||||
if ( ! restApiRoutes.current ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { isResolving, hasFinishedResolution } = select(
|
||||
SCHEMA_STORE_KEY
|
||||
);
|
||||
const {
|
||||
receiveRoutes,
|
||||
startResolution,
|
||||
finishResolution,
|
||||
} = registry.dispatch( SCHEMA_STORE_KEY );
|
||||
|
||||
Object.keys( restApiRoutes.current ).forEach( ( namespace ) => {
|
||||
const routes = restApiRoutes.current[ namespace ];
|
||||
if (
|
||||
! isResolving( 'getRoutes', [ namespace ] ) &&
|
||||
! hasFinishedResolution( 'getRoutes', [ namespace ] )
|
||||
) {
|
||||
startResolution( 'getRoutes', [ namespace ] );
|
||||
receiveRoutes( routes, [ namespace ] );
|
||||
finishResolution( 'getRoutes', [ namespace ] );
|
||||
}
|
||||
} );
|
||||
}, [] );
|
||||
};
|
||||
|
||||
/**
|
||||
* HOC that calls the useRestApiHydration hook.
|
||||
*/
|
||||
const withRestApiHydration = ( OriginalComponent ) => {
|
||||
return ( props ) => {
|
||||
useRestApiHydration();
|
||||
return <OriginalComponent { ...props } />;
|
||||
};
|
||||
};
|
||||
|
||||
export default withRestApiHydration;
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Component } from '@wordpress/element';
|
||||
import { debounce } from 'lodash';
|
||||
import { createHigherOrderComponent } from '@wordpress/compose';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IS_LARGE_CATALOG } from '@woocommerce/block-settings';
|
||||
import { getProducts } from '@woocommerce/block-components/utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { formatError } from '../base/utils/errors.js';
|
||||
|
||||
/**
|
||||
* A higher order component that enhances the provided component with products
|
||||
* from a search query.
|
||||
*/
|
||||
const withSearchedProducts = createHigherOrderComponent(
|
||||
( OriginalComponent ) => {
|
||||
/**
|
||||
* A Component wrapping the passed in component.
|
||||
*
|
||||
* @class WrappedComponent
|
||||
* @extends {Component}
|
||||
*/
|
||||
class WrappedComponent extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
this.state = {
|
||||
list: [],
|
||||
loading: true,
|
||||
};
|
||||
this.setError = this.setError.bind( this );
|
||||
this.debouncedOnSearch = debounce(
|
||||
this.onSearch.bind( this ),
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { selected } = this.props;
|
||||
getProducts( { selected } )
|
||||
.then( ( list ) => {
|
||||
this.setState( { list, loading: false } );
|
||||
} )
|
||||
.catch( this.setError );
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.debouncedOnSearch.cancel();
|
||||
}
|
||||
|
||||
onSearch( search ) {
|
||||
const { selected } = this.props;
|
||||
|
||||
getProducts( { selected, search } )
|
||||
.then( ( list ) => {
|
||||
this.setState( { list, loading: false } );
|
||||
} )
|
||||
.catch( this.setError );
|
||||
}
|
||||
|
||||
async setError( e ) {
|
||||
const error = await formatError( e );
|
||||
|
||||
this.setState( { list: [], loading: false, error } );
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error, list, loading } = this.state;
|
||||
|
||||
return (
|
||||
<OriginalComponent
|
||||
{ ...this.props }
|
||||
error={ error }
|
||||
products={ list }
|
||||
isLoading={ loading }
|
||||
onSearch={
|
||||
IS_LARGE_CATALOG
|
||||
? ( search ) => {
|
||||
this.setState( { loading: true } );
|
||||
this.debouncedOnSearch( search );
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
WrappedComponent.propTypes = {
|
||||
selected: PropTypes.array,
|
||||
};
|
||||
WrappedComponent.defaultProps = {
|
||||
selected: [],
|
||||
};
|
||||
return WrappedComponent;
|
||||
},
|
||||
'withSearchedProducts'
|
||||
);
|
||||
|
||||
export default withSearchedProducts;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user