/** * External dependencies */ import { Component } from '@wordpress/element'; import PropTypes from 'prop-types'; import isShallowEqual from '@wordpress/is-shallow-equal'; /** * Internal dependencies */ import { getReviews } from '../../blocks/reviews/utils'; import { formatError } from '../utils/errors'; /** * HOC that queries reviews for a component. * * @param {Function} OriginalComponent Component being wrapped. */ const withReviews = ( OriginalComponent ) => { class WrappedComponent extends Component { static 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, ] ), }; static defaultProps = { delayFunction: ( f ) => f, onReviewsAppended: () => {}, onReviewsLoadError: () => {}, onReviewsReplaced: () => {}, }; isPreview = !! this.props.attributes.previewReviews; delayedAppendReviews = this.props.delayFunction( this.appendReviews ); isMounted = false; state = { error: null, loading: true, reviews: this.isPreview ? this.props.attributes.previewReviews : [], totalReviews: this.isPreview ? this.props.attributes.previewReviews.length : 0, }; componentDidMount() { this.isMounted = true; 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 ) ); } componentWillUnmount() { this.isMounted = false; if ( this.delayedAppendReviews.cancel ) { this.delayedAppendReviews.cancel(); } } getArgs( reviewsToSkip ) { const { categoryIds, order, orderby, productId, reviewsToDisplay } = this.props; const args = { order, orderby, per_page: reviewsToDisplay - reviewsToSkip, offset: reviewsToSkip, }; if ( categoryIds ) { const categories = Array.isArray( categoryIds ) ? categoryIds : JSON.parse( categoryIds ); args.category_id = Array.isArray( categories ) ? categories.join( ',' ) : categories; } if ( productId ) { args.product_id = productId; } return args; } replaceReviews() { if ( this.isPreview ) { return; } const { onReviewsReplaced } = this.props; this.updateListOfReviews().then( onReviewsReplaced ); } appendReviews() { if ( this.isPreview ) { return; } 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, } ) => { if ( this.isMounted ) { this.setState( { reviews: oldReviews .filter( ( review ) => Object.keys( review ).length ) .concat( newReviews ), totalReviews: newTotalReviews, loading: false, error: null, } ); } return { newReviews }; } ) .catch( this.setError ); } setError = async ( e ) => { if ( ! this.isMounted ) { return; } const { onReviewsLoadError } = this.props; const error = await formatError( e ); this.setState( { reviews: [], loading: false, error } ); onReviewsLoadError( error ); }; render() { const { reviewsToDisplay } = this.props; const { error, loading, reviews, totalReviews } = this.state; return ( ); } } const { displayName = OriginalComponent.name || 'Component' } = OriginalComponent; WrappedComponent.displayName = `WithReviews( ${ displayName } )`; return WrappedComponent; }; export default withReviews;