From ad38f9d327f3d7b95f638e40ef0f440011729821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Thu, 15 Aug 2019 16:55:57 +0200 Subject: [PATCH] Create Reviews by Product block (https://github.com/woocommerce/woocommerce-blocks/pull/658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create Reviews by Product block * Honor Content settings * Fix wrong className * Load new wc-packages file * Add reviews-by-product JS files to webpack config * Cleanup * Remove error messages * Add Reviews by Product icon * Update sort options * Allow additional CSS classes attribute * Refactor block styles * Fix wrong default for reviews_orderby * Don't enforce CSS chunks * Add reviews count to Reviews by Product controls (https://github.com/woocommerce/woocommerce-blocks/pull/671) * Add label to Reviews by Product controls count (https://github.com/woocommerce/woocommerce-blocks/pull/677) * Add reviews count to Reviews by Product controls * Add label to Reviews by Product controls count * Add label to Reviews by Product controls count * Update components package * Review ordering and placeholders (https://github.com/woocommerce/woocommerce-blocks/pull/688) * Add support for comment_count ordering and add to productcontrol * Add a placeholder if rating count is 0 * Update assets/js/components/utils/index.js Co-Authored-By: Albert Juhé Lluveras * Update assets/js/blocks/reviews-by-product/block.js Co-Authored-By: Albert Juhé Lluveras * grammar * Fix some linting errors and warnings (https://github.com/woocommerce/woocommerce-blocks/pull/693) * Create Reviews by Product block placeholder (https://github.com/woocommerce/woocommerce-blocks/pull/691) * Create Reviews by Product block placeholder * Reviews by Product: load and render reviews with JS (https://github.com/woocommerce/woocommerce-blocks/pull/696) * Reviews by Product: load and render reviews with JS * Add dangerouslySetInnerHTML explanatory comment * Fix wrong dependency source * Debounce getReviews call when creating the Reviews by Product block * Rename 'Reviewer Picture' with 'Avatar' (https://github.com/woocommerce/woocommerce-blocks/pull/702) * Lint errors * Replace stringify query with addQueryArgs (https://github.com/woocommerce/woocommerce-blocks/pull/707) * Add reviews endpoint (https://github.com/woocommerce/woocommerce-blocks/pull/705) * Prevent state updates on unmounted components (https://github.com/woocommerce/woocommerce-blocks/pull/715) * Add Order by and Load more controls in Reviews by Product frontend (https://github.com/woocommerce/woocommerce-blocks/pull/716) * Export IconReviewsByProduct (https://github.com/woocommerce/woocommerce-blocks/pull/721) * Fix Reviews by Product layout in IE11 (https://github.com/woocommerce/woocommerce-blocks/pull/723) * Set minimum to per page input field (https://github.com/woocommerce/woocommerce-blocks/pull/731) * Hide avatars in Reviews by Products if 'show_avatars' settings is false (https://github.com/woocommerce/woocommerce-blocks/pull/730) * Blocks API - Reviews endpoint with rating sort and category filtering (https://github.com/woocommerce/woocommerce-blocks/pull/726) * Move file to correct location * We are only using the reviews endpoint not revioews/id * Remove sensistive data and make endpoint public * Allow guest access to approved reviews * Add support for rating sorting * category filtering * update arg name * fix category query * Reviews by Product: add placeholders when loading reviews (https://github.com/woocommerce/woocommerce-blocks/pull/732) * Add placeholder animation (https://github.com/woocommerce/woocommerce-blocks/pull/733) * Hook up Reviews by Product 'Order by' with endpoint (https://github.com/woocommerce/woocommerce-blocks/pull/736) * Hook up Reviews by Product 'Order by' with endpoint * Use onChange instead of onBlur in select control * Reviews by Product: Hide ratings if they are disabled in settings (https://github.com/woocommerce/woocommerce-blocks/pull/740) * Hide ratings in Reviews by Product if disabled in settings * Hide order by select if ratings are disabled * Reviews by Product cleanup (https://github.com/woocommerce/woocommerce-blocks/pull/773) * Fix wrong method name * Reduce the number of dependencies used in Reviews by Product (https://github.com/woocommerce/woocommerce-blocks/pull/774) * Reduce the number of dependencies used in Reviews by Product * Use 'withComponentId' HOC * Remove debounce * Fix wrong proptype * Get rid of JS warning * Load render from react-dom * Add formatted_date_created item schema (https://github.com/woocommerce/woocommerce-blocks/pull/788) * Fix import of WithComponentID * Add new settings to Reviews by Product block (https://github.com/woocommerce/woocommerce-blocks/pull/787) * Add new settings to Reviews by Product block * Remove helpText and add notices * Use RangeControl for numeric settings * Prevent fetching new reviews if all were already fetched * Enable product image in reviews * Remove unnecessary catch * Refactor getReviews * Move getReviews back to block's code * Cleanup * Fix wrong order in editor * Hide 'Load More Reviews' if showLoadMore is false * Move getReviews to utils.js * Add @woocommerce/navigation to package.json * Make notices non-dismissable * Reviews by Product: prevent importing all HOCs and import only withComponentId (https://github.com/woocommerce/woocommerce-blocks/pull/811) * Reviews by product: Update review styling and content (https://github.com/woocommerce/woocommerce-blocks/pull/806) * Add new settings to Reviews by Product block * Remove helpText and add notices * Use RangeControl for numeric settings * Prevent fetching new reviews if all were already fetched * Enable product image in reviews * Remove unnecessary catch * Refactor getReviews * Move getReviews back to block's code * Cleanup * Fix wrong order in editor * Hide 'Load More Reviews' if showLoadMore is false * Move getReviews to utils.js * Add @woocommerce/navigation to package.json * Make notices non-dismissable * Review design/layout * verified icons * Read more component * remove comment * expanded -> isExpanded * Localise and change default elipses * Simplify ReadMore * Support children rather than passing content * remove outside * remove list style * Update assets/js/components/read-more/index.js Co-Authored-By: Albert Juhé Lluveras * Update assets/js/components/read-more/index.js Co-Authored-By: Albert Juhé Lluveras * merge set state * Add missing parameter doc in renderReview (https://github.com/woocommerce/woocommerce-blocks/pull/820) * Fix Reviews by Product order by select not honoring default setting (https://github.com/woocommerce/woocommerce-blocks/pull/818) * Read more component - change how clamped content is shown (https://github.com/woocommerce/woocommerce-blocks/pull/821) * Pass review as components * Build summary from content and track both * Toggle display after inital load * remove unused variable * remove componentDidUpdate * Simplify clampLines * Put back componentDidUpdate, but store final summary in state * clampEnabled * Call clampLines from componentDidMount (https://github.com/woocommerce/woocommerce-blocks/pull/826) * truncate html tests * implement trimHTML and pass test * Feedback * test short content * Use withProduct HOC in ReviewsByProductEditor (https://github.com/woocommerce/woocommerce-blocks/pull/828) * Use withProduct HOC * Convert ReviewsByProductEditor to a functional component * Add loading and error states * Prevent loading screen appearing when changing products * Reviews: only save wrapper element to post (https://github.com/woocommerce/woocommerce-blocks/pull/830) * Fix bundlesize config not picking frontend files (https://github.com/woocommerce/woocommerce-blocks/pull/840) * Reviews by Product: split 'block.js' into smaller chunks (https://github.com/woocommerce/woocommerce-blocks/pull/841) * Split 'block.js' into smaller chunks * Move frontend blocks to their specific folder * Order imports * Typo * Add frontend components proptypes * Fix indentation * Call 'this.getDefaultArgs' directly inside 'getReviews' * Move access to wc_product_block_data to the top of the file * Rename 'frontend' folder to 'base' * Rename base components and move styles to their folder * Fix Reviews by Product using rating count instead of review count (https://github.com/woocommerce/woocommerce-blocks/pull/860) * Improve Reviews by Product accessibility (https://github.com/woocommerce/woocommerce-blocks/pull/861) * Improve Reviews by Product accessibility * Make 'onClick' prop not required in * Wrap Reviews by Product editor block with * Reviews: fix reviews without rating not appearing when sorting by rating (https://github.com/woocommerce/woocommerce-blocks/pull/863) --- plugins/woocommerce-blocks/.eslintrc.js | 4 + .../assets/css/abstracts/_mixins.scss | 16 + .../base/components/load-more-button/index.js | 45 ++ .../components/load-more-button/style.scss | 4 + .../js/base/components/read-more/index.js | 163 +++++++ .../base/components/read-more/test/index.js | 30 ++ .../js/base/components/read-more/utils.js | 76 ++++ .../base/components/review-list-item/index.js | 108 +++++ .../components/review-list-item/style.scss | 149 +++++++ .../js/base/components/review-list/index.js | 45 ++ .../js/base/components/review-list/style.scss | 3 + .../components/review-order-select/index.js | 53 +++ .../components/review-order-select/style.scss | 10 + .../js/{ => base}/hocs/with-component-id.js | 0 .../js/blocks/product-categories/block.js | 2 +- .../js/blocks/reviews-by-product/edit.js | 355 +++++++++++++++ .../blocks/reviews-by-product/editor-block.js | 107 +++++ .../js/blocks/reviews-by-product/editor.scss | 13 + .../reviews-by-product/frontend-block.js | 169 +++++++ .../js/blocks/reviews-by-product/frontend.js | 28 ++ .../js/blocks/reviews-by-product/index.js | 159 +++++++ .../js/blocks/reviews-by-product/utils.js | 40 ++ .../assets/js/components/icons/index.js | 1 + .../js/components/icons/reviews-by-product.js | 18 + .../js/components/product-control/index.js | 31 +- .../assets/js/components/utils/index.js | 24 +- .../assets/js/hocs/index.js | 1 - .../woocommerce-blocks/bundlesize.config.json | 4 +- plugins/woocommerce-blocks/package-lock.json | 153 +++---- plugins/woocommerce-blocks/package.json | 5 +- plugins/woocommerce-blocks/postcss.config.js | 2 +- plugins/woocommerce-blocks/src/Assets.php | 33 +- .../src/BlockTypes/ProductCategories.php | 4 +- .../src/BlockTypes/ReviewsByProduct.php | 51 +++ plugins/woocommerce-blocks/src/Library.php | 1 + plugins/woocommerce-blocks/src/RestApi.php | 1 + .../RestApi/Controllers/ProductReviews.php | 415 ++++++++++++++++++ .../src/RestApi/Controllers/Products.php | 9 +- plugins/woocommerce-blocks/webpack.config.js | 22 +- 39 files changed, 2219 insertions(+), 135 deletions(-) create mode 100644 plugins/woocommerce-blocks/assets/js/base/components/load-more-button/index.js create mode 100644 plugins/woocommerce-blocks/assets/js/base/components/load-more-button/style.scss create mode 100644 plugins/woocommerce-blocks/assets/js/base/components/read-more/index.js create mode 100644 plugins/woocommerce-blocks/assets/js/base/components/read-more/test/index.js create mode 100644 plugins/woocommerce-blocks/assets/js/base/components/read-more/utils.js create mode 100644 plugins/woocommerce-blocks/assets/js/base/components/review-list-item/index.js create mode 100644 plugins/woocommerce-blocks/assets/js/base/components/review-list-item/style.scss create mode 100644 plugins/woocommerce-blocks/assets/js/base/components/review-list/index.js create mode 100644 plugins/woocommerce-blocks/assets/js/base/components/review-list/style.scss create mode 100644 plugins/woocommerce-blocks/assets/js/base/components/review-order-select/index.js create mode 100644 plugins/woocommerce-blocks/assets/js/base/components/review-order-select/style.scss rename plugins/woocommerce-blocks/assets/js/{ => base}/hocs/with-component-id.js (100%) create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/edit.js create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/editor-block.js create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/editor.scss create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/frontend-block.js create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/frontend.js create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/index.js create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/utils.js create mode 100644 plugins/woocommerce-blocks/assets/js/components/icons/reviews-by-product.js create mode 100644 plugins/woocommerce-blocks/src/BlockTypes/ReviewsByProduct.php create mode 100644 plugins/woocommerce-blocks/src/RestApi/Controllers/ProductReviews.php diff --git a/plugins/woocommerce-blocks/.eslintrc.js b/plugins/woocommerce-blocks/.eslintrc.js index f55c80679d1..98009a4eb13 100644 --- a/plugins/woocommerce-blocks/.eslintrc.js +++ b/plugins/woocommerce-blocks/.eslintrc.js @@ -14,6 +14,10 @@ module.exports = { ], rules: { '@wordpress/dependency-group': 'off', + 'camelcase': [ 'error', { + allow: [ 'wc_product_block_data' ], + properties: 'never', + } ], 'valid-jsdoc': 'off', } }; diff --git a/plugins/woocommerce-blocks/assets/css/abstracts/_mixins.scss b/plugins/woocommerce-blocks/assets/css/abstracts/_mixins.scss index 851aeac531a..749c94d94ba 100644 --- a/plugins/woocommerce-blocks/assets/css/abstracts/_mixins.scss +++ b/plugins/woocommerce-blocks/assets/css/abstracts/_mixins.scss @@ -15,6 +15,18 @@ } } +@keyframes loading-fade { + 0% { + opacity: 0.7; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.7; + } +} + // Adds animation to placeholder section @mixin placeholder( $lighten-percentage: 30% ) { animation: loading-fade 1.6s ease-in-out infinite; @@ -24,6 +36,10 @@ &::after { content: "\00a0"; } + + @media screen and (prefers-reduced-motion: reduce) { + animation: none; + } } // Adds animation to transforms diff --git a/plugins/woocommerce-blocks/assets/js/base/components/load-more-button/index.js b/plugins/woocommerce-blocks/assets/js/base/components/load-more-button/index.js new file mode 100644 index 00000000000..4a91ea6e8f5 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/components/load-more-button/index.js @@ -0,0 +1,45 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from 'react'; +import PropTypes from 'prop-types'; + +/** + * Internal dependencies + */ +import './style.scss'; + +export const LoadMoreButton = ( { onClick, label, screenReaderLabel } ) => { + const labelNode = ( screenReaderLabel && label !== screenReaderLabel ) ? ( + + + { label } + + + { screenReaderLabel } + + + ) : label; + + return ( + + ); +}; + +LoadMoreButton.propTypes = { + label: PropTypes.string, + onClick: PropTypes.func, + screenReaderLabel: PropTypes.string, +}; + +LoadMoreButton.defaultProps = { + label: __( 'Load more', 'woo-gutenberg-products-block' ), +}; + +export default LoadMoreButton; diff --git a/plugins/woocommerce-blocks/assets/js/base/components/load-more-button/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/load-more-button/style.scss new file mode 100644 index 00000000000..1447142a2cb --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/components/load-more-button/style.scss @@ -0,0 +1,4 @@ +.wc-block-load-more { + display: block; + margin: 0 auto; +} diff --git a/plugins/woocommerce-blocks/assets/js/base/components/read-more/index.js b/plugins/woocommerce-blocks/assets/js/base/components/read-more/index.js new file mode 100644 index 00000000000..3b736ddebf9 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/components/read-more/index.js @@ -0,0 +1,163 @@ +/** + * Show text based content, limited to a number of lines, with a read more link. + * + * Based on https://github.com/zoltantothcom/react-clamp-lines. + */ +import React, { createRef, Component } from 'react'; +import PropTypes from 'prop-types'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { clampLines } from './utils'; + +class ReadMore extends Component { + constructor( props ) { + super( ...arguments ); + + this.state = { + /** + * This is true when read more has been pressed and the full review is shown. + */ + isExpanded: false, + /** + * True if we are clamping content. False if the review is short. Null during init. + */ + clampEnabled: null, + /** + * Content is passed in via children. + */ + content: props.children, + /** + * Summary content generated from content HTML. + */ + summary: '.', + }; + + this.reviewSummary = createRef(); + this.reviewContent = createRef(); + this.getButton = this.getButton.bind( this ); + this.onClick = this.onClick.bind( this ); + } + + componentDidMount() { + if ( this.props.children ) { + const { maxLines, ellipsis } = this.props; + + const lineHeight = this.reviewSummary.current.clientHeight + 1; + const reviewHeight = this.reviewContent.current.clientHeight + 1; + const maxHeight = ( lineHeight * maxLines ) + 1; + const clampEnabled = reviewHeight > maxHeight; + + this.setState( { + clampEnabled, + } ); + + if ( clampEnabled ) { + this.setState( { + summary: clampLines( this.reviewContent.current.innerHTML, this.reviewSummary.current, maxHeight, ellipsis ), + } ); + } + } + } + + getButton() { + const { isExpanded } = this.state; + const { className, lessText, moreText } = this.props; + + const buttonText = isExpanded ? lessText : moreText; + + if ( ! buttonText ) { + return; + } + + return ( + + { buttonText } + + ); + } + + /** + * Handles the click event for the read more/less button. + * + * @param {obj} e event + */ + onClick( e ) { + e.preventDefault(); + + const { isExpanded } = this.state; + + this.setState( { + isExpanded: ! isExpanded, + } ); + } + + render() { + const { className } = this.props; + const { content, summary, clampEnabled, isExpanded } = this.state; + + if ( ! content ) { + return null; + } + + if ( false === clampEnabled ) { + return ( +
+
+ { content } +
+
+ ); + } + + return ( +
+ { ( ! isExpanded || null === clampEnabled ) && ( +
+ ) } + { ( isExpanded || null === clampEnabled ) && ( +
+ { content } +
+ ) } + { this.getButton() } +
+ ); + } +} + +ReadMore.propTypes = { + children: PropTypes.node.isRequired, + maxLines: PropTypes.number, + ellipsis: PropTypes.string, + moreText: PropTypes.string, + lessText: PropTypes.string, + className: PropTypes.string, +}; + +ReadMore.defaultProps = { + maxLines: 3, + ellipsis: '…', + moreText: __( 'Read more', 'woo-gutenberg-products-block' ), + lessText: __( 'Read less', 'woo-gutenberg-products-block' ), + className: 'read-more-content', +}; + +export default ReadMore; diff --git a/plugins/woocommerce-blocks/assets/js/base/components/read-more/test/index.js b/plugins/woocommerce-blocks/assets/js/base/components/read-more/test/index.js new file mode 100644 index 00000000000..d3e193c716c --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/components/read-more/test/index.js @@ -0,0 +1,30 @@ +/** + * Internal dependencies + */ +import { truncateHtml } from '../utils'; +const shortContent = + '

Lorem ipsum dolor sit amet, consectetur..

'; + +const longContent = + '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam a condimentum diam. Donec finibus enim eros, et lobortis magna varius quis. Nulla lacinia tellus ac neque aliquet, in porttitor metus interdum. Maecenas vestibulum nisi et auctor vestibulum. Maecenas vehicula, lacus et pellentesque tempor, orci nulla mattis purus, id porttitor augue magna et metus. Aenean hendrerit aliquet massa ac convallis. Mauris vestibulum neque in condimentum porttitor. Donec viverra, orci a accumsan vehicula, dui massa lobortis lorem, et cursus est purus pulvinar elit. Vestibulum vitae tincidunt ex, ut vulputate nisi.

' + + '

Morbi tristique iaculis felis, sed porta urna tincidunt vitae. Etiam nisl sem, eleifend non varius quis, placerat a arcu. Donec consectetur nunc at orci fringilla pulvinar. Nam hendrerit tellus in est aliquet varius id in diam. Donec eu ullamcorper ante. Ut ultricies, felis vel sodales aliquet, nibh massa vestibulum ipsum, sed dignissim mi nunc eget lacus. Curabitur mattis placerat magna a aliquam. Nullam diam elit, cursus nec erat ullamcorper, tempor eleifend mauris. Nunc placerat nunc ut enim ornare tempus. Fusce porta molestie ante eget faucibus. Fusce eu lectus sit amet diam auctor lacinia et in diam. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Mauris eu lacus lobortis, faucibus est vel, pulvinar odio. Duis feugiat tortor quis dui euismod varius.

'; + +describe( 'ReadMore Component', () => { + describe( 'Test the truncateHtml function', () => { + it( 'Truncate long HTML content to length of 10', async () => { + const truncatedContent = truncateHtml( longContent, 10 ); + + expect( truncatedContent ).toEqual( '

Lorem ipsum...

' ); + } ); + it( 'Truncate long HTML content, but avoid cutting off HTML tags.', async () => { + const truncatedContent = truncateHtml( longContent, 40 ); + + expect( truncatedContent ).toEqual( '

Lorem ipsum dolor sit amet, consectetur...

' ); + } ); + it( 'No need to truncate short HTML content.', async () => { + const truncatedContent = truncateHtml( shortContent, 100 ); + + expect( truncatedContent ).toEqual( '

Lorem ipsum dolor sit amet, consectetur..

' ); + } ); + } ); +} ); diff --git a/plugins/woocommerce-blocks/assets/js/base/components/read-more/utils.js b/plugins/woocommerce-blocks/assets/js/base/components/read-more/utils.js new file mode 100644 index 00000000000..75866340a49 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/components/read-more/utils.js @@ -0,0 +1,76 @@ +import trimHtml from 'trim-html'; + +/** + * Truncate some HTML content to a given length. + * + * @param {string} html HTML that will be truncated. + * @param {int} length Legth to truncate the string to. + * @param {string} ellipsis Character to append to truncated content. + */ +export const truncateHtml = ( html, length, ellipsis = '...' ) => { + const trimmed = trimHtml( html, { + suffix: ellipsis, + limit: length, + } ); + + return trimmed.html; +}; + +/** + * Clamp lines calculates the height of a line of text and then limits it to the + * value of the lines prop. Content is updated once limited. + * + * @param {string} originalContent Content to be clamped. + * @param {object} targetElement Element which will contain the clamped content. + * @param {integer} maxHeight Max height of the clamped content. + * @param {string} ellipsis Character to append to clamped content. + * @return {string} clamped content + */ +export const clampLines = ( originalContent, targetElement, maxHeight, ellipsis ) => { + const length = calculateLength( originalContent, targetElement, maxHeight ); + + return truncateHtml( originalContent, length - ellipsis.length, ellipsis ); +}; + +/** + * Calculate how long the content can be based on the maximum number of lines allowed, and client height. + * + * @param {string} originalContent Content to be clamped. + * @param {object} targetElement Element which will contain the clamped content. + * @param {integer} maxHeight Max height of the clamped content. + */ +const calculateLength = ( originalContent, targetElement, maxHeight ) => { + let markers = { + start: 0, + middle: 0, + end: originalContent.length, + }; + + while ( markers.start <= markers.end ) { + markers.middle = Math.floor( ( markers.start + markers.end ) / 2 ); + + // We set the innerHTML directly in the DOM here so we can reliably check the clientHeight later in moveMarkers. + targetElement.innerHTML = truncateHtml( originalContent, markers.middle ); + + markers = moveMarkers( markers, targetElement.clientHeight, maxHeight ); + } + + return markers.middle; +}; + +/** + * Move string markers. Used by calculateLength. + * + * @param {object} markers Markers for clamped content. + * @param {integer} currentHeight Current height of clamped content. + * @param {integer} maxHeight Max height of the clamped content. + */ +const moveMarkers = ( markers, currentHeight, maxHeight ) => { + if ( currentHeight <= maxHeight ) { + markers.start = markers.middle + 1; + } else { + markers.end = markers.middle - 1; + } + + return markers; +}; diff --git a/plugins/woocommerce-blocks/assets/js/base/components/review-list-item/index.js b/plugins/woocommerce-blocks/assets/js/base/components/review-list-item/index.js new file mode 100644 index 00000000000..4867b9a50e4 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/components/review-list-item/index.js @@ -0,0 +1,108 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import PropTypes from 'prop-types'; + +/** + * Internal dependencies + */ +import ReadMore from '../read-more'; +import './style.scss'; + +function getReviewClasses( isLoading ) { + const classArray = [ 'wc-block-review-list-item__item' ]; + + if ( isLoading ) { + classArray.push( 'is-loading' ); + } + + return classArray.join( ' ' ); +} + +function getReviewImage( review, imageType, isLoading ) { + if ( isLoading || ! review ) { + return ( +
+ ); + } + + return ( +
+ { imageType === 'product' ? ( + + ) : ( + + ) } + { review.verified && ( +
{ __( 'Verified buyer', 'woo-gutenberg-products-block' ) }
+ ) } +
+ ); +} + +function getReviewContent( review ) { + return ( + +
+ + ); +} + +const ReviewListItem = ( { attributes, review = {} } ) => { + const { imageType, showReviewDate, showReviewerName, showReviewImage, showReviewRating: showReviewRatingAttr } = attributes; + const { date_created: dateCreated, formatted_date_created: formattedDateCreated, rating, reviewer = '' } = review; + const isLoading = ! Object.keys( review ).length > 0; + const showReviewRating = Number.isFinite( rating ) && showReviewRatingAttr; + const classes = getReviewClasses( isLoading ); + const starStyle = { + width: ( rating / 5 * 100 ) + '%', + }; + + return ( +
  • + { ( showReviewDate || showReviewerName || showReviewImage || showReviewRating ) && ( +
    + { showReviewImage && getReviewImage( review, imageType, isLoading ) } + { ( showReviewerName || showReviewRating || showReviewDate ) && ( +
    + { showReviewerName && ( + { reviewer } + ) } + { showReviewRating && ( +
    +
    + { sprintf( __( 'Rated %d out of 5', 'woo-gutenberg-products-block' ), rating ) } +
    +
    + ) } + { showReviewDate && ( + + ) } +
    + ) } +
    + ) } + { getReviewContent( review ) } +
  • + ); +}; + +ReviewListItem.propTypes = { + attributes: PropTypes.object.isRequired, + review: PropTypes.object, +}; + +export default ReviewListItem; diff --git a/plugins/woocommerce-blocks/assets/js/base/components/review-list-item/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/review-list-item/style.scss new file mode 100644 index 00000000000..8311683b05b --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/components/review-list-item/style.scss @@ -0,0 +1,149 @@ +.is-loading { + .wc-block-review-list-item__text { + @include placeholder(); + display: block; + width: 60%; + } + + .wc-block-review-list-item__info { + .wc-block-review-list-item__image { + @include placeholder(); + } + + .wc-block-review-list-item__meta { + .wc-block-review-list-item__author { + @include placeholder(); + font-size: 1em; + width: 80px; + } + + .wc-block-review-list-item__rating { + .wc-block-review-list-item__rating__stars > span { + display: none; + } + } + } + + .wc-block-review-list-item__published-date { + @include placeholder(); + height: 1em; + width: 120px; + } + } +} + +.wc-block-review-list-item__item { + margin: 0 0 $gap-large * 2; + list-style: none; +} + +.wc-block-review-list-item__info { + display: grid; + grid-template-columns: 1fr; + margin-bottom: $gap-large; +} + +.wc-block-review-list-item__meta { + grid-column: 1; + grid-row: 1; +} + +.has-image { + .wc-block-review-list-item__info { + grid-template-columns: #{48px + $gap} 1fr; + } + .wc-block-review-list-item__meta { + grid-column: 2; + } +} + +.wc-block-review-list-item__image { + height: 48px; + grid-column: 1; + grid-row: 1 / 3; + width: 48px; + position: relative; + + img { + width: 100%; + height: 100%; + display: block; + } +} + +.wc-block-review-list-item__verified { + width: 21px; + height: 21px; + text-indent: 21px; + margin: 0; + line-height: 21px; + overflow: hidden; + position: absolute; + right: -7px; + bottom: -7px; + + &::before { + width: 21px; + height: 21px; + background: transparent url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="21" height="21" fill="none"%3E%3Ccircle cx="10.5" cy="10.5" r="10.5" fill="%23fff"/%3E%3Cpath fill="%23008A21" fill-rule="evenodd" d="M2.1667 10.5003c0-4.6 3.7333-8.3333 8.3333-8.3333s8.3334 3.7333 8.3334 8.3333S15.1 18.8337 10.5 18.8337s-8.3333-3.7334-8.3333-8.3334zm2.5 0l4.1666 4.1667 7.5001-7.5-1.175-1.1833-6.325 6.325-2.9917-2.9834-1.175 1.175z" clip-rule="evenodd"/%3E%3Cmask id="a" width="17" height="17" x="2" y="2" maskUnits="userSpaceOnUse"%3E%3Cpath fill="%23fff" fill-rule="evenodd" d="M2.1667 10.5003c0-4.6 3.7333-8.3333 8.3333-8.3333s8.3334 3.7333 8.3334 8.3333S15.1 18.8337 10.5 18.8337s-8.3333-3.7334-8.3333-8.3334zm2.5 0l4.1666 4.1667 7.5001-7.5-1.175-1.1833-6.325 6.325-2.9917-2.9834-1.175 1.175z" clip-rule="evenodd"/%3E%3C/mask%3E%3Cg mask="url(%23a)"%3E%3Cpath fill="%23008A21" d="M.5.5h20v20H.5z"/%3E%3C/g%3E%3C/svg%3E') center center no-repeat; /* stylelint-disable-line */ + display: block; + content: ""; + } +} + +.wc-block-review-list-item__author { + display: block; +} + +.wc-block-review-list-item__rating { + display: inline-block; + vertical-align: middle; + margin-right: $gap; + height: 1em; + + > .wc-block-review-list-item__rating__stars { + display: inline-block; + top: -5px; + overflow: hidden; + position: relative; + height: 1.618em; + line-height: 1.618; + font-size: 1em; + width: 5.3em; + font-family: star; /* stylelint-disable-line */ + font-weight: 400; + } + + > .wc-block-review-list-item__rating__stars::before { + content: "\53\53\53\53\53"; + opacity: 0.25; + float: left; + top: 0; + left: 0; + position: absolute; + } + + > .wc-block-review-list-item__rating__stars span { + overflow: hidden; + float: left; + top: 0; + left: 0; + position: absolute; + padding-top: 1.5em; + } + + > .wc-block-review-list-item__rating__stars span::before { + content: "\53\53\53\53\53"; + top: 0; + position: absolute; + left: 0; + color: #e6a237; + } +} + +.wc-block-review-list-item__published-date { + display: inline-block; + vertical-align: middle; + color: #808080; + font-size: 0.875em; +} diff --git a/plugins/woocommerce-blocks/assets/js/base/components/review-list/index.js b/plugins/woocommerce-blocks/assets/js/base/components/review-list/index.js new file mode 100644 index 00000000000..20bab883ec7 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/components/review-list/index.js @@ -0,0 +1,45 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * Internal dependencies + */ +import ReviewListItem from '../review-list-item'; +import './style.scss'; + +const ReviewList = ( { attributes, componentId, reviews } ) => { + const showReviewImage = ( wc_product_block_data.showAvatars || attributes.imageType === 'product' ) && attributes.showReviewImage; + const showReviewRating = wc_product_block_data.enableReviewRating && attributes.showReviewRating; + const attrs = { + ...attributes, + showReviewImage, + showReviewRating, + }; + + return ( +
      + { reviews.length === 0 ? + ( + + ) : ( + reviews.map( ( review, i ) => ( + + ) ) + ) + } +
    + ); +}; + +ReviewList.propTypes = { + attributes: PropTypes.object.isRequired, + componentId: PropTypes.number.isRequired, + reviews: PropTypes.array.isRequired, +}; + +export default ReviewList; diff --git a/plugins/woocommerce-blocks/assets/js/base/components/review-list/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/review-list/style.scss new file mode 100644 index 00000000000..37df7807c13 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/components/review-list/style.scss @@ -0,0 +1,3 @@ +.wc-block-review-list { + margin: 0; +} diff --git a/plugins/woocommerce-blocks/assets/js/base/components/review-order-select/index.js b/plugins/woocommerce-blocks/assets/js/base/components/review-order-select/index.js new file mode 100644 index 00000000000..81c762a137b --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/components/review-order-select/index.js @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import PropTypes from 'prop-types'; + +/** + * Internal dependencies + */ +import './style.scss'; + +const ReviewOrderSelect = ( { componentId, onChange, readOnly, value } ) => { + const selectId = `wc-block-review-order-select__select-${ componentId }`; + + return ( +

    + + +

    + ); +}; + +ReviewOrderSelect.propTypes = { + componentId: PropTypes.number.isRequired, + onChange: PropTypes.func, + readOnly: PropTypes.bool, + value: PropTypes.oneOf( [ 'most-recent', 'highest-rating', 'lowest-rating' ] ), +}; + +export default ReviewOrderSelect; diff --git a/plugins/woocommerce-blocks/assets/js/base/components/review-order-select/style.scss b/plugins/woocommerce-blocks/assets/js/base/components/review-order-select/style.scss new file mode 100644 index 00000000000..be5e57584f3 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/base/components/review-order-select/style.scss @@ -0,0 +1,10 @@ +.wc-block-review-order-select { + margin-bottom: $gap-small; + text-align: right; +} + +.wc-block-review-order-select__label { + margin-right: $gap-small; + display: inline-block; + font-weight: normal; +} diff --git a/plugins/woocommerce-blocks/assets/js/hocs/with-component-id.js b/plugins/woocommerce-blocks/assets/js/base/hocs/with-component-id.js similarity index 100% rename from plugins/woocommerce-blocks/assets/js/hocs/with-component-id.js rename to plugins/woocommerce-blocks/assets/js/base/hocs/with-component-id.js diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-categories/block.js b/plugins/woocommerce-blocks/assets/js/blocks/product-categories/block.js index b1a01168225..8c2ba580636 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-categories/block.js +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-categories/block.js @@ -8,7 +8,7 @@ import classnames from 'classnames'; /** * Internal dependencies */ -import withComponentId from '../../hocs/with-component-id'; +import withComponentId from '../../base/hocs/with-component-id'; /** * Component displaying the categories as dropdown or list. diff --git a/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/edit.js b/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/edit.js new file mode 100644 index 00000000000..d228cb9c3ac --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/edit.js @@ -0,0 +1,355 @@ +/** + * External dependencies + */ +import { __, _n, sprintf } from '@wordpress/i18n'; +import { + BlockControls, + InspectorControls, +} from '@wordpress/editor'; +import { + Button, + Disabled, + Notice, + PanelBody, + Placeholder, + RangeControl, + SelectControl, + Spinner, + ToggleControl, + Toolbar, + withSpokenMessages, +} from '@wordpress/components'; +import classNames from 'classnames'; +import { SearchListItem } from '@woocommerce/components'; +import { Fragment, RawHTML } from '@wordpress/element'; +import { compose } from '@wordpress/compose'; +import { escapeHTML } from '@wordpress/escape-html'; +import PropTypes from 'prop-types'; +import { getAdminLink } from '@woocommerce/navigation'; + +/** + * Internal dependencies + */ +import ApiErrorPlaceholder from '../../components/api-error-placeholder'; +import EditorBlock from './editor-block.js'; +import ProductControl from '../../components/product-control'; +import ToggleButtonControl from '../../components/toggle-button-control'; +import { IconReviewsByProduct } from '../../components/icons'; +import { withProduct } from '../../hocs'; + +const enableReviewRating = !! ( typeof wc_product_block_data !== 'undefined' && wc_product_block_data.enableReviewRating ); +const showAvatars = !! ( typeof wc_product_block_data !== 'undefined' && wc_product_block_data.showAvatars ); + +/** + * Component to handle edit mode of "Reviews by Product". + */ +const ReviewsByProductEditor = ( { attributes, debouncedSpeak, error, getProduct, isLoading, product, setAttributes } ) => { + const { className, editMode, productId, showReviewDate, showReviewerName } = attributes; + + const getBlockControls = () => ( + + setAttributes( { editMode: ! editMode } ), + isActive: editMode, + }, + ] } + /> + + ); + + const renderProductControlItem = ( args ) => { + const { item = 0 } = args; + + return ( + + ); + }; + + const getInspectorControls = () => { + const minPerPage = 1; + const maxPerPage = 20; + + return ( + + + { + const id = value[ 0 ] ? value[ 0 ].id : 0; + setAttributes( { productId: id } ); + } } + renderItem={ renderProductControlItem } + /> + + + setAttributes( { showReviewRating: ! attributes.showReviewRating } ) } + /> + { ( attributes.showReviewRating && ! enableReviewRating ) && ( + + + { sprintf( __( 'Product rating is disabled in your %sstore settings%s.', 'woo-gutenberg-products-block' ), ``, '' ) } + + + ) } + setAttributes( { showReviewerName: ! attributes.showReviewerName } ) } + /> + setAttributes( { showReviewImage: ! attributes.showReviewImage } ) } + /> + setAttributes( { showReviewDate: ! attributes.showReviewDate } ) } + /> + { attributes.showReviewImage && ( + + setAttributes( { imageType: value } ) } + /> + { ( attributes.imageType === 'reviewer' && ! showAvatars ) && ( + + + { sprintf( __( 'Reviewer photo is disabled in your %ssite settings%s.', 'woo-gutenberg-products-block' ), ``, '' ) } + + + ) } + + ) } + + + setAttributes( { showOrderby: ! attributes.showOrderby } ) } + /> + setAttributes( { orderby } ) } + /> + setAttributes( { reviewsOnPageLoad } ) } + max={ maxPerPage } + min={ minPerPage } + /> + setAttributes( { showLoadMore: ! attributes.showLoadMore } ) } + /> + { attributes.showLoadMore && ( + setAttributes( { reviewsOnLoadMore } ) } + max={ maxPerPage } + min={ minPerPage } + /> + ) } + + + ); + }; + + const renderApiError = () => ( + + ); + + const renderLoadingScreen = () => { + return ( + } + label={ __( 'Reviews by Product', 'woo-gutenberg-products-block' ) } + className="wc-block-reviews-by-product" + > + + + ); + }; + + const renderEditMode = () => { + const onDone = () => { + setAttributes( { editMode: false } ); + debouncedSpeak( + __( + 'Showing Reviews by Product block preview.', + 'woo-gutenberg-products-block' + ) + ); + }; + + return ( + } + label={ __( 'Reviews by Product', 'woo-gutenberg-products-block' ) } + className="wc-block-reviews-by-product" + > + { __( + 'Show reviews of your product to build trust', + 'woo-gutenberg-products-block' + ) } +
    + { + const id = value[ 0 ] ? value[ 0 ].id : 0; + setAttributes( { productId: id } ); + } } + queryArgs={ { + orderby: 'comment_count', + order: 'desc', + } } + renderItem={ renderProductControlItem } + /> + +
    +
    + ); + }; + + const renderViewMode = () => { + const showReviewImage = ( showAvatars || attributes.imageType === 'product' ) && attributes.showReviewImage; + const showReviewRating = enableReviewRating && attributes.showReviewRating; + const classes = classNames( 'wc-block-reviews-by-product', className, { + 'has-image': showReviewImage, + 'has-name': showReviewerName, + 'has-date': showReviewDate, + 'has-rating': showReviewRating, + } ); + + return ( + + { product.review_count === 0 ? ( + } + label={ __( 'Reviews by Product', 'woo-gutenberg-products-block' ) } + > +
    ' + escapeHTML( product.name ) + '' + ), + } } /> + + ) : ( + +
    + +
    +
    + ) } + + ); + }; + + if ( error ) { + return renderApiError(); + } + + if ( ! productId || editMode ) { + return renderEditMode(); + } + + if ( ! product || isLoading ) { + return renderLoadingScreen(); + } + + return ( + + { getBlockControls() } + { getInspectorControls() } + { renderViewMode() } + + ); +}; + +ReviewsByProductEditor.propTypes = { + /** + * The attributes for this block. + */ + attributes: PropTypes.object.isRequired, + /** + * The register block name. + */ + name: PropTypes.string.isRequired, + /** + * A callback to update attributes. + */ + setAttributes: PropTypes.func.isRequired, + // from withProduct + error: PropTypes.object, + getProduct: PropTypes.func, + isLoading: PropTypes.bool, + product: PropTypes.shape( { + name: PropTypes.node, + review_count: PropTypes.number, + } ), + // from withSpokenMessages + debouncedSpeak: PropTypes.func.isRequired, +}; + +export default compose( [ + withProduct, + withSpokenMessages, +] )( ReviewsByProductEditor ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/editor-block.js b/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/editor-block.js new file mode 100644 index 00000000000..7554efcb722 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/editor-block.js @@ -0,0 +1,107 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { debounce } from 'lodash'; + +/** + * 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'; + +const enableReviewRating = !! ( typeof wc_product_block_data !== 'undefined' && wc_product_block_data.enableReviewRating ); + +/** + * Block rendered in the editor. + */ +class EditorBlock extends Component { + constructor() { + super( ...arguments ); + + this.state = { + reviews: [], + totalReviews: 0, + }; + + 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() { + 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 } ); + } ).catch( () => { + this.setState( { reviews: [] } ); + } ); + } + + render() { + const { attributes, componentId } = this.props; + const { reviews, totalReviews } = this.state; + + return ( + + { ( attributes.showOrderby && enableReviewRating ) && ( + + ) } + + { ( attributes.showLoadMore && totalReviews > reviews.length ) && ( + + ) } + + ); + } +} + +EditorBlock.propTypes = { + /** + * The attributes for this block. + */ + attributes: PropTypes.object.isRequired, + // from withComponentId + componentId: PropTypes.number, +}; + +export default withComponentId( EditorBlock ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/editor.scss new file mode 100644 index 00000000000..1640e158517 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/editor.scss @@ -0,0 +1,13 @@ +.wc-block-reviews-by-product__selection { + width: 100%; +} + +.components-base-control { + + .wc-block-reviews-by-product__notice { + margin: -$gap 0 $gap; + } + + &:nth-last-child(2) + .wc-block-reviews-by-product__notice { + margin: -$gap 0 $gap-small; + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/frontend-block.js b/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/frontend-block.js new file mode 100644 index 00000000000..890058b9b16 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/frontend-block.js @@ -0,0 +1,169 @@ +/** + * 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'; + +const enableReviewRating = !! ( typeof wc_product_block_data !== 'undefined' && wc_product_block_data.enableReviewRating ); + +/** + * 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; + + return ( + + { ( attributes.showOrderby && enableReviewRating ) && ( + + ) } + + { ( attributes.showLoadMore && totalReviews > reviews.length ) && ( + + ) } + + ); + } +} + +FrontendBlock.propTypes = { + /** + * The attributes for this block. + */ + attributes: PropTypes.object.isRequired, + // from withComponentId + componentId: PropTypes.number, +}; + +export default withComponentId( FrontendBlock ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/frontend.js b/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/frontend.js new file mode 100644 index 00000000000..40cbf7f9d38 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/frontend.js @@ -0,0 +1,28 @@ +/** + * External dependencies + */ +import { render } from 'react-dom'; + +/** + * Internal dependencies + */ +import FrontendBlock from './frontend-block.js'; + +const containers = document.querySelectorAll( + '.wp-block-woocommerce-reviews-by-product' +); + +if ( containers.length ) { + // Use Array.forEach for IE11 compatibility + Array.prototype.forEach.call( containers, ( el ) => { + const attributes = { + ...el.dataset, + showReviewDate: el.classList.contains( 'has-date' ), + showReviewerName: el.classList.contains( 'has-name' ), + showReviewImage: el.classList.contains( 'has-image' ), + showReviewRating: el.classList.contains( 'has-rating' ), + }; + + render( , el ); + } ); +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/index.js b/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/index.js new file mode 100644 index 00000000000..d0d3413fcd5 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/index.js @@ -0,0 +1,159 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import classNames from 'classnames'; +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import './editor.scss'; +import Editor from './edit'; +import { IconReviewsByProduct } from '../../components/icons'; + +/** + * Register and run the "Reviews by Product" block. + */ +registerBlockType( 'woocommerce/reviews-by-product', { + title: __( 'Reviews by Product', 'woo-gutenberg-products-block' ), + icon: ( + + ), + category: 'woocommerce', + keywords: [ __( 'WooCommerce', 'woo-gutenberg-products-block' ) ], + description: __( + 'Show reviews of your product to build trust.', + 'woo-gutenberg-products-block' + ), + attributes: { + /** + * Toggle for edit mode in the block preview. + */ + editMode: { + type: 'boolean', + default: true, + }, + + /** + * Whether to display the reviewer or product image. + */ + imageType: { + type: 'string', + default: 'reviewer', + }, + + /** + * Order to use for the reviews listing. + */ + orderby: { + type: 'string', + default: 'most-recent', + }, + + /** + * The id of the product to load reviews for. + */ + productId: { + type: 'number', + }, + + /** + * Number of reviews to add when clicking on load more. + */ + reviewsOnLoadMore: { + type: 'number', + default: 10, + }, + + /** + * Number of reviews to display on page load. + */ + reviewsOnPageLoad: { + type: 'number', + default: 10, + }, + + /** + * Show the load more button. + */ + showLoadMore: { + type: 'boolean', + default: true, + }, + + /** + * Show the order by selector. + */ + showOrderby: { + type: 'boolean', + default: true, + }, + + /** + * Show the review date. + */ + showReviewDate: { + type: 'boolean', + default: true, + }, + + /** + * Show the reviewer name. + */ + showReviewerName: { + type: 'boolean', + default: true, + }, + + /** + * Show the review image.. + */ + showReviewImage: { + type: 'boolean', + default: true, + }, + + /** + * Show the product rating. + */ + showReviewRating: { + type: 'boolean', + default: true, + }, + }, + + /** + * Renders and manages the block. + */ + edit( props ) { + return ; + }, + + /** + * Save the props to post content. + */ + save( { attributes } ) { + const { className, imageType, orderby, productId, reviewsOnPageLoad, reviewsOnLoadMore, showLoadMore, showOrderby, showReviewDate, showReviewerName, showReviewImage, showReviewRating } = attributes; + + const classes = classNames( 'wc-block-reviews-by-product', className, { + 'has-date': showReviewDate, + 'has-name': showReviewerName, + 'has-image': showReviewImage, + 'has-rating': showReviewRating, + } ); + const data = { + 'data-image-type': imageType, + 'data-product-id': productId, + 'data-orderby': orderby, + 'data-reviews-on-page-load': reviewsOnPageLoad, + 'data-reviews-on-load-more': reviewsOnLoadMore, + 'data-show-load-more': showLoadMore, + 'data-show-orderby': showOrderby, + }; + + return ( +
    + ); + }, +} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/utils.js b/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/utils.js new file mode 100644 index 00000000000..98057709ae5 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/reviews-by-product/utils.js @@ -0,0 +1,40 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + +const enableReviewRating = !! ( typeof wc_product_block_data !== 'undefined' && wc_product_block_data.enableReviewRating ); + +export const getOrderArgs = ( orderValue ) => { + if ( enableReviewRating ) { + if ( orderValue === 'lowest-rating' ) { + return { + order: 'asc', + orderby: 'rating', + }; + } + if ( orderValue === 'highest-rating' ) { + return { + order: 'desc', + orderby: 'rating', + }; + } + } + + return { + order: 'desc', + orderby: 'date_gmt', + }; +}; + +export const getReviews = ( args ) => { + return apiFetch( { + path: '/wc/blocks/products/reviews?' + Object.entries( args ).map( ( arg ) => arg.join( '=' ) ).join( '&' ), + parse: false, + } ).then( ( response ) => { + return response.json().then( ( reviews ) => { + const totalReviews = parseInt( response.headers.get( 'x-wp-total' ), 10 ); + return { reviews, totalReviews }; + } ); + } ); +}; diff --git a/plugins/woocommerce-blocks/assets/js/components/icons/index.js b/plugins/woocommerce-blocks/assets/js/components/icons/index.js index af00dc2c9b8..4e883c9d7d9 100644 --- a/plugins/woocommerce-blocks/assets/js/components/icons/index.js +++ b/plugins/woocommerce-blocks/assets/js/components/icons/index.js @@ -6,5 +6,6 @@ export { default as IconFolderStar } from './folder-star'; export { default as IconNewReleases } from './new-releases'; export { default as IconRadioSelected } from './radio-selected'; export { default as IconRadioUnselected } from './radio-unselected'; +export { default as IconReviewsByProduct } from './reviews-by-product'; export { default as IconWidgets } from './widgets'; export { default as IconWoo } from './woo'; diff --git a/plugins/woocommerce-blocks/assets/js/components/icons/reviews-by-product.js b/plugins/woocommerce-blocks/assets/js/components/icons/reviews-by-product.js new file mode 100644 index 00000000000..d0ad4b82242 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/components/icons/reviews-by-product.js @@ -0,0 +1,18 @@ +/** + * External dependencies + */ +import { Icon } from '@wordpress/components'; + +export default ( { className, fillColor } ) => ( + + + + + + + } + /> +); diff --git a/plugins/woocommerce-blocks/assets/js/components/product-control/index.js b/plugins/woocommerce-blocks/assets/js/components/product-control/index.js index dd2e92e4afc..262e26516a5 100644 --- a/plugins/woocommerce-blocks/assets/js/components/product-control/index.js +++ b/plugins/woocommerce-blocks/assets/js/components/product-control/index.js @@ -54,10 +54,15 @@ class ProductControl extends Component { this.onProductSelect = this.onProductSelect.bind( this ); } - componentDidMount() { - const { selected } = this.props; + componentWillUnmount() { + this.debouncedOnSearch.cancel(); + this.debouncedGetVariations.cancel(); + } - getProducts( { selected } ) + componentDidMount() { + const { selected, queryArgs } = this.props; + + getProducts( { selected, queryArgs } ) .then( ( products ) => { products = products.map( ( product ) => { const count = product.variations ? product.variations.length : 0; @@ -81,7 +86,7 @@ class ProductControl extends Component { } getVariations() { - const { product, variationsList } = this.state; + const { product, products, variationsList } = this.state; if ( ! product ) { this.setState( { @@ -91,7 +96,7 @@ class ProductControl extends Component { return; } - const productDetails = this.state.products.find( ( findProduct ) => findProduct.id === product ); + const productDetails = products.find( ( findProduct ) => findProduct.id === product ); if ( ! productDetails.variations || productDetails.variations.length === 0 ) { return; @@ -119,8 +124,8 @@ class ProductControl extends Component { } onSearch( search ) { - const { selected } = this.props; - getProducts( { selected, search } ) + const { selected, queryArgs } = this.props; + getProducts( { selected, search, queryArgs } ) .then( ( products ) => { this.setState( { products, loading: false } ); } ) @@ -238,7 +243,7 @@ class ProductControl extends Component { render() { const { products, loading, product, variationsList } = this.state; - const { onChange, selected } = this.props; + const { onChange, renderItem, selected } = this.props; const currentVariations = variationsList[ product ] || []; const currentList = [ ...products, ...currentVariations ]; const messages = { @@ -267,9 +272,9 @@ class ProductControl extends Component { isSingle selected={ selectedListItems } onChange={ onChange } + renderItem={ renderItem } onSearch={ isLargeCatalog ? this.debouncedOnSearch : null } messages={ messages } - renderItem={ this.renderItem } isHierarchical /> @@ -282,10 +287,18 @@ ProductControl.propTypes = { * Callback to update the selected products. */ onChange: PropTypes.func.isRequired, + /** + * Callback to render each item in the selection list, allows any custom object-type rendering. + */ + renderItem: PropTypes.func.isRequired, /** * The ID of the currently selected product. */ selected: PropTypes.number.isRequired, + /** + * Query args to pass to getProducts. + */ + queryArgs: PropTypes.object, }; export default ProductControl; diff --git a/plugins/woocommerce-blocks/assets/js/components/utils/index.js b/plugins/woocommerce-blocks/assets/js/components/utils/index.js index 28624eabc5f..644b05de657 100644 --- a/plugins/woocommerce-blocks/assets/js/components/utils/index.js +++ b/plugins/woocommerce-blocks/assets/js/components/utils/index.js @@ -14,14 +14,20 @@ export const isLargeCatalog = wc_product_block_data.isLargeCatalog || false; export const limitTags = wc_product_block_data.limitTags || false; export const hasTags = wc_product_block_data.hasTags || false; -const getProductsRequests = ( { selected = [], search } ) => { +const getProductsRequests = ( { selected = [], search = '', queryArgs = [] } ) => { + const defaultArgs = { + per_page: isLargeCatalog ? 100 : -1, + catalog_visibility: 'any', + status: 'publish', + search, + orderby: 'title', + order: 'asc', + }; const requests = [ - addQueryArgs( ENDPOINTS.products, { - per_page: isLargeCatalog ? 100 : -1, - catalog_visibility: 'any', - status: 'publish', - search, - } ), + addQueryArgs( + ENDPOINTS.products, + { ...defaultArgs, ...queryArgs } + ), ]; // If we have a large catalog, we might not get all selected products in the first page. @@ -43,8 +49,8 @@ const getProductsRequests = ( { selected = [], search } ) => { * * @param {Object} - A query object with the list of selected products and search term. */ -export const getProducts = ( { selected = [], search } ) => { - const requests = getProductsRequests( { selected, search } ); +export const getProducts = ( { selected = [], search = '', queryArgs = [] } ) => { + const requests = getProductsRequests( { selected, search, queryArgs } ); return Promise.all( requests.map( ( path ) => apiFetch( { path } ) ) ).then( ( data ) => { return uniqBy( flatten( data ), 'id' ); diff --git a/plugins/woocommerce-blocks/assets/js/hocs/index.js b/plugins/woocommerce-blocks/assets/js/hocs/index.js index b9ad4cbc1f0..07300d79fb4 100644 --- a/plugins/woocommerce-blocks/assets/js/hocs/index.js +++ b/plugins/woocommerce-blocks/assets/js/hocs/index.js @@ -1,4 +1,3 @@ -export { default as withComponentId } from './with-component-id'; export { default as withProduct } from './with-product'; export { default as withCategory } from './with-category'; export { default as withSearchedProducts } from './with-searched-products'; diff --git a/plugins/woocommerce-blocks/bundlesize.config.json b/plugins/woocommerce-blocks/bundlesize.config.json index d65fa71c8d7..3dee494cedb 100644 --- a/plugins/woocommerce-blocks/bundlesize.config.json +++ b/plugins/woocommerce-blocks/bundlesize.config.json @@ -5,7 +5,7 @@ "maxSize": "300 kB" }, { - "path": "./build/frontend*.js", + "path": "./build/*frontend*.js", "maxSize": "130 kB" }, { @@ -13,4 +13,4 @@ "maxSize": "100kB" } ] -} \ No newline at end of file +} diff --git a/plugins/woocommerce-blocks/package-lock.json b/plugins/woocommerce-blocks/package-lock.json index e944e906051..669dc03ce71 100644 --- a/plugins/woocommerce-blocks/package-lock.json +++ b/plugins/woocommerce-blocks/package-lock.json @@ -1394,9 +1394,9 @@ } }, "@babel/runtime": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.0.tgz", - "integrity": "sha512-2xsuyZ0R0RBFwjgae5NpXk8FcfH4qovj5cEM5VEeB7KXnKqzaisIu2HSV/mCEISolJJuR4wkViUGYujA8MH9tw==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.2.tgz", + "integrity": "sha512-9M29wrrP7//JBGX70+IrDuD1w4iOYhUGpJNMQJVNAXue+cFeFlMTqBECouIziXPUphlgrfjcfiEpGX4t0WGK4g==", "requires": { "regenerator-runtime": "^0.13.2" } @@ -2365,6 +2365,28 @@ "react-transition-group": "2.9.0" }, "dependencies": { + "@woocommerce/navigation": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@woocommerce/navigation/-/navigation-2.1.0.tgz", + "integrity": "sha512-lxw6OQkP4mOiOIButbyOPIFLIgABkrgJEbdGi1N6+VhyWKbGsB1elKukRi7bo57FK+p/+bRFiqeDU+FeusFkcA==", + "requires": { + "@babel/runtime-corejs2": "7.4.3", + "history": "4.9.0", + "lodash": "4.17.11", + "qs": "6.7.0" + }, + "dependencies": { + "@babel/runtime-corejs2": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.4.3.tgz", + "integrity": "sha512-anTLTF7IK8Hd5f73zpPzt875I27UaaTWARJlfMGgnmQhvEe1uNHQRKBUbXL0Gc0VEYiVzsHsTPso5XdK8NGvFg==", + "requires": { + "core-js": "^2.6.5", + "regenerator-runtime": "^0.13.2" + } + } + } + }, "@wordpress/components": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/@wordpress/components/-/components-7.4.0.tgz", @@ -2605,34 +2627,22 @@ } }, "@woocommerce/navigation": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@woocommerce/navigation/-/navigation-2.1.0.tgz", - "integrity": "sha512-lxw6OQkP4mOiOIButbyOPIFLIgABkrgJEbdGi1N6+VhyWKbGsB1elKukRi7bo57FK+p/+bRFiqeDU+FeusFkcA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@woocommerce/navigation/-/navigation-2.1.1.tgz", + "integrity": "sha512-ZmYIpmjfIiDJoAUIHiXLtXi+/2e2h9TA9gCqoEJdCa11NX8gzrIkRIcXsc7E+EV/eQT0Ud5PzMccGIpOiCzS5w==", + "dev": true, "requires": { - "@babel/runtime-corejs2": "7.4.3", + "@babel/runtime-corejs2": "7.4.5", "history": "4.9.0", "lodash": "4.17.11", "qs": "6.7.0" }, "dependencies": { - "@babel/runtime-corejs2": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.4.3.tgz", - "integrity": "sha512-anTLTF7IK8Hd5f73zpPzt875I27UaaTWARJlfMGgnmQhvEe1uNHQRKBUbXL0Gc0VEYiVzsHsTPso5XdK8NGvFg==", - "requires": { - "core-js": "^2.6.5", - "regenerator-runtime": "^0.13.2" - } - }, - "core-js": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", - "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" - }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true } } }, @@ -7552,8 +7562,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -7574,14 +7583,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7596,20 +7603,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -7726,8 +7730,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -7739,7 +7742,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7754,7 +7756,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7762,14 +7763,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7788,7 +7787,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -7869,8 +7867,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -7882,7 +7879,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -7968,8 +7964,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -8005,7 +8000,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -8025,7 +8019,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -8069,14 +8062,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -11391,8 +11382,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -11413,14 +11403,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -11435,20 +11423,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -11565,8 +11550,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -11578,7 +11562,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -11593,7 +11576,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -11601,14 +11583,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -11627,7 +11607,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -11715,8 +11694,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -11728,7 +11706,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -11814,8 +11791,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -11851,7 +11827,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -11871,7 +11846,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -11915,14 +11889,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -21349,9 +21321,9 @@ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, "tiny-invariant": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.4.tgz", - "integrity": "sha512-lMhRd/djQJ3MoaHEBrw8e2/uM4rs9YMNk0iOr8rHQ0QdbM7D4l0gFl3szKdeixrlyfm9Zqi4dxHCM2qVG8ND5g==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.5.tgz", + "integrity": "sha512-BziszNEQNwtyMS9OVJia2LK9N9b6VJ35kBrvhDDDpr4hreLYqhCie15dB35uZMdqv9ZTQ55GHQtkz2FnleTHIA==" }, "tiny-lr": { "version": "1.1.1", @@ -21368,9 +21340,9 @@ } }, "tiny-warning": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.2.tgz", - "integrity": "sha512-rru86D9CpQRLvsFG5XFdy0KdLAvjdQDyZCsRcuu60WtzFylDM3eAWSxEVz5kzL2Gp544XiUvPbVKtOA/txLi9Q==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, "tinycolor2": { "version": "1.4.1", @@ -21523,6 +21495,11 @@ "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", "dev": true }, + "trim-html": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/trim-html/-/trim-html-0.1.9.tgz", + "integrity": "sha1-puYK1yFeItozcOR7bDRTp8ydSz4=" + }, "trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", diff --git a/plugins/woocommerce-blocks/package.json b/plugins/woocommerce-blocks/package.json index 5e043a2f90e..cca56626863 100644 --- a/plugins/woocommerce-blocks/package.json +++ b/plugins/woocommerce-blocks/package.json @@ -37,6 +37,7 @@ }, "devDependencies": { "@babel/core": "7.5.5", + "@woocommerce/navigation": "^2.1.1", "@wordpress/babel-preset-default": "4.4.0", "@wordpress/blocks": "6.5.0", "@wordpress/browserslist-config": "2.6.0", @@ -71,6 +72,7 @@ "eslint-plugin-jest": "22.15.1", "har-validator": "5.1.3", "husky": "2.4.1", + "ignore-loader": "^0.1.2", "interpolate-components": "1.1.1", "js-md5": "0.7.3", "lint-staged": "9.2.1", @@ -95,7 +97,8 @@ }, "dependencies": { "@woocommerce/components": "3.1.0", - "gridicons": "3.3.1" + "gridicons": "3.3.1", + "trim-html": "^0.1.9" }, "husky": { "hooks": { diff --git a/plugins/woocommerce-blocks/postcss.config.js b/plugins/woocommerce-blocks/postcss.config.js index a01d1f2ed2d..3c3296d5805 100644 --- a/plugins/woocommerce-blocks/postcss.config.js +++ b/plugins/woocommerce-blocks/postcss.config.js @@ -1,6 +1,6 @@ module.exports = ( { env } ) => ( { plugins: { - autoprefixer: {}, + autoprefixer: { grid: true }, cssnano: env === 'production', }, } ); diff --git a/plugins/woocommerce-blocks/src/Assets.php b/plugins/woocommerce-blocks/src/Assets.php index f0e8ff84ef0..ab2edef2935 100644 --- a/plugins/woocommerce-blocks/src/Assets.php +++ b/plugins/woocommerce-blocks/src/Assets.php @@ -49,6 +49,7 @@ class Assets { self::register_script( 'wc-featured-category', plugins_url( 'build/featured-category.js', __DIR__ ), array( 'wc-vendors', 'wc-blocks' ) ); self::register_script( 'wc-product-categories', plugins_url( 'build/product-categories.js', __DIR__ ), array( 'wc-vendors', 'wc-blocks' ) ); self::register_script( 'wc-product-tag', plugins_url( 'build/product-tag.js', __DIR__ ), array( 'wc-vendors', 'wc-blocks' ) ); + self::register_script( 'wc-reviews-by-product', plugins_url( 'build/reviews-by-product.js', __DIR__ ), array( 'wc-vendors', 'wc-blocks' ) ); self::register_script( 'wc-product-search', plugins_url( 'build/product-search.js', __DIR__ ), array( 'wc-vendors', 'wc-blocks' ) ); } @@ -124,21 +125,23 @@ class Assets { // Global settings used in each block. $block_settings = array( - 'min_columns' => wc_get_theme_support( 'product_blocks::min_columns', 1 ), - 'max_columns' => wc_get_theme_support( 'product_blocks::max_columns', 6 ), - 'default_columns' => wc_get_theme_support( 'product_blocks::default_columns', 3 ), - 'min_rows' => wc_get_theme_support( 'product_blocks::min_rows', 1 ), - 'max_rows' => wc_get_theme_support( 'product_blocks::max_rows', 6 ), - 'default_rows' => wc_get_theme_support( 'product_blocks::default_rows', 1 ), - 'thumbnail_size' => wc_get_theme_support( 'thumbnail_image_width', 300 ), - 'placeholderImgSrc' => wc_placeholder_img_src(), - 'min_height' => wc_get_theme_support( 'featured_block::min_height', 500 ), - 'default_height' => wc_get_theme_support( 'featured_block::default_height', 500 ), - 'isLargeCatalog' => $product_counts->publish > 200, - 'limitTags' => $tag_count > 100, - 'hasTags' => $tag_count > 0, - 'productCategories' => $product_categories, - 'homeUrl' => esc_js( home_url( '/' ) ), + 'min_columns' => wc_get_theme_support( 'product_blocks::min_columns', 1 ), + 'max_columns' => wc_get_theme_support( 'product_blocks::max_columns', 6 ), + 'default_columns' => wc_get_theme_support( 'product_blocks::default_columns', 3 ), + 'min_rows' => wc_get_theme_support( 'product_blocks::min_rows', 1 ), + 'max_rows' => wc_get_theme_support( 'product_blocks::max_rows', 6 ), + 'default_rows' => wc_get_theme_support( 'product_blocks::default_rows', 1 ), + 'thumbnail_size' => wc_get_theme_support( 'thumbnail_image_width', 300 ), + 'placeholderImgSrc' => wc_placeholder_img_src(), + 'min_height' => wc_get_theme_support( 'featured_block::min_height', 500 ), + 'default_height' => wc_get_theme_support( 'featured_block::default_height', 500 ), + 'isLargeCatalog' => $product_counts->publish > 200, + 'limitTags' => $tag_count > 100, + 'hasTags' => $tag_count > 0, + 'productCategories' => $product_categories, + 'homeUrl' => esc_js( home_url( '/' ) ), + 'showAvatars' => '1' === get_option( 'show_avatars' ), + 'enableReviewRating' => 'yes' === get_option( 'woocommerce_enable_review_rating' ), ); ?>