This commit is contained in:
KhaiNguyen
2020-02-13 10:39:37 +07:00
commit 59401cb805
12867 changed files with 4646216 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { InspectorControls } from '@wordpress/editor';
import { PanelBody, ToggleControl } from '@wordpress/components';
import { Fragment } from '@wordpress/element';
import PropTypes from 'prop-types';
import { IconAllReviews } from '@woocommerce/block-components/icons';
/**
* Internal dependencies
*/
import EditorContainerBlock from '../editor-container-block.js';
import NoReviewsPlaceholder from './no-reviews-placeholder.js';
import {
getSharedReviewContentControls,
getSharedReviewListControls,
} from '../edit-utils.js';
/**
* Component to handle edit mode of "All Reviews".
*/
const AllReviewsEditor = ( { attributes, setAttributes } ) => {
const getInspectorControls = () => {
return (
<InspectorControls key="inspector">
<PanelBody
title={ __( 'Content', 'woocommerce' ) }
>
<ToggleControl
label={ __(
'Product name',
'woocommerce'
) }
checked={ attributes.showProductName }
onChange={ () =>
setAttributes( {
showProductName: ! attributes.showProductName,
} )
}
/>
{ getSharedReviewContentControls(
attributes,
setAttributes
) }
</PanelBody>
<PanelBody
title={ __(
'List Settings',
'woocommerce'
) }
>
{ getSharedReviewListControls( attributes, setAttributes ) }
</PanelBody>
</InspectorControls>
);
};
return (
<Fragment>
{ getInspectorControls() }
<EditorContainerBlock
attributes={ attributes }
className="wc-block-all-reviews"
icon={ <IconAllReviews className="block-editor-block-icon" /> }
name={ __( 'All Reviews', 'woocommerce' ) }
noReviewsPlaceholder={ NoReviewsPlaceholder }
/>
</Fragment>
);
};
AllReviewsEditor.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,
};
export default AllReviewsEditor;

View File

@@ -0,0 +1,61 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
import { IconAllReviews } from '@woocommerce/block-components/icons';
/**
* Internal dependencies
*/
import '../editor.scss';
import Editor from './edit';
import sharedAttributes from '../attributes';
import save from '../save.js';
import { example } from '../example';
/**
* Register and run the "All Reviews" block.
*/
registerBlockType( 'woocommerce/all-reviews', {
title: __( 'All Reviews', 'woocommerce' ),
icon: {
src: <IconAllReviews />,
foreground: '#96588a',
},
category: 'woocommerce',
keywords: [ __( 'WooCommerce', 'woocommerce' ) ],
description: __(
'Shows a list of all product reviews.',
'woocommerce'
),
example: {
...example,
attributes: {
...example.attributes,
showProductName: true,
},
},
attributes: {
...sharedAttributes,
/**
* Show the product name.
*/
showProductName: {
type: 'boolean',
default: true,
},
},
/**
* Renders and manages the block.
*/
edit( props ) {
return <Editor { ...props } />;
},
/**
* Save the props to post content.
*/
save,
} );

View File

@@ -0,0 +1,23 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Placeholder } from '@wordpress/components';
import { IconAllReviews } from '@woocommerce/block-components/icons';
const NoCategoryReviewsPlaceholder = () => {
return (
<Placeholder
className="wc-block-all-reviews"
icon={ <IconAllReviews className="block-editor-block-icon" /> }
label={ __( 'All Reviews', 'woocommerce' ) }
>
{ __(
'This block shows a list of all product reviews. Your store does not have any reviews yet, but they will show up here when it does.',
'woocommerce'
) }
</Placeholder>
);
};
export default NoCategoryReviewsPlaceholder;

View File

@@ -0,0 +1,102 @@
export default {
/**
* 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',
},
/**
* 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,
},
/**
* Show the product content.
*/
showReviewContent: {
type: 'boolean',
default: true,
},
previewReviews: {
type: 'array',
default: null,
},
};

View File

@@ -0,0 +1,224 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { Fragment, RawHTML } from '@wordpress/element';
import { escapeHTML } from '@wordpress/escape-html';
import {
Notice,
ToggleControl,
Toolbar,
RangeControl,
SelectControl,
} from '@wordpress/components';
import { BlockControls } from '@wordpress/editor';
import { getAdminLink } from '@woocommerce/settings';
import {
ENABLE_REVIEW_RATING,
SHOW_AVATARS,
} from '@woocommerce/block-settings';
import ToggleButtonControl from '@woocommerce/block-components/toggle-button-control';
export const getBlockControls = ( editMode, setAttributes ) => (
<BlockControls>
<Toolbar
controls={ [
{
icon: 'edit',
title: __( 'Edit', 'woocommerce' ),
onClick: () => setAttributes( { editMode: ! editMode } ),
isActive: editMode,
},
] }
/>
</BlockControls>
);
export const getSharedReviewContentControls = ( attributes, setAttributes ) => {
return (
<Fragment>
<ToggleControl
label={ __( 'Product rating', 'woocommerce' ) }
checked={ attributes.showReviewRating }
onChange={ () =>
setAttributes( {
showReviewRating: ! attributes.showReviewRating,
} )
}
/>
{ attributes.showReviewRating && ! ENABLE_REVIEW_RATING && (
<Notice
className="wc-block-reviews__notice"
isDismissible={ false }
>
<RawHTML>
{ sprintf(
escapeHTML(
/* translators: A notice that links to WooCommerce settings. */
__(
'Product rating is disabled in your %sstore settings%s.',
'woocommerce'
)
),
`<a href="${ getAdminLink(
'admin.php?page=wc-settings&tab=products'
) }" target="_blank">`,
'</a>'
) }
</RawHTML>
</Notice>
) }
<ToggleControl
label={ __( 'Reviewer name', 'woocommerce' ) }
checked={ attributes.showReviewerName }
onChange={ () =>
setAttributes( {
showReviewerName: ! attributes.showReviewerName,
} )
}
/>
<ToggleControl
label={ __( 'Image', 'woocommerce' ) }
checked={ attributes.showReviewImage }
onChange={ () =>
setAttributes( {
showReviewImage: ! attributes.showReviewImage,
} )
}
/>
<ToggleControl
label={ __( 'Review date', 'woocommerce' ) }
checked={ attributes.showReviewDate }
onChange={ () =>
setAttributes( {
showReviewDate: ! attributes.showReviewDate,
} )
}
/>
<ToggleControl
label={ __( 'Review content', 'woocommerce' ) }
checked={ attributes.showReviewContent }
onChange={ () =>
setAttributes( {
showReviewContent: ! attributes.showReviewContent,
} )
}
/>
{ attributes.showReviewImage && (
<Fragment>
<ToggleButtonControl
label={ __(
'Review image',
'woocommerce'
) }
value={ attributes.imageType }
options={ [
{
label: __(
'Reviewer photo',
'woocommerce'
),
value: 'reviewer',
},
{
label: __(
'Product',
'woocommerce'
),
value: 'product',
},
] }
onChange={ ( value ) =>
setAttributes( { imageType: value } )
}
/>
{ attributes.imageType === 'reviewer' && ! SHOW_AVATARS && (
<Notice
className="wc-block-reviews__notice"
isDismissible={ false }
>
<RawHTML>
{ sprintf(
escapeHTML(
/* translators: A notice that links to WordPress settings. */
__(
'Reviewer photo is disabled in your %ssite settings%s.',
'woocommerce'
)
),
`<a href="${ getAdminLink(
'options-discussion.php'
) }" target="_blank">`,
'</a>'
) }
</RawHTML>
</Notice>
) }
</Fragment>
) }
</Fragment>
);
};
export const getSharedReviewListControls = ( attributes, setAttributes ) => {
const minPerPage = 1;
const maxPerPage = 20;
return (
<Fragment>
<ToggleControl
label={ __( 'Order by', 'woocommerce' ) }
checked={ attributes.showOrderby }
onChange={ () =>
setAttributes( { showOrderby: ! attributes.showOrderby } )
}
/>
<SelectControl
label={ __(
'Order Product Reviews by',
'woocommerce'
) }
value={ attributes.orderby }
options={ [
{ label: 'Most recent', value: 'most-recent' },
{ label: 'Highest Rating', value: 'highest-rating' },
{ label: 'Lowest Rating', value: 'lowest-rating' },
] }
onChange={ ( orderby ) => setAttributes( { orderby } ) }
/>
<RangeControl
label={ __(
'Starting Number of Reviews',
'woocommerce'
) }
value={ attributes.reviewsOnPageLoad }
onChange={ ( reviewsOnPageLoad ) =>
setAttributes( { reviewsOnPageLoad } )
}
max={ maxPerPage }
min={ minPerPage }
/>
<ToggleControl
label={ __( 'Load more', 'woocommerce' ) }
checked={ attributes.showLoadMore }
onChange={ () =>
setAttributes( { showLoadMore: ! attributes.showLoadMore } )
}
/>
{ attributes.showLoadMore && (
<RangeControl
label={ __(
'Load More Reviews',
'woocommerce'
) }
value={ attributes.reviewsOnLoadMore }
onChange={ ( reviewsOnLoadMore ) =>
setAttributes( { reviewsOnLoadMore } )
}
max={ maxPerPage }
min={ minPerPage }
/>
) }
</Fragment>
);
};

View File

@@ -0,0 +1,72 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component } from 'react';
import PropTypes from 'prop-types';
import { Disabled } from '@wordpress/components';
import { ENABLE_REVIEW_RATING } from '@woocommerce/block-settings';
import ErrorPlaceholder from '@woocommerce/block-components/error-placeholder';
import LoadMoreButton from '@woocommerce/base-components/load-more-button';
import ReviewList from '@woocommerce/base-components/review-list';
import ReviewSortSelect from '@woocommerce/base-components/review-sort-select';
import withReviews from '@woocommerce/base-hocs/with-reviews';
/**
* Block rendered in the editor.
*/
class EditorBlock extends Component {
static propTypes = {
/**
* The attributes for this block.
*/
attributes: PropTypes.object.isRequired,
// from withReviews
reviews: PropTypes.array,
totalReviews: PropTypes.number,
};
render() {
const {
attributes,
error,
isLoading,
noReviewsPlaceholder: NoReviewsPlaceholder,
reviews,
totalReviews,
} = this.props;
if ( error ) {
return (
<ErrorPlaceholder
className="wc-block-featured-product-error"
error={ error }
isLoading={ isLoading }
/>
);
}
if ( reviews.length === 0 && ! isLoading ) {
return <NoReviewsPlaceholder attributes={ attributes } />;
}
return (
<Disabled>
{ attributes.showOrderby && ENABLE_REVIEW_RATING && (
<ReviewSortSelect readOnly value={ attributes.orderby } />
) }
<ReviewList attributes={ attributes } reviews={ reviews } />
{ attributes.showLoadMore && totalReviews > reviews.length && (
<LoadMoreButton
screenReaderLabel={ __(
'Load more reviews',
'woocommerce'
) }
/>
) }
</Disabled>
);
}
}
export default withReviews( EditorBlock );

View File

@@ -0,0 +1,84 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component } from 'react';
import PropTypes from 'prop-types';
import { debounce } from 'lodash';
import { Placeholder } from '@wordpress/components';
/**
* Internal dependencies
*/
import EditorBlock from './editor-block.js';
import { getBlockClassName, getSortArgs } from './utils.js';
/**
* Container of the block rendered in the editor.
*/
class EditorContainerBlock extends Component {
renderHiddenContentPlaceholder() {
const { icon, name } = this.props;
return (
<Placeholder icon={ icon } label={ name }>
{ __(
'The content for this block is hidden due to block settings.',
'woocommerce'
) }
</Placeholder>
);
}
render() {
const { attributes, className, noReviewsPlaceholder } = this.props;
const {
categoryIds,
productId,
reviewsOnPageLoad,
showProductName,
showReviewDate,
showReviewerName,
showReviewContent,
showReviewImage,
showReviewRating,
} = attributes;
const { order, orderby } = getSortArgs( attributes.orderby );
const isAllContentHidden =
! showReviewContent &&
! showReviewRating &&
! showReviewDate &&
! showReviewerName &&
! showReviewImage &&
! showProductName;
if ( isAllContentHidden ) {
return this.renderHiddenContentPlaceholder();
}
return (
<div className={ getBlockClassName( className, attributes ) }>
<EditorBlock
attributes={ attributes }
categoryIds={ categoryIds }
delayFunction={ ( callback ) => debounce( callback, 400 ) }
noReviewsPlaceholder={ noReviewsPlaceholder }
orderby={ orderby }
order={ order }
productId={ productId }
reviewsToDisplay={ reviewsOnPageLoad }
/>
</div>
);
}
}
EditorContainerBlock.propTypes = {
attributes: PropTypes.object.isRequired,
icon: PropTypes.node.isRequired,
name: PropTypes.string.isRequired,
noReviewsPlaceholder: PropTypes.func.isRequired,
className: PropTypes.string,
};
export default EditorContainerBlock;

View File

@@ -0,0 +1,13 @@
.wc-block-reviews__selection {
width: 100%;
}
.components-base-control {
+ .wc-block-reviews__notice {
margin: -$gap 0 $gap;
}
&:nth-last-child(2) + .wc-block-reviews__notice {
margin: -$gap 0 $gap-small;
}
}

View File

@@ -0,0 +1,22 @@
/**
* External dependencies
*/
import { previewReviews } from '@woocommerce/resource-previews';
export const example = {
attributes: {
editMode: false,
imageType: 'reviewer',
orderby: 'most-recent',
reviewsOnLoadMore: 10,
reviewsOnPageLoad: 10,
showLoadMore: true,
showOrderby: true,
showReviewDate: true,
showReviewerName: true,
showReviewImage: true,
showReviewRating: true,
showReviewContent: true,
previewReviews,
},
};

View File

@@ -0,0 +1,64 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Fragment } from 'react';
import PropTypes from 'prop-types';
import { ENABLE_REVIEW_RATING } from '@woocommerce/block-settings';
import LoadMoreButton from '@woocommerce/base-components/load-more-button';
import ReviewSortSelect from '@woocommerce/base-components/review-sort-select';
import ReviewList from '@woocommerce/base-components/review-list';
import withReviews from '@woocommerce/base-hocs/with-reviews';
/**
* Block rendered in the frontend.
*/
const FrontendBlock = ( {
attributes,
onAppendReviews,
onChangeOrderby,
reviews,
totalReviews,
} ) => {
const { orderby } = attributes;
if ( reviews.length === 0 ) {
return null;
}
return (
<Fragment>
{ attributes.showOrderby !== 'false' && ENABLE_REVIEW_RATING && (
<ReviewSortSelect
defaultValue={ orderby }
onChange={ onChangeOrderby }
/>
) }
<ReviewList attributes={ attributes } reviews={ reviews } />
{ attributes.showLoadMore !== 'false' &&
totalReviews > reviews.length && (
<LoadMoreButton
onClick={ onAppendReviews }
screenReaderLabel={ __(
'Load more reviews',
'woocommerce'
) }
/>
) }
</Fragment>
);
};
FrontendBlock.propTypes = {
/**
* The attributes for this block.
*/
attributes: PropTypes.object.isRequired,
onAppendReviews: PropTypes.func,
onChangeArgs: PropTypes.func,
// from withReviewsattributes
reviews: PropTypes.array,
totalReviews: PropTypes.number,
};
export default withReviews( FrontendBlock );

View File

@@ -0,0 +1,109 @@
/**
* 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 { getSortArgs } 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,
'woocommerce'
),
newReviews.length
)
);
}
onReviewsReplaced() {
speak( __( 'Reviews list updated.', 'woocommerce' ) );
}
onReviewsLoadError() {
speak(
__(
'There was an error loading the reviews.',
'woocommerce'
)
);
}
render() {
const { attributes } = this.props;
const { categoryIds, productId } = attributes;
const { reviewsToDisplay } = this.state;
const { order, orderby } = getSortArgs( 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;

View File

@@ -0,0 +1,26 @@
/**
* Internal dependencies
*/
import FrontendContainerBlock from './frontend-container-block.js';
import renderFrontend from '../../utils/render-frontend.js';
const selector = `
.wp-block-woocommerce-all-reviews,
.wp-block-woocommerce-reviews-by-product,
.wp-block-woocommerce-reviews-by-category
`;
const getProps = ( el ) => {
return {
attributes: {
showReviewDate: el.classList.contains( 'has-date' ),
showReviewerName: el.classList.contains( 'has-name' ),
showReviewImage: el.classList.contains( 'has-image' ),
showReviewRating: el.classList.contains( 'has-rating' ),
showReviewContent: el.classList.contains( 'has-content' ),
showProductName: el.classList.contains( 'has-product-name' ),
},
};
};
renderFrontend( selector, FrontendContainerBlock, getProps );

View File

@@ -0,0 +1,204 @@
/**
* External dependencies
*/
import { __, _n, sprintf } from '@wordpress/i18n';
import { InspectorControls } from '@wordpress/editor';
import {
Button,
PanelBody,
Placeholder,
ToggleControl,
withSpokenMessages,
} from '@wordpress/components';
import { SearchListItem } from '@woocommerce/components';
import { Fragment } from '@wordpress/element';
import PropTypes from 'prop-types';
import ProductCategoryControl from '@woocommerce/block-components/product-category-control';
import { IconReviewsByCategory } from '@woocommerce/block-components/icons';
/**
* Internal dependencies
*/
import EditorContainerBlock from '../editor-container-block.js';
import NoReviewsPlaceholder from './no-reviews-placeholder.js';
import {
getBlockControls,
getSharedReviewContentControls,
getSharedReviewListControls,
} from '../edit-utils.js';
/**
* Component to handle edit mode of "Reviews by Category".
*/
const ReviewsByCategoryEditor = ( {
attributes,
debouncedSpeak,
setAttributes,
} ) => {
const { editMode, categoryIds } = attributes;
const renderCategoryControlItem = ( args ) => {
const { item, search, depth = 0 } = args;
const classes = [ 'woocommerce-product-categories__item' ];
if ( search.length ) {
classes.push( 'is-searching' );
}
if ( depth === 0 && item.parent !== 0 ) {
classes.push( 'is-skip-level' );
}
const accessibleName = ! item.breadcrumbs.length
? item.name
: `${ item.breadcrumbs.join( ', ' ) }, ${ item.name }`;
return (
<SearchListItem
className={ classes.join( ' ' ) }
{ ...args }
showCount
aria-label={ sprintf(
_n(
'%s, has %d product',
'%s, has %d products',
item.count,
'woocommerce'
),
accessibleName,
item.count
) }
/>
);
};
const getInspectorControls = () => {
return (
<InspectorControls key="inspector">
<PanelBody
title={ __( 'Category', 'woocommerce' ) }
initialOpen={ false }
>
<ProductCategoryControl
selected={ attributes.categoryIds }
onChange={ ( value = [] ) => {
const ids = value.map( ( { id } ) => id );
setAttributes( { categoryIds: ids } );
} }
renderItem={ renderCategoryControlItem }
/>
</PanelBody>
<PanelBody
title={ __( 'Content', 'woocommerce' ) }
>
<ToggleControl
label={ __(
'Product name',
'woocommerce'
) }
checked={ attributes.showProductName }
onChange={ () =>
setAttributes( {
showProductName: ! attributes.showProductName,
} )
}
/>
{ getSharedReviewContentControls(
attributes,
setAttributes
) }
</PanelBody>
<PanelBody
title={ __(
'List Settings',
'woocommerce'
) }
>
{ getSharedReviewListControls( attributes, setAttributes ) }
</PanelBody>
</InspectorControls>
);
};
const renderEditMode = () => {
const onDone = () => {
setAttributes( { editMode: false } );
debouncedSpeak(
__(
'Showing Reviews by Category block preview.',
'woocommerce'
)
);
};
return (
<Placeholder
icon={
<IconReviewsByCategory className="block-editor-block-icon" />
}
label={ __(
'Reviews by Category',
'woocommerce'
) }
>
{ __(
'Show product reviews from specific categories.',
'woocommerce'
) }
<div className="wc-block-reviews__selection">
<ProductCategoryControl
selected={ attributes.categoryIds }
onChange={ ( value = [] ) => {
const ids = value.map( ( { id } ) => id );
setAttributes( { categoryIds: ids } );
} }
showReviewCount={ true }
/>
<Button isDefault onClick={ onDone }>
{ __( 'Done', 'woocommerce' ) }
</Button>
</div>
</Placeholder>
);
};
if ( ! categoryIds || editMode ) {
return renderEditMode();
}
return (
<Fragment>
{ getBlockControls( editMode, setAttributes ) }
{ getInspectorControls() }
<EditorContainerBlock
attributes={ attributes }
className="wc-block-reviews-by-category"
icon={
<IconReviewsByCategory className="block-editor-block-icon" />
}
name={ __(
'Reviews by Category',
'woocommerce'
) }
noReviewsPlaceholder={ NoReviewsPlaceholder }
/>
</Fragment>
);
};
ReviewsByCategoryEditor.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 withSpokenMessages
debouncedSpeak: PropTypes.func.isRequired,
};
export default withSpokenMessages( ReviewsByCategoryEditor );

View File

@@ -0,0 +1,3 @@
.wc-block-reviews-by-category__selection {
width: 100%;
}

View File

@@ -0,0 +1,69 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
import { IconReviewsByCategory } from '@woocommerce/block-components/icons';
/**
* Internal dependencies
*/
import '../editor.scss';
import Editor from './edit';
import sharedAttributes from '../attributes';
import save from '../save.js';
import { example } from '../example';
/**
* Register and run the "Reviews by category" block.
*/
registerBlockType( 'woocommerce/reviews-by-category', {
title: __( 'Reviews by Category', 'woocommerce' ),
icon: {
src: <IconReviewsByCategory />,
foreground: '#96588a',
},
category: 'woocommerce',
keywords: [ __( 'WooCommerce', 'woocommerce' ) ],
description: __(
'Show product reviews from specific categories.',
'woocommerce'
),
example: {
...example,
attributes: {
...example.attributes,
categoryIds: [ 1 ],
showProductName: true,
},
},
attributes: {
...sharedAttributes,
/**
* The ids of the categories to load reviews for.
*/
categoryIds: {
type: 'array',
default: [],
},
/**
* Show the product name.
*/
showProductName: {
type: 'boolean',
default: true,
},
},
/**
* Renders and manages the block.
*/
edit( props ) {
return <Editor { ...props } />;
},
/**
* Save the props to post content.
*/
save,
} );

View File

@@ -0,0 +1,28 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Placeholder } from '@wordpress/components';
import { IconReviewsByCategory } from '@woocommerce/block-components/icons';
const NoReviewsPlaceholder = () => {
return (
<Placeholder
className="wc-block-reviews-by-category"
icon={
<IconReviewsByCategory className="block-editor-block-icon" />
}
label={ __(
'Reviews by Category',
'woocommerce'
) }
>
{ __(
'This block lists reviews for products from selected categories. The selected categories do not have any reviews yet, but they will show up here when they do.',
'woocommerce'
) }
</Placeholder>
);
};
export default NoReviewsPlaceholder;

View File

@@ -0,0 +1,192 @@
/**
* External dependencies
*/
import { __, _n, sprintf } from '@wordpress/i18n';
import { InspectorControls } from '@wordpress/editor';
import {
Button,
PanelBody,
Placeholder,
withSpokenMessages,
} from '@wordpress/components';
import { SearchListItem } from '@woocommerce/components';
import { Fragment } from '@wordpress/element';
import PropTypes from 'prop-types';
import ProductControl from '@woocommerce/block-components/product-control';
import { IconReviewsByProduct } from '@woocommerce/block-components/icons';
/**
* Internal dependencies
*/
import EditorContainerBlock from '../editor-container-block.js';
import NoReviewsPlaceholder from './no-reviews-placeholder.js';
import {
getBlockControls,
getSharedReviewContentControls,
getSharedReviewListControls,
} from '../edit-utils.js';
/**
* Component to handle edit mode of "Reviews by Product".
*/
const ReviewsByProductEditor = ( {
attributes,
debouncedSpeak,
setAttributes,
} ) => {
const { editMode, productId } = attributes;
const renderProductControlItem = ( args ) => {
const { item = 0 } = args;
return (
<SearchListItem
{ ...args }
countLabel={ sprintf(
_n(
'%d Review',
'%d Reviews',
item.review_count,
'woocommerce'
),
item.review_count
) }
showCount
aria-label={ sprintf(
_n(
'%s, has %d review',
'%s, has %d reviews',
item.review_count,
'woocommerce'
),
item.name,
item.review_count
) }
/>
);
};
const getInspectorControls = () => {
return (
<InspectorControls key="inspector">
<PanelBody
title={ __( 'Product', 'woocommerce' ) }
initialOpen={ false }
>
<ProductControl
selected={ attributes.productId || 0 }
onChange={ ( value = [] ) => {
const id = value[ 0 ] ? value[ 0 ].id : 0;
setAttributes( { productId: id } );
} }
renderItem={ renderProductControlItem }
/>
</PanelBody>
<PanelBody
title={ __( 'Content', 'woocommerce' ) }
>
{ getSharedReviewContentControls(
attributes,
setAttributes
) }
</PanelBody>
<PanelBody
title={ __(
'List Settings',
'woocommerce'
) }
>
{ getSharedReviewListControls( attributes, setAttributes ) }
</PanelBody>
</InspectorControls>
);
};
const renderEditMode = () => {
const onDone = () => {
setAttributes( { editMode: false } );
debouncedSpeak(
__(
'Showing Reviews by Product block preview.',
'woocommerce'
)
);
};
return (
<Placeholder
icon={
<IconReviewsByProduct className="block-editor-block-icon" />
}
label={ __(
'Reviews by Product',
'woocommerce'
) }
>
{ __(
'Show reviews of your product to build trust',
'woocommerce'
) }
<div className="wc-block-reviews__selection">
<ProductControl
selected={ attributes.productId || 0 }
onChange={ ( value = [] ) => {
const id = value[ 0 ] ? value[ 0 ].id : 0;
setAttributes( { productId: id } );
} }
queryArgs={ {
orderby: 'comment_count',
order: 'desc',
} }
renderItem={ renderProductControlItem }
/>
<Button isDefault onClick={ onDone }>
{ __( 'Done', 'woocommerce' ) }
</Button>
</div>
</Placeholder>
);
};
if ( ! productId || editMode ) {
return renderEditMode();
}
return (
<Fragment>
{ getBlockControls( editMode, setAttributes ) }
{ getInspectorControls() }
<EditorContainerBlock
attributes={ attributes }
className="wc-block-all-reviews"
icon={
<IconReviewsByProduct className="block-editor-block-icon" />
}
name={ __(
'Reviews by Product',
'woocommerce'
) }
noReviewsPlaceholder={ NoReviewsPlaceholder }
/>
</Fragment>
);
};
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 withSpokenMessages
debouncedSpeak: PropTypes.func.isRequired,
};
export default withSpokenMessages( ReviewsByProductEditor );

View File

@@ -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;
}
}

View File

@@ -0,0 +1,60 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
import { IconReviewsByProduct } from '@woocommerce/block-components/icons';
/**
* Internal dependencies
*/
import '../editor.scss';
import Editor from './edit';
import sharedAttributes from '../attributes';
import save from '../save.js';
import { example } from '../example';
/**
* Register and run the "Reviews by Product" block.
*/
registerBlockType( 'woocommerce/reviews-by-product', {
title: __( 'Reviews by Product', 'woocommerce' ),
icon: {
src: <IconReviewsByProduct />,
foreground: '#96588a',
},
category: 'woocommerce',
keywords: [ __( 'WooCommerce', 'woocommerce' ) ],
description: __(
'Show reviews of your product to build trust.',
'woocommerce'
),
example: {
...example,
attributes: {
...example.attributes,
productId: 1,
},
},
attributes: {
...sharedAttributes,
/**
* The id of the product to load reviews for.
*/
productId: {
type: 'number',
},
},
/**
* Renders and manages the block.
*/
edit( props ) {
return <Editor { ...props } />;
},
/**
* Save the props to post content.
*/
save,
} );

View File

@@ -0,0 +1,61 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { Placeholder, Spinner } from '@wordpress/components';
import PropTypes from 'prop-types';
import ErrorPlaceholder from '@woocommerce/block-components/error-placeholder';
import { IconReviewsByProduct } from '@woocommerce/block-components/icons';
import { withProduct } from '@woocommerce/block-hocs';
const NoReviewsPlaceholder = ( { error, getProduct, isLoading, product } ) => {
const renderApiError = () => (
<ErrorPlaceholder
className="wc-block-featured-product-error"
error={ error }
isLoading={ isLoading }
onRetry={ getProduct }
/>
);
if ( error ) {
return renderApiError();
}
const content =
! product || isLoading ? (
<Spinner />
) : (
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.",
'woocommerce'
),
product.name
)
);
return (
<Placeholder
className="wc-block-reviews-by-product"
icon={
<IconReviewsByProduct className="block-editor-block-icon" />
}
label={ __( 'Reviews by Product', 'woocommerce' ) }
>
{ content }
</Placeholder>
);
};
NoReviewsPlaceholder.propTypes = {
// from withProduct
error: PropTypes.object,
isLoading: PropTypes.bool,
product: PropTypes.shape( {
name: PropTypes.node,
review_count: PropTypes.number,
} ),
};
export default withProduct( NoReviewsPlaceholder );

View File

@@ -0,0 +1,45 @@
/**
* Internal dependencies
*/
import './editor.scss';
import { getBlockClassName } from './utils.js';
export default ( { attributes } ) => {
const {
categoryIds,
imageType,
orderby,
productId,
reviewsOnPageLoad,
reviewsOnLoadMore,
showLoadMore,
showOrderby,
} = attributes;
const data = {
'data-image-type': imageType,
'data-orderby': orderby,
'data-reviews-on-page-load': reviewsOnPageLoad,
'data-reviews-on-load-more': reviewsOnLoadMore,
'data-show-load-more': showLoadMore,
'data-show-orderby': showOrderby,
};
let className = 'wc-block-all-reviews';
if ( productId ) {
data[ 'data-product-id' ] = productId;
className = 'wc-block-reviews-by-product';
}
if ( Array.isArray( categoryIds ) ) {
data[ 'data-category-ids' ] = categoryIds.join( ',' );
className = 'wc-block-reviews-by-category';
}
return (
<div
className={ getBlockClassName( className, attributes ) }
{ ...data }
/>
);
};

View File

@@ -0,0 +1,68 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import classNames from 'classnames';
import { ENABLE_REVIEW_RATING } from '@woocommerce/block-settings';
export const getSortArgs = ( sortValue ) => {
if ( ENABLE_REVIEW_RATING ) {
if ( sortValue === 'lowest-rating' ) {
return {
order: 'asc',
orderby: 'rating',
};
}
if ( sortValue === '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 };
} );
} );
};
export const getBlockClassName = ( blockClassName, attributes ) => {
const {
className,
showReviewDate,
showReviewerName,
showReviewContent,
showProductName,
showReviewImage,
showReviewRating,
} = attributes;
return classNames( blockClassName, className, {
'has-image': showReviewImage,
'has-name': showReviewerName,
'has-date': showReviewDate,
'has-rating': showReviewRating,
'has-content': showReviewContent,
'has-product-name': showProductName,
} );
};