Create withReviews base HOC (https://github.com/woocommerce/woocommerce-blocks/pull/877)
* Create withReviews base HOC * Add tests * Make Reviews by Category use withReviews HOC * Move componentDidUpdate and debounce dependency to decouple HOC from components * Spaces * Rename 'delayMethod' to 'delayFunction' * Refactor withReviews HOC * Update tests * Minor fixes * Undo fix being handled in woocommerce/woocommerce-blocks#884 * Remove hardcoded from withReviews * Update delay comment * Use callbacks instead of announceUpdates prop * Move props check to a 'shouldReplaceReviews' method * Fix productId propType * Move per_page and offset args to 'getArgs' * Update withReviews displayName * Fix tests * Add callback propsTypes * Use is-shallow-equal
This commit is contained in:
parent
0b9559e2db
commit
46934d2946
|
@ -9,7 +9,7 @@ import PropTypes from 'prop-types';
|
|||
*/
|
||||
import './style.scss';
|
||||
|
||||
const ReviewOrderSelect = ( { componentId, onChange, readOnly, value } ) => {
|
||||
const ReviewOrderSelect = ( { componentId, defaultValue, onChange, readOnly, value } ) => {
|
||||
const selectId = `wc-block-review-order-select__select-${ componentId }`;
|
||||
|
||||
return (
|
||||
|
@ -25,6 +25,7 @@ const ReviewOrderSelect = ( { componentId, onChange, readOnly, value } ) => {
|
|||
<select // eslint-disable-line jsx-a11y/no-onchange
|
||||
id={ selectId }
|
||||
className="wc-block-review-order-select__select"
|
||||
defaultValue={ defaultValue }
|
||||
onChange={ onChange }
|
||||
readOnly={ readOnly }
|
||||
value={ value }
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import withReviews from '../with-reviews';
|
||||
import * as mockUtils from '../../../blocks/reviews/utils';
|
||||
|
||||
jest.mock( '../../../blocks/reviews/utils', () => ( {
|
||||
getOrderArgs: () => ( {
|
||||
order: 'desc',
|
||||
orderby: 'date_gmt',
|
||||
} ),
|
||||
getReviews: jest.fn(),
|
||||
} ) );
|
||||
|
||||
const mockReviews = [
|
||||
{ reviewer: 'Alice', review: 'Lorem ipsum', rating: 2 },
|
||||
{ reviewer: 'Bob', review: 'Dolor sit amet', rating: 3 },
|
||||
{ reviewer: 'Carol', review: 'Consectetur adipiscing elit', rating: 5 },
|
||||
];
|
||||
const defaultArgs = {
|
||||
offset: 0,
|
||||
order: 'desc',
|
||||
orderby: 'date_gmt',
|
||||
per_page: 2,
|
||||
product_id: 1,
|
||||
};
|
||||
const TestComponent = withReviews( ( props ) => {
|
||||
return <div
|
||||
error={ props.error }
|
||||
getReviews={ props.getReviews }
|
||||
appendReviews={ props.appendReviews }
|
||||
onChangeArgs={ props.onChangeArgs }
|
||||
isLoading={ props.isLoading }
|
||||
reviews={ props.reviews }
|
||||
totalReviews={ props.totalReviews }
|
||||
/>;
|
||||
} );
|
||||
const render = () => {
|
||||
return TestRenderer.create(
|
||||
<TestComponent
|
||||
order="desc"
|
||||
orderby="date_gmt"
|
||||
productId={ 1 }
|
||||
reviewsToDisplay={ 2 }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe( 'withReviews Component', () => {
|
||||
let renderer;
|
||||
afterEach( () => {
|
||||
mockUtils.getReviews.mockReset();
|
||||
} );
|
||||
|
||||
describe( 'lifecycle events', () => {
|
||||
beforeEach( () => {
|
||||
mockUtils.getReviews.mockImplementationOnce(
|
||||
() => Promise.resolve( { reviews: mockReviews.slice( 0, 2 ), totalReviews: mockReviews.length } )
|
||||
).mockImplementationOnce(
|
||||
() => Promise.resolve( { reviews: mockReviews.slice( 2, 3 ), totalReviews: mockReviews.length } )
|
||||
);
|
||||
renderer = render();
|
||||
} );
|
||||
|
||||
it( 'getReviews is called on mount with default args', () => {
|
||||
const { getReviews } = mockUtils;
|
||||
|
||||
expect( getReviews ).toHaveBeenCalledWith( defaultArgs );
|
||||
expect( getReviews ).toHaveBeenCalledTimes( 1 );
|
||||
} );
|
||||
|
||||
it( 'getReviews is called on component update', () => {
|
||||
const { getReviews } = mockUtils;
|
||||
renderer.update(
|
||||
<TestComponent
|
||||
order="desc"
|
||||
orderby="date_gmt"
|
||||
productId={ 1 }
|
||||
reviewsToDisplay={ 3 }
|
||||
/>
|
||||
);
|
||||
|
||||
expect( getReviews ).toHaveBeenNthCalledWith( 2, { ...defaultArgs, offset: 2, per_page: 1 } );
|
||||
expect( getReviews ).toHaveBeenCalledTimes( 2 );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'when the API returns product data', () => {
|
||||
beforeEach( () => {
|
||||
mockUtils.getReviews.mockImplementation(
|
||||
() => Promise.resolve( { reviews: mockReviews.slice( 0, 2 ), totalReviews: mockReviews.length } )
|
||||
);
|
||||
renderer = render();
|
||||
} );
|
||||
|
||||
it( 'sets reviews based on API response', () => {
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
|
||||
expect( props.error ).toBeNull();
|
||||
expect( props.isLoading ).toBe( false );
|
||||
expect( props.reviews ).toEqual( mockReviews.slice( 0, 2 ) );
|
||||
expect( props.totalReviews ).toEqual( mockReviews.length );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'when the API returns an error', () => {
|
||||
beforeEach( () => {
|
||||
mockUtils.getReviews.mockImplementation(
|
||||
() => Promise.reject( { message: 'There was an error.' } )
|
||||
);
|
||||
renderer = render();
|
||||
} );
|
||||
|
||||
it( 'sets the error prop', () => {
|
||||
const props = renderer.root.findByType( 'div' ).props;
|
||||
|
||||
expect( props.error ).toEqual( { apiMessage: 'There was an error.' } );
|
||||
expect( props.isLoading ).toBe( false );
|
||||
expect( props.reviews ).toEqual( [] );
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getReviews } from '../../blocks/reviews/utils';
|
||||
|
||||
const withReviews = ( OriginalComponent ) => {
|
||||
class WrappedComponent extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
|
||||
this.state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
reviews: [],
|
||||
totalReviews: 0,
|
||||
};
|
||||
|
||||
this.setError = this.setError.bind( this );
|
||||
this.delayedAppendReviews = this.props.delayFunction( this.appendReviews );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.replaceReviews();
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
if ( prevProps.reviewsToDisplay < this.props.reviewsToDisplay ) {
|
||||
// Since this attribute might be controlled via something with
|
||||
// short intervals between value changes, this allows for optionally
|
||||
// delaying review fetches via the provided delay function.
|
||||
this.delayedAppendReviews();
|
||||
} else if (
|
||||
this.shouldReplaceReviews( prevProps, this.props )
|
||||
) {
|
||||
this.replaceReviews();
|
||||
}
|
||||
}
|
||||
|
||||
shouldReplaceReviews( prevProps, nextProps ) {
|
||||
return (
|
||||
prevProps.orderby !== nextProps.orderby ||
|
||||
prevProps.order !== nextProps.order ||
|
||||
prevProps.productId !== nextProps.productId ||
|
||||
! isShallowEqual( prevProps.categoryIds, nextProps.categoryIds )
|
||||
);
|
||||
}
|
||||
|
||||
getArgs( reviewsToSkip ) {
|
||||
const { categoryIds, order, orderby, productId, reviewsToDisplay } = this.props;
|
||||
const args = {
|
||||
order,
|
||||
orderby,
|
||||
per_page: reviewsToDisplay - reviewsToSkip,
|
||||
offset: reviewsToSkip,
|
||||
};
|
||||
|
||||
if ( categoryIds && categoryIds.length ) {
|
||||
args.category_id = Array.isArray( categoryIds ) ? categoryIds.join( ',' ) : categoryIds;
|
||||
}
|
||||
|
||||
if ( productId ) {
|
||||
args.product_id = productId;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
replaceReviews() {
|
||||
const { onReviewsReplaced } = this.props;
|
||||
|
||||
this.updateListOfReviews().then( onReviewsReplaced );
|
||||
}
|
||||
|
||||
appendReviews() {
|
||||
const { onReviewsAppended, reviewsToDisplay } = this.props;
|
||||
const { reviews } = this.state;
|
||||
|
||||
// Given that this function is delayed, props might have been updated since
|
||||
// it was called so we need to check again if fetching new reviews is necessary.
|
||||
if ( reviewsToDisplay <= reviews.length ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateListOfReviews( reviews ).then( onReviewsAppended );
|
||||
}
|
||||
|
||||
updateListOfReviews( oldReviews = [] ) {
|
||||
const { reviewsToDisplay } = this.props;
|
||||
const { totalReviews } = this.state;
|
||||
const reviewsToLoad = Math.min( totalReviews, reviewsToDisplay ) - oldReviews.length;
|
||||
|
||||
this.setState( {
|
||||
loading: true,
|
||||
reviews: oldReviews.concat( Array( reviewsToLoad ).fill( {} ) ),
|
||||
} );
|
||||
|
||||
return getReviews( this.getArgs( oldReviews.length ) )
|
||||
.then( ( { reviews: newReviews, totalReviews: newTotalReviews } ) => {
|
||||
this.setState( {
|
||||
reviews: oldReviews.filter( ( review ) => Object.keys( review ).length ).concat( newReviews ),
|
||||
totalReviews: newTotalReviews,
|
||||
loading: false,
|
||||
error: null,
|
||||
} );
|
||||
|
||||
return { newReviews };
|
||||
} )
|
||||
.catch( this.setError );
|
||||
}
|
||||
|
||||
setError( apiError ) {
|
||||
const { onReviewsLoadError } = this.props;
|
||||
const error = typeof apiError === 'object' && apiError.hasOwnProperty( 'message' ) ? {
|
||||
apiMessage: apiError.message,
|
||||
} : {
|
||||
apiMessage: null,
|
||||
};
|
||||
|
||||
this.setState( { reviews: [], loading: false, error } );
|
||||
|
||||
onReviewsLoadError();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { reviewsToDisplay } = this.props;
|
||||
const { error, loading, reviews, totalReviews } = this.state;
|
||||
|
||||
return <OriginalComponent
|
||||
{ ...this.props }
|
||||
error={ error }
|
||||
isLoading={ loading }
|
||||
reviews={ reviews.slice( 0, reviewsToDisplay ) }
|
||||
totalReviews={ totalReviews }
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
WrappedComponent.propTypes = {
|
||||
order: PropTypes.oneOf( [ 'asc', 'desc' ] ).isRequired,
|
||||
orderby: PropTypes.string.isRequired,
|
||||
reviewsToDisplay: PropTypes.number.isRequired,
|
||||
categoryIds: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array ] ),
|
||||
delayFunction: PropTypes.func,
|
||||
onReviewsAppended: PropTypes.func,
|
||||
onReviewsLoadError: PropTypes.func,
|
||||
onReviewsReplaced: PropTypes.func,
|
||||
productId: PropTypes.oneOfType( [ PropTypes.string, PropTypes.number ] ),
|
||||
};
|
||||
|
||||
WrappedComponent.defaultProps = {
|
||||
delayFunction: ( f ) => f,
|
||||
onReviewsAppended: () => {},
|
||||
onReviewsLoadError: () => {},
|
||||
onReviewsReplaced: () => {},
|
||||
};
|
||||
|
||||
const { displayName = OriginalComponent.name || 'Component' } = OriginalComponent;
|
||||
WrappedComponent.displayName = `WithReviews( ${ displayName } )`;
|
||||
|
||||
return WrappedComponent;
|
||||
};
|
||||
|
||||
export default withReviews;
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import LoadMoreButton from '../../base/components/load-more-button';
|
||||
import ReviewOrderSelect from '../../base/components/review-order-select';
|
||||
import ReviewList from '../../base/components/review-list';
|
||||
import withComponentId from '../../base/hocs/with-component-id';
|
||||
import withReviews from '../../base/hocs/with-reviews';
|
||||
import { ENABLE_REVIEW_RATING } from '../../constants';
|
||||
|
||||
/**
|
||||
* Block rendered in the frontend.
|
||||
*/
|
||||
const FrontendBlock = ( { attributes, componentId, onAppendReviews, onChangeOrderby, reviews, totalReviews } ) => {
|
||||
const { orderby } = attributes;
|
||||
|
||||
if ( 0 === reviews.length ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{ ( attributes.showOrderby && ENABLE_REVIEW_RATING ) && (
|
||||
<ReviewOrderSelect
|
||||
componentId={ componentId }
|
||||
defaultValue={ orderby }
|
||||
onChange={ onChangeOrderby }
|
||||
/>
|
||||
) }
|
||||
<ReviewList
|
||||
attributes={ attributes }
|
||||
componentId={ componentId }
|
||||
reviews={ reviews }
|
||||
/>
|
||||
{ ( attributes.showLoadMore && totalReviews > reviews.length ) && (
|
||||
<LoadMoreButton
|
||||
onClick={ onAppendReviews }
|
||||
screenReaderLabel={ __( 'Load more reviews', 'woo-gutenberg-products-block' ) }
|
||||
/>
|
||||
) }
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
FrontendBlock.propTypes = {
|
||||
/**
|
||||
* The attributes for this block.
|
||||
*/
|
||||
attributes: PropTypes.object.isRequired,
|
||||
onAppendReviews: PropTypes.func,
|
||||
onChangeArgs: PropTypes.func,
|
||||
// from withComponentId
|
||||
componentId: PropTypes.number,
|
||||
// from withReviewsattributes
|
||||
reviews: PropTypes.array,
|
||||
totalReviews: PropTypes.number,
|
||||
};
|
||||
|
||||
export default withComponentId( withReviews( FrontendBlock ) );
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import { speak } from '@wordpress/a11y';
|
||||
import { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getOrderArgs } from './utils';
|
||||
import FrontendBlock from './frontend-block';
|
||||
|
||||
/**
|
||||
* Container of the block rendered in the frontend.
|
||||
*/
|
||||
class FrontendContainerBlock extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
const { attributes } = this.props;
|
||||
|
||||
this.state = {
|
||||
orderby: attributes.orderby,
|
||||
reviewsToDisplay: parseInt( attributes.reviewsOnPageLoad, 10 ),
|
||||
};
|
||||
|
||||
this.onAppendReviews = this.onAppendReviews.bind( this );
|
||||
this.onChangeOrderby = this.onChangeOrderby.bind( this );
|
||||
}
|
||||
|
||||
onAppendReviews() {
|
||||
const { attributes } = this.props;
|
||||
const { reviewsToDisplay } = this.state;
|
||||
|
||||
this.setState( {
|
||||
reviewsToDisplay: reviewsToDisplay + parseInt( attributes.reviewsOnLoadMore, 10 ),
|
||||
} );
|
||||
}
|
||||
|
||||
onChangeOrderby( event ) {
|
||||
const { attributes } = this.props;
|
||||
|
||||
this.setState( {
|
||||
orderby: event.target.value,
|
||||
reviewsToDisplay: parseInt( attributes.reviewsOnPageLoad, 10 ),
|
||||
} );
|
||||
}
|
||||
|
||||
onReviewsAppended( { newReviews } ) {
|
||||
speak(
|
||||
sprintf(
|
||||
_n(
|
||||
'%d review loaded.',
|
||||
'%d reviews loaded.',
|
||||
newReviews.length,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
newReviews.length,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
onReviewsReplaced() {
|
||||
speak( __( 'Reviews list updated.', 'woo-gutenberg-products-block' ) );
|
||||
}
|
||||
|
||||
onReviewsLoadError() {
|
||||
speak( __( 'There was an error loading the reviews.', 'woo-gutenberg-products-block' ) );
|
||||
}
|
||||
|
||||
render() {
|
||||
const { attributes } = this.props;
|
||||
const { categoryIds, productId } = attributes;
|
||||
const { reviewsToDisplay } = this.state;
|
||||
const { order, orderby } = getOrderArgs( this.state.orderby );
|
||||
|
||||
return (
|
||||
<FrontendBlock
|
||||
attributes={ attributes }
|
||||
categoryIds={ categoryIds }
|
||||
onAppendReviews={ this.onAppendReviews }
|
||||
onChangeOrderby={ this.onChangeOrderby }
|
||||
onReviewsAppended={ this.onReviewsAppended }
|
||||
onReviewsLoadError={ this.onReviewsLoadError }
|
||||
onReviewsReplaced={ this.onReviewsReplaced }
|
||||
order={ order }
|
||||
orderby={ orderby }
|
||||
productId={ productId }
|
||||
reviewsToDisplay={ reviewsToDisplay }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FrontendContainerBlock.propTypes = {
|
||||
/**
|
||||
* The attributes for this block.
|
||||
*/
|
||||
attributes: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default FrontendContainerBlock;
|
|
@ -18,6 +18,7 @@ import { SearchListItem } from '@woocommerce/components';
|
|||
import { Fragment } from '@wordpress/element';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import PropTypes from 'prop-types';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -26,7 +27,7 @@ import EditorBlock from './editor-block.js';
|
|||
import ProductCategoryControl from '../../../components/product-category-control';
|
||||
import { IconReviewsByCategory } from '../../../components/icons';
|
||||
import { getSharedReviewContentControls, getSharedReviewListControls } from '../edit.js';
|
||||
import { getBlockClassName } from '../utils.js';
|
||||
import { getBlockClassName, getOrderArgs } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Component to handle edit mode of "Reviews by Category".
|
||||
|
@ -167,9 +168,19 @@ const ReviewsByCategoryEditor = ( { attributes, debouncedSpeak, setAttributes }
|
|||
return renderHiddenContentPlaceholder();
|
||||
}
|
||||
|
||||
const { reviewsOnPageLoad } = attributes;
|
||||
const { order, orderby } = getOrderArgs( attributes.orderby );
|
||||
|
||||
return (
|
||||
<div className={ getBlockClassName( 'wc-block-reviews-by-category', attributes ) }>
|
||||
<EditorBlock attributes={ attributes } />
|
||||
<EditorBlock
|
||||
attributes={ attributes }
|
||||
categoryIds={ categoryIds }
|
||||
delayFunction={ ( callback ) => debounce( callback, 400 ) }
|
||||
orderby={ orderby }
|
||||
order={ order }
|
||||
reviewsToDisplay={ reviewsOnPageLoad }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,17 +4,16 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { debounce } from 'lodash';
|
||||
import { Disabled, Placeholder } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getOrderArgs, getReviews } from '../utils';
|
||||
import LoadMoreButton from '../../../base/components/load-more-button';
|
||||
import ReviewList from '../../../base/components/review-list';
|
||||
import ReviewOrderSelect from '../../../base/components/review-order-select';
|
||||
import withComponentId from '../../../base/hocs/with-component-id';
|
||||
import withReviews from '../../../base/hocs/with-reviews';
|
||||
import { IconReviewsByCategory } from '../../../components/icons';
|
||||
import { ENABLE_REVIEW_RATING } from '../../../constants';
|
||||
|
||||
|
@ -22,54 +21,6 @@ import { ENABLE_REVIEW_RATING } from '../../../constants';
|
|||
* Block rendered in the editor.
|
||||
*/
|
||||
class EditorBlock extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
|
||||
this.state = {
|
||||
reviews: [],
|
||||
totalReviews: 0,
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
this.renderNoReviews = this.renderNoReviews.bind( this );
|
||||
this.debouncedLoadFirstReviews = debounce( this.loadFirstReviews.bind( this ), 400 );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadFirstReviews();
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
if (
|
||||
prevProps.attributes.orderby !== this.props.attributes.orderby ||
|
||||
prevProps.attributes.categoryIds !== this.props.attributes.categoryIds ||
|
||||
prevProps.attributes.reviewsOnPageLoad !== this.props.attributes.reviewsOnPageLoad
|
||||
) {
|
||||
this.debouncedLoadFirstReviews();
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultArgs() {
|
||||
const { attributes } = this.props;
|
||||
const { order, orderby } = getOrderArgs( attributes.orderby );
|
||||
const { categoryIds, reviewsOnPageLoad } = attributes;
|
||||
|
||||
return {
|
||||
order,
|
||||
orderby,
|
||||
per_page: reviewsOnPageLoad,
|
||||
category_id: categoryIds.join( ',' ),
|
||||
};
|
||||
}
|
||||
|
||||
loadFirstReviews() {
|
||||
getReviews( this.getDefaultArgs() ).then( ( { reviews, totalReviews } ) => {
|
||||
this.setState( { reviews, totalReviews, isLoading: false } );
|
||||
} ).catch( () => {
|
||||
this.setState( { reviews: [], isLoading: false } );
|
||||
} );
|
||||
}
|
||||
|
||||
renderNoReviews() {
|
||||
return (
|
||||
<Placeholder
|
||||
|
@ -83,8 +34,7 @@ class EditorBlock extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { attributes, componentId } = this.props;
|
||||
const { reviews, totalReviews, isLoading } = this.state;
|
||||
const { attributes, componentId, isLoading, reviews, totalReviews } = this.props;
|
||||
|
||||
if ( 0 === reviews.length && ! isLoading ) {
|
||||
return this.renderNoReviews();
|
||||
|
@ -123,6 +73,9 @@ EditorBlock.propTypes = {
|
|||
* From withComponentId.
|
||||
*/
|
||||
componentId: PropTypes.number,
|
||||
// from withReviews
|
||||
reviews: PropTypes.array,
|
||||
totalReviews: PropTypes.number,
|
||||
};
|
||||
|
||||
export default withComponentId( EditorBlock );
|
||||
export default withComponentId( withReviews( EditorBlock ) );
|
||||
|
|
|
@ -1,172 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { speak } from '@wordpress/a11y';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getOrderArgs, getReviews } from '../utils';
|
||||
import LoadMoreButton from '../../../base/components/load-more-button';
|
||||
import ReviewOrderSelect from '../../../base/components/review-order-select';
|
||||
import ReviewList from '../../../base/components/review-list';
|
||||
import withComponentId from '../../../base/hocs/with-component-id';
|
||||
import { ENABLE_REVIEW_RATING } from '../../../constants';
|
||||
|
||||
/**
|
||||
* Block rendered in the frontend.
|
||||
*/
|
||||
class FrontendBlock extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
const { attributes } = this.props;
|
||||
|
||||
this.state = {
|
||||
orderby: attributes.orderby,
|
||||
reviews: [],
|
||||
totalReviews: 0,
|
||||
};
|
||||
|
||||
this.onChangeOrderby = this.onChangeOrderby.bind( this );
|
||||
this.appendReviews = this.appendReviews.bind( this );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadFirstReviews();
|
||||
}
|
||||
|
||||
getDefaultArgs() {
|
||||
const { attributes } = this.props;
|
||||
const { order, orderby } = getOrderArgs( this.state.orderby );
|
||||
const { categoryIds, reviewsOnPageLoad } = attributes;
|
||||
|
||||
return {
|
||||
order,
|
||||
orderby,
|
||||
per_page: reviewsOnPageLoad,
|
||||
category_id: categoryIds,
|
||||
};
|
||||
}
|
||||
|
||||
loadFirstReviews() {
|
||||
getReviews( this.getDefaultArgs() ).then( ( { reviews, totalReviews } ) => {
|
||||
this.setState( { reviews, totalReviews } );
|
||||
} ).catch( () => {
|
||||
this.setState( { reviews: [] } );
|
||||
speak(
|
||||
__( 'There was an error loading the reviews.', 'woo-gutenberg-products-block' )
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
appendReviews() {
|
||||
const { attributes } = this.props;
|
||||
const { reviewsOnLoadMore } = attributes;
|
||||
const { reviews, totalReviews } = this.state;
|
||||
|
||||
const reviewsToLoad = Math.min( totalReviews - reviews.length, reviewsOnLoadMore );
|
||||
this.setState( { reviews: reviews.concat( Array( reviewsToLoad ).fill( {} ) ) } );
|
||||
|
||||
const args = {
|
||||
...this.getDefaultArgs(),
|
||||
offset: reviews.length,
|
||||
per_page: reviewsOnLoadMore,
|
||||
};
|
||||
getReviews( args ).then( ( { reviews: newReviews, totalReviews: newTotalReviews } ) => {
|
||||
this.setState( {
|
||||
reviews: reviews.filter( ( review ) => Object.keys( review ).length ).concat( newReviews ),
|
||||
totalReviews: newTotalReviews,
|
||||
} );
|
||||
speak(
|
||||
sprintf(
|
||||
_n(
|
||||
'%d review loaded.',
|
||||
'%d reviews loaded.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
newReviews.length
|
||||
)
|
||||
);
|
||||
} ).catch( () => {
|
||||
this.setState( { reviews: [] } );
|
||||
speak(
|
||||
__( 'There was an error loading the reviews.', 'woo-gutenberg-products-block' )
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
onChangeOrderby( event ) {
|
||||
const { attributes } = this.props;
|
||||
const { reviewsOnPageLoad } = attributes;
|
||||
const { totalReviews } = this.state;
|
||||
const { order, orderby } = getOrderArgs( event.target.value );
|
||||
const newReviews = Math.min( totalReviews, reviewsOnPageLoad );
|
||||
|
||||
this.setState( {
|
||||
reviews: Array( newReviews ).fill( {} ),
|
||||
orderby: event.target.value,
|
||||
} );
|
||||
|
||||
const args = {
|
||||
...this.getDefaultArgs(),
|
||||
order,
|
||||
orderby,
|
||||
per_page: reviewsOnPageLoad,
|
||||
};
|
||||
getReviews( args ).then( ( { reviews, totalReviews: newTotalReviews } ) => {
|
||||
this.setState( { reviews, totalReviews: newTotalReviews } );
|
||||
speak( __( 'Reviews order updated.', 'woo-gutenberg-products-block' ) );
|
||||
} ).catch( () => {
|
||||
this.setState( { reviews: [] } );
|
||||
speak(
|
||||
__( 'There was an error loading the reviews.', 'woo-gutenberg-products-block' )
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
render() {
|
||||
const { attributes, componentId } = this.props;
|
||||
const { orderby, reviews, totalReviews } = this.state;
|
||||
|
||||
if ( 0 === reviews.length ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{ ( attributes.showOrderby && ENABLE_REVIEW_RATING ) && (
|
||||
<ReviewOrderSelect
|
||||
componentId={ componentId }
|
||||
onChange={ this.onChangeOrderby }
|
||||
value={ orderby }
|
||||
/>
|
||||
) }
|
||||
<ReviewList
|
||||
attributes={ attributes }
|
||||
componentId={ componentId }
|
||||
reviews={ reviews }
|
||||
/>
|
||||
{ ( attributes.showLoadMore && totalReviews > reviews.length ) && (
|
||||
<LoadMoreButton
|
||||
onClick={ this.appendReviews }
|
||||
screenReaderLabel={ __( 'Load more reviews', 'woo-gutenberg-products-block' ) }
|
||||
/>
|
||||
) }
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FrontendBlock.propTypes = {
|
||||
/**
|
||||
* The attributes for this block.
|
||||
*/
|
||||
attributes: PropTypes.object.isRequired,
|
||||
// from withComponentId
|
||||
componentId: PropTypes.number,
|
||||
};
|
||||
|
||||
export default withComponentId( FrontendBlock );
|
|
@ -6,7 +6,7 @@ import { render } from 'react-dom';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import FrontendBlock from './frontend-block.js';
|
||||
import FrontendContainerBlock from '../frontend-container-block.js';
|
||||
|
||||
const containers = document.querySelectorAll(
|
||||
'.wp-block-woocommerce-reviews-by-category'
|
||||
|
@ -25,6 +25,6 @@ if ( containers.length ) {
|
|||
showProductName: el.classList.contains( 'has-product-name' ),
|
||||
};
|
||||
|
||||
render( <FrontendBlock attributes={ attributes } />, el );
|
||||
render( <FrontendContainerBlock attributes={ attributes } />, el );
|
||||
} );
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { SearchListItem } from '@woocommerce/components';
|
||||
import { Fragment } from '@wordpress/element';
|
||||
import PropTypes from 'prop-types';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -24,7 +25,7 @@ import EditorBlock from './editor-block.js';
|
|||
import ProductControl from '../../../components/product-control';
|
||||
import { IconReviewsByProduct } from '../../../components/icons';
|
||||
import { getSharedReviewContentControls, getSharedReviewListControls } from '../edit.js';
|
||||
import { getBlockClassName } from '../utils.js';
|
||||
import { getBlockClassName, getOrderArgs } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Component to handle edit mode of "Reviews by Product".
|
||||
|
@ -160,9 +161,19 @@ const ReviewsByProductEditor = ( { attributes, debouncedSpeak, setAttributes } )
|
|||
return renderHiddenContentPlaceholder();
|
||||
}
|
||||
|
||||
const { reviewsOnPageLoad } = attributes;
|
||||
const { order, orderby } = getOrderArgs( attributes.orderby );
|
||||
|
||||
return (
|
||||
<div className={ getBlockClassName( 'wc-block-reviews-by-product', attributes ) }>
|
||||
<EditorBlock attributes={ attributes } />
|
||||
<EditorBlock
|
||||
attributes={ attributes }
|
||||
delayFunction={ ( callback ) => debounce( callback, 400 ) }
|
||||
orderby={ orderby }
|
||||
order={ order }
|
||||
productId={ productId }
|
||||
reviewsToDisplay={ reviewsOnPageLoad }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,76 +1,52 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { debounce } from 'lodash';
|
||||
import { Disabled } from '@wordpress/components';
|
||||
import { Disabled, Placeholder } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getOrderArgs, getReviews } from '../utils';
|
||||
import LoadMoreButton from '../../../base/components/load-more-button';
|
||||
import ReviewList from '../../../base/components/review-list';
|
||||
import ReviewOrderSelect from '../../../base/components/review-order-select';
|
||||
import withComponentId from '../../../base/hocs/with-component-id';
|
||||
import withReviews from '../../../base/hocs/with-reviews';
|
||||
import { IconReviewsByProduct } from '../../../components/icons';
|
||||
import NoReviewsPlaceholder from './no-reviews-placeholder.js';
|
||||
import { ENABLE_REVIEW_RATING } from '../../../constants';
|
||||
import { escapeHTML } from '@wordpress/escape-html';
|
||||
|
||||
/**
|
||||
* Block rendered in the editor.
|
||||
*/
|
||||
class EditorBlock extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
|
||||
this.state = {
|
||||
reviews: [],
|
||||
totalReviews: 0,
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
this.debouncedLoadFirstReviews = debounce( this.loadFirstReviews.bind( this ), 400 );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadFirstReviews();
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
if (
|
||||
prevProps.attributes.orderby !== this.props.attributes.orderby ||
|
||||
prevProps.attributes.productId !== this.props.attributes.productId ||
|
||||
prevProps.attributes.reviewsOnPageLoad !== this.props.attributes.reviewsOnPageLoad
|
||||
) {
|
||||
this.debouncedLoadFirstReviews();
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultArgs() {
|
||||
renderNoReviews() {
|
||||
const { attributes } = this.props;
|
||||
const { order, orderby } = getOrderArgs( attributes.orderby );
|
||||
const { productId, reviewsOnPageLoad } = attributes;
|
||||
|
||||
return {
|
||||
order,
|
||||
orderby,
|
||||
per_page: reviewsOnPageLoad,
|
||||
product_id: productId,
|
||||
};
|
||||
}
|
||||
|
||||
loadFirstReviews() {
|
||||
getReviews( this.getDefaultArgs() ).then( ( { reviews, totalReviews } ) => {
|
||||
this.setState( { reviews, totalReviews, isLoading: false } );
|
||||
} ).catch( () => {
|
||||
this.setState( { reviews: [], isLoading: false } );
|
||||
} );
|
||||
const { product } = attributes;
|
||||
return (
|
||||
<Placeholder
|
||||
className="wc-block-reviews-by-product"
|
||||
icon={ <IconReviewsByProduct className="block-editor-block-icon" /> }
|
||||
label={ __( 'Reviews by Product', 'woo-gutenberg-products-block' ) }
|
||||
>
|
||||
<div dangerouslySetInnerHTML={ {
|
||||
__html: sprintf(
|
||||
__(
|
||||
"This block lists reviews for a selected product. %s doesn't have any reviews yet, but they will show up here when it does.",
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
'<strong>' + escapeHTML( product.name ) + '</strong>'
|
||||
),
|
||||
} } />
|
||||
</Placeholder>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { attributes, componentId } = this.props;
|
||||
const { reviews, totalReviews, isLoading } = this.state;
|
||||
const { attributes, componentId, isLoading, reviews, totalReviews } = this.props;
|
||||
|
||||
if ( 0 === reviews.length && ! isLoading ) {
|
||||
return <NoReviewsPlaceholder attributes={ attributes } />;
|
||||
|
@ -107,6 +83,9 @@ EditorBlock.propTypes = {
|
|||
attributes: PropTypes.object.isRequired,
|
||||
// from withComponentId
|
||||
componentId: PropTypes.number,
|
||||
// from withReviews
|
||||
reviews: PropTypes.array,
|
||||
totalReviews: PropTypes.number,
|
||||
};
|
||||
|
||||
export default withComponentId( EditorBlock );
|
||||
export default withComponentId( withReviews( EditorBlock ) );
|
||||
|
|
|
@ -1,172 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { speak } from '@wordpress/a11y';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getOrderArgs, getReviews } from '../utils';
|
||||
import LoadMoreButton from '../../../base/components/load-more-button';
|
||||
import ReviewOrderSelect from '../../../base/components/review-order-select';
|
||||
import ReviewList from '../../../base/components/review-list';
|
||||
import withComponentId from '../../../base/hocs/with-component-id';
|
||||
import { ENABLE_REVIEW_RATING } from '../../../constants';
|
||||
|
||||
/**
|
||||
* Block rendered in the frontend.
|
||||
*/
|
||||
class FrontendBlock extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
const { attributes } = this.props;
|
||||
|
||||
this.state = {
|
||||
orderby: attributes.orderby,
|
||||
reviews: [],
|
||||
totalReviews: 0,
|
||||
};
|
||||
|
||||
this.onChangeOrderby = this.onChangeOrderby.bind( this );
|
||||
this.appendReviews = this.appendReviews.bind( this );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadFirstReviews();
|
||||
}
|
||||
|
||||
getDefaultArgs() {
|
||||
const { attributes } = this.props;
|
||||
const { order, orderby } = getOrderArgs( this.state.orderby );
|
||||
const { productId, reviewsOnPageLoad } = attributes;
|
||||
|
||||
return {
|
||||
order,
|
||||
orderby,
|
||||
per_page: reviewsOnPageLoad,
|
||||
product_id: productId,
|
||||
};
|
||||
}
|
||||
|
||||
loadFirstReviews() {
|
||||
getReviews( this.getDefaultArgs() ).then( ( { reviews, totalReviews } ) => {
|
||||
this.setState( { reviews, totalReviews } );
|
||||
} ).catch( () => {
|
||||
this.setState( { reviews: [] } );
|
||||
speak(
|
||||
__( 'There was an error loading the reviews.', 'woo-gutenberg-products-block' )
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
appendReviews() {
|
||||
const { attributes } = this.props;
|
||||
const { reviewsOnLoadMore } = attributes;
|
||||
const { reviews, totalReviews } = this.state;
|
||||
|
||||
const reviewsToLoad = Math.min( totalReviews - reviews.length, reviewsOnLoadMore );
|
||||
this.setState( { reviews: reviews.concat( Array( reviewsToLoad ).fill( {} ) ) } );
|
||||
|
||||
const args = {
|
||||
...this.getDefaultArgs(),
|
||||
offset: reviews.length,
|
||||
per_page: reviewsOnLoadMore,
|
||||
};
|
||||
getReviews( args ).then( ( { reviews: newReviews, totalReviews: newTotalReviews } ) => {
|
||||
this.setState( {
|
||||
reviews: reviews.filter( ( review ) => Object.keys( review ).length ).concat( newReviews ),
|
||||
totalReviews: newTotalReviews,
|
||||
} );
|
||||
speak(
|
||||
sprintf(
|
||||
_n(
|
||||
'%d review loaded.',
|
||||
'%d reviews loaded.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
newReviews.length
|
||||
)
|
||||
);
|
||||
} ).catch( () => {
|
||||
this.setState( { reviews: [] } );
|
||||
speak(
|
||||
__( 'There was an error loading the reviews.', 'woo-gutenberg-products-block' )
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
onChangeOrderby( event ) {
|
||||
const { attributes } = this.props;
|
||||
const { reviewsOnPageLoad } = attributes;
|
||||
const { totalReviews } = this.state;
|
||||
const { order, orderby } = getOrderArgs( event.target.value );
|
||||
const newReviews = Math.min( totalReviews, reviewsOnPageLoad );
|
||||
|
||||
this.setState( {
|
||||
reviews: Array( newReviews ).fill( {} ),
|
||||
orderby: event.target.value,
|
||||
} );
|
||||
|
||||
const args = {
|
||||
...this.getDefaultArgs(),
|
||||
order,
|
||||
orderby,
|
||||
per_page: reviewsOnPageLoad,
|
||||
};
|
||||
getReviews( args ).then( ( { reviews, totalReviews: newTotalReviews } ) => {
|
||||
this.setState( { reviews, totalReviews: newTotalReviews } );
|
||||
speak( __( 'Reviews order updated.', 'woo-gutenberg-products-block' ) );
|
||||
} ).catch( () => {
|
||||
this.setState( { reviews: [] } );
|
||||
speak(
|
||||
__( 'There was an error loading the reviews.', 'woo-gutenberg-products-block' )
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
render() {
|
||||
const { attributes, componentId } = this.props;
|
||||
const { orderby, reviews, totalReviews } = this.state;
|
||||
|
||||
if ( 0 === reviews.length ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{ ( attributes.showOrderby && ENABLE_REVIEW_RATING ) && (
|
||||
<ReviewOrderSelect
|
||||
componentId={ componentId }
|
||||
onChange={ this.onChangeOrderby }
|
||||
value={ orderby }
|
||||
/>
|
||||
) }
|
||||
<ReviewList
|
||||
attributes={ attributes }
|
||||
componentId={ componentId }
|
||||
reviews={ reviews }
|
||||
/>
|
||||
{ ( attributes.showLoadMore && totalReviews > reviews.length ) && (
|
||||
<LoadMoreButton
|
||||
onClick={ this.appendReviews }
|
||||
screenReaderLabel={ __( 'Load more reviews', 'woo-gutenberg-products-block' ) }
|
||||
/>
|
||||
) }
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FrontendBlock.propTypes = {
|
||||
/**
|
||||
* The attributes for this block.
|
||||
*/
|
||||
attributes: PropTypes.object.isRequired,
|
||||
// from withComponentId
|
||||
componentId: PropTypes.number,
|
||||
};
|
||||
|
||||
export default withComponentId( FrontendBlock );
|
|
@ -6,7 +6,7 @@ import { render } from 'react-dom';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import FrontendBlock from './frontend-block.js';
|
||||
import FrontendContainerBlock from '../frontend-container-block.js';
|
||||
|
||||
const containers = document.querySelectorAll(
|
||||
'.wp-block-woocommerce-reviews-by-product'
|
||||
|
@ -24,6 +24,6 @@ if ( containers.length ) {
|
|||
showReviewContent: el.classList.contains( 'has-content' ),
|
||||
};
|
||||
|
||||
render( <FrontendBlock attributes={ attributes } />, el );
|
||||
render( <FrontendContainerBlock attributes={ attributes } />, el );
|
||||
} );
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
"@babel/core": "7.5.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||
"@octokit/rest": "16.28.7",
|
||||
"@woocommerce/navigation": "2.1.1",
|
||||
"@woocommerce/navigation": "2.1.1",
|
||||
"@wordpress/babel-preset-default": "4.4.0",
|
||||
"@wordpress/blocks": "6.5.0",
|
||||
"@wordpress/browserslist-config": "2.6.0",
|
||||
|
@ -50,6 +50,7 @@
|
|||
"@wordpress/editor": "9.5.0",
|
||||
"@wordpress/element": "2.6.0",
|
||||
"@wordpress/i18n": "3.6.0",
|
||||
"@wordpress/is-shallow-equal": "^1.5.0",
|
||||
"@wordpress/jest-preset-default": "4.3.0",
|
||||
"@wordpress/rich-text": "3.5.0",
|
||||
"@wordpress/scripts": "3.4.0",
|
||||
|
|
Loading…
Reference in New Issue