initial commit

This commit is contained in:
2021-12-10 12:03:04 +00:00
commit c46c7ddbf0
3643 changed files with 582794 additions and 0 deletions

View File

@ -0,0 +1,96 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl } from '@wordpress/components';
import PropTypes from 'prop-types';
import { Icon, discussion } from '@woocommerce/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".
*
* @param {Object} props Incoming props for the component.
* @param {Object} props.attributes Incoming block attributes.
* @param {function(any):any} props.setAttributes Setter for block attributes.
*/
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 (
<>
{ getInspectorControls() }
<EditorContainerBlock
attributes={ attributes }
icon={
<Icon
icon={ discussion }
className="block-editor-block-icon"
/>
}
name={ __( 'All Reviews', 'woocommerce' ) }
noReviewsPlaceholder={ NoReviewsPlaceholder }
/>
</>
);
};
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,84 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createBlock, registerBlockType } from '@wordpress/blocks';
import { Icon, discussion } from '@woocommerce/icons';
/**
* Internal dependencies
*/
import '../editor.scss';
import edit from './edit';
import sharedAttributes from '../attributes';
import save from '../save.js';
import { example } from '../example';
/**
* Register and run the "All Reviews" block.
* This block lists all product reviews.
*/
registerBlockType( 'woocommerce/all-reviews', {
apiVersion: 2,
title: __( 'All Reviews', 'woocommerce' ),
icon: {
src: <Icon srcElement={ discussion } />,
foreground: '#96588a',
},
category: 'woocommerce',
keywords: [ __( 'WooCommerce', 'woocommerce' ) ],
description: __(
'Show a list of all product reviews.',
'woocommerce'
),
supports: {
html: false,
color: {
background: false,
},
typography: {
fontSize: true,
},
},
example: {
...example,
attributes: {
...example.attributes,
showProductName: true,
},
},
attributes: {
...sharedAttributes,
/**
* Show the product name.
*/
showProductName: {
type: 'boolean',
default: true,
},
},
transforms: {
from: [
{
type: 'block',
blocks: [ 'core/legacy-widget' ],
// We can't transform if raw instance isn't shown in the REST API.
isMatch: ( { idBase, instance } ) =>
idBase === 'woocommerce_recent_reviews' && !! instance?.raw,
transform: ( { instance } ) =>
createBlock( 'woocommerce/all-reviews', {
reviewsOnPageLoad: instance.raw.number,
imageType: 'product',
showLoadMore: false,
showOrderby: false,
showReviewDate: false,
showReviewContent: false,
} ),
},
],
},
edit,
save,
} );

View File

@ -0,0 +1,28 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Placeholder } from '@wordpress/components';
import { Icon, discussion } from '@woocommerce/icons';
const NoCategoryReviewsPlaceholder = () => {
return (
<Placeholder
className="wc-block-all-reviews"
icon={
<Icon
srcElement={ discussion }
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,227 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createInterpolateElement } from '@wordpress/element';
import {
Notice,
ToggleControl,
ToolbarGroup,
RangeControl,
SelectControl,
} from '@wordpress/components';
import { BlockControls } from '@wordpress/block-editor';
import { getAdminLink, getSetting } from '@woocommerce/settings';
import ToggleButtonControl from '@woocommerce/editor-components/toggle-button-control';
export const getBlockControls = ( editMode, setAttributes ) => (
<BlockControls>
<ToolbarGroup
controls={ [
{
icon: 'edit',
title: __( 'Edit', 'woocommerce' ),
onClick: () => setAttributes( { editMode: ! editMode } ),
isActive: editMode,
},
] }
/>
</BlockControls>
);
export const getSharedReviewContentControls = ( attributes, setAttributes ) => {
const showAvatars = getSetting( 'showAvatars', true );
const reviewRatingsEnabled = getSetting( 'reviewRatingsEnabled', true );
return (
<>
<ToggleControl
label={ __( 'Product rating', 'woocommerce' ) }
checked={ attributes.showReviewRating }
onChange={ () =>
setAttributes( {
showReviewRating: ! attributes.showReviewRating,
} )
}
/>
{ attributes.showReviewRating && ! reviewRatingsEnabled && (
<Notice
className="wc-block-base-control-notice"
isDismissible={ false }
>
{ createInterpolateElement(
__(
'Product rating is disabled in your <a>store settings</a>.',
'woocommerce'
),
{
a: (
// eslint-disable-next-line jsx-a11y/anchor-has-content
<a
href={ getAdminLink(
'admin.php?page=wc-settings&tab=products'
) }
target="_blank"
rel="noopener noreferrer"
/>
),
}
) }
</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 && (
<>
<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' && ! showAvatars && (
<Notice
className="wc-block-base-control-notice"
isDismissible={ false }
>
{ createInterpolateElement(
__(
'Reviewer photo is disabled in your <a>site settings</a>.',
'woocommerce'
),
{
a: (
// eslint-disable-next-line jsx-a11y/anchor-has-content
<a
href={ getAdminLink(
'options-discussion.php'
) }
target="_blank"
rel="noopener noreferrer"
/>
),
}
) }
</Notice>
) }
</>
) }
</>
);
};
export const getSharedReviewListControls = ( attributes, setAttributes ) => {
const minPerPage = 1;
const maxPerPage = 20;
return (
<>
<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 }
/>
) }
</>
);
};

View File

@ -0,0 +1,76 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component } from 'react';
import PropTypes from 'prop-types';
import { Disabled } from '@wordpress/components';
import { getSetting } from '@woocommerce/settings';
import ErrorPlaceholder from '@woocommerce/editor-components/error-placeholder';
import LoadMoreButton from '@woocommerce/base-components/load-more-button';
import {
ReviewList,
ReviewSortSelect,
} from '@woocommerce/base-components/reviews';
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 } />;
}
const reviewRatingsEnabled = getSetting( 'reviewRatingsEnabled', true );
return (
<Disabled>
{ attributes.showOrderby && reviewRatingsEnabled && (
<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,81 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import PropTypes from 'prop-types';
import { debounce } from 'lodash';
import { Placeholder } from '@wordpress/components';
import { useBlockProps } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import EditorBlock from './editor-block.js';
import { getBlockClassName, getSortArgs } from './utils.js';
const EditorContainerBlock = ( {
attributes,
icon,
name,
noReviewsPlaceholder,
} ) => {
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;
const blockProps = useBlockProps( {
className: getBlockClassName( attributes ),
} );
if ( isAllContentHidden ) {
return (
<Placeholder icon={ icon } label={ name }>
{ __(
'The content for this block is hidden due to block settings.',
'woocommerce'
) }
</Placeholder>
);
}
return (
<div { ...blockProps }>
<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.element.isRequired,
className: PropTypes.string,
};
export default EditorContainerBlock;

View File

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

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,74 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import PropTypes from 'prop-types';
import { getSetting } from '@woocommerce/settings';
import LoadMoreButton from '@woocommerce/base-components/load-more-button';
import {
ReviewList,
ReviewSortSelect,
} from '@woocommerce/base-components/reviews';
import withReviews from '@woocommerce/base-hocs/with-reviews';
/**
* Block rendered in the frontend.
*
* @param {Object} props Incoming props for the component.
* @param {Object} props.attributes Incoming block attributes.
* @param {function(any):any} props.onAppendReviews Function called when appending review.
* @param {function(any):any} props.onChangeOrderby
* @param {Array} props.reviews
* @param {string} props.sortSelectValue
* @param {number} props.totalReviews
*/
const FrontendBlock = ( {
attributes,
onAppendReviews,
onChangeOrderby,
reviews,
sortSelectValue,
totalReviews,
} ) => {
if ( reviews.length === 0 ) {
return null;
}
const reviewRatingsEnabled = getSetting( 'reviewRatingsEnabled', true );
return (
<>
{ attributes.showOrderby !== 'false' && reviewRatingsEnabled && (
<ReviewSortSelect
value={ sortSelectValue }
onChange={ onChangeOrderby }
/>
) }
<ReviewList attributes={ attributes } reviews={ reviews } />
{ attributes.showLoadMore !== 'false' &&
totalReviews > reviews.length && (
<LoadMoreButton
onClick={ onAppendReviews }
screenReaderLabel={ __(
'Load more reviews',
'woocommerce'
) }
/>
) }
</>
);
};
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,111 @@
/**
* 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(
/* translators: %d is the count of reviews loaded. */
_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 }
sortSelectValue={ this.state.orderby }
/>
);
}
}
FrontendContainerBlock.propTypes = {
/**
* The attributes for this block.
*/
attributes: PropTypes.object.isRequired,
};
export default FrontendContainerBlock;

View File

@ -0,0 +1,30 @@
/**
* External dependencies
*/
import { renderFrontend } from '@woocommerce/base-utils';
/**
* Internal dependencies
*/
import FrontendContainerBlock from './frontend-container-block.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, Block: FrontendContainerBlock, getProps } );

View File

@ -0,0 +1,181 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { InspectorControls } from '@wordpress/block-editor';
import {
Button,
PanelBody,
Placeholder,
ToggleControl,
withSpokenMessages,
} from '@wordpress/components';
import PropTypes from 'prop-types';
import ProductCategoryControl from '@woocommerce/editor-components/product-category-control';
import { Icon, review } from '@woocommerce/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".
*
* @param {Object} props Incoming props for the component.
* @param {Object} props.attributes Incoming block attributes.
* @param {function(any):any} props.debouncedSpeak
* @param {function(any):any} props.setAttributes Setter for block attributes.
*/
const ReviewsByCategoryEditor = ( {
attributes,
debouncedSpeak,
setAttributes,
} ) => {
const { editMode, categoryIds } = attributes;
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 } );
} }
isCompact={ true }
showReviewCount={ true }
/>
</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={
<Icon
srcElement={ review }
className="block-editor-block-icon"
/>
}
label={ __(
'Reviews by Category',
'woocommerce'
) }
className="wc-block-reviews-by-category"
>
{ __(
'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 isPrimary onClick={ onDone }>
{ __( 'Done', 'woocommerce' ) }
</Button>
</div>
</Placeholder>
);
};
if ( ! categoryIds || editMode ) {
return renderEditMode();
}
return (
<>
{ getBlockControls( editMode, setAttributes ) }
{ getInspectorControls() }
<EditorContainerBlock
attributes={ attributes }
icon={
<Icon
srcElement={ review }
className="block-editor-block-icon"
/>
}
name={ __(
'Reviews by Category',
'woocommerce'
) }
noReviewsPlaceholder={ NoReviewsPlaceholder }
/>
</>
);
};
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,80 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
import { Icon, review } from '@woocommerce/icons';
/**
* Internal dependencies
*/
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', {
apiVersion: 2,
title: __( 'Reviews by Category', 'woocommerce' ),
icon: {
src: <Icon srcElement={ review } />,
foreground: '#96588a',
},
category: 'woocommerce',
keywords: [ __( 'WooCommerce', 'woocommerce' ) ],
description: __(
'Show product reviews from specific categories.',
'woocommerce'
),
supports: {
html: false,
color: {
background: false,
},
typography: {
fontSize: true,
},
},
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.
*
* @param {Object} props Props to pass to block.
*/
edit( props ) {
return <Editor { ...props } />;
},
/**
* Save the props to post content.
*/
save,
} );

View File

@ -0,0 +1,30 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Placeholder } from '@wordpress/components';
import { Icon, review } from '@woocommerce/icons';
const NoReviewsPlaceholder = () => {
return (
<Placeholder
className="wc-block-reviews-by-category"
icon={
<Icon
srcElement={ review }
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,204 @@
/**
* External dependencies
*/
import { __, _n, sprintf } from '@wordpress/i18n';
import { InspectorControls } from '@wordpress/block-editor';
import {
Button,
PanelBody,
Placeholder,
withSpokenMessages,
} from '@wordpress/components';
import { SearchListItem } from '@woocommerce/components';
import PropTypes from 'prop-types';
import ProductControl from '@woocommerce/editor-components/product-control';
import { Icon, comment } from '@woocommerce/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".
*
* @param {Object} props Incoming props for the component.
* @param {Object} props.attributes Incoming block attributes.
* @param {function(any):any} props.debouncedSpeak
* @param {function(any):any} props.setAttributes Setter for block attributes.
*/
const ReviewsByProductEditor = ( {
attributes,
debouncedSpeak,
setAttributes,
} ) => {
const { editMode, productId } = attributes;
const renderProductControlItem = ( args ) => {
const { item = 0 } = args;
return (
<SearchListItem
{ ...args }
countLabel={ sprintf(
/* translators: %d is the review count. */
_n(
'%d review',
'%d reviews',
item.review_count,
'woocommerce'
),
item.review_count
) }
aria-label={ sprintf(
/* translators: %1$s is the item name, and %2$d is the number of reviews for the item. */
_n(
'%1$s, has %2$d review',
'%1$s, has %2$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 }
isCompact={ true }
/>
</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={
<Icon
icon={ comment }
className="block-editor-block-icon"
/>
}
label={ __(
'Reviews by Product',
'woocommerce'
) }
className="wc-block-reviews-by-product"
>
{ __(
'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 isPrimary onClick={ onDone }>
{ __( 'Done', 'woocommerce' ) }
</Button>
</div>
</Placeholder>
);
};
if ( ! productId || editMode ) {
return renderEditMode();
}
return (
<>
{ getBlockControls( editMode, setAttributes ) }
{ getInspectorControls() }
<EditorContainerBlock
attributes={ attributes }
icon={
<Icon
icon={ comment }
className="block-editor-block-icon"
/>
}
name={ __(
'Reviews by Product',
'woocommerce'
) }
noReviewsPlaceholder={ NoReviewsPlaceholder }
/>
</>
);
};
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,9 @@
.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,71 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
import { Icon, comment } from '@woocommerce/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', {
apiVersion: 2,
title: __( 'Reviews by Product', 'woocommerce' ),
icon: {
src: <Icon srcElement={ comment } />,
foreground: '#96588a',
},
category: 'woocommerce',
keywords: [ __( 'WooCommerce', 'woocommerce' ) ],
description: __(
'Show reviews of your products to build trust.',
'woocommerce'
),
supports: {
html: false,
color: {
background: false,
},
typography: {
fontSize: true,
},
},
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.
*
* @param {Object} props Props to pass to block.
*/
edit( props ) {
return <Editor { ...props } />;
},
/**
* Save the props to post content.
*/
save,
} );

View File

@ -0,0 +1,65 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { Placeholder, Spinner } from '@wordpress/components';
import PropTypes from 'prop-types';
import ErrorPlaceholder from '@woocommerce/editor-components/error-placeholder';
import { Icon, comment } from '@woocommerce/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(
/* translators: %s is the product name. */
__(
"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={
<Icon
srcElement={ comment }
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,21 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import './editor.scss';
import { getBlockClassName, getDataAttrs } from './utils.js';
export default ( { attributes } ) => {
return (
<div
{ ...useBlockProps.save( {
className: getBlockClassName( attributes ),
} ) }
{ ...getDataAttrs( attributes ) }
/>
);
};

View File

@ -0,0 +1,114 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import classNames from 'classnames';
import { getSetting } from '@woocommerce/settings';
export const getSortArgs = ( sortValue ) => {
const reviewRatingsEnabled = getSetting( 'reviewRatingsEnabled', true );
if ( reviewRatingsEnabled ) {
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/store/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 = ( attributes ) => {
const {
className,
categoryIds,
productId,
showReviewDate,
showReviewerName,
showReviewContent,
showProductName,
showReviewImage,
showReviewRating,
} = attributes;
let blockClassName = 'wc-block-all-reviews';
if ( productId ) {
blockClassName = 'wc-block-reviews-by-product';
}
if ( Array.isArray( categoryIds ) ) {
blockClassName = 'wc-block-reviews-by-category';
}
return classNames( blockClassName, className, {
'has-image': showReviewImage,
'has-name': showReviewerName,
'has-date': showReviewDate,
'has-rating': showReviewRating,
'has-content': showReviewContent,
'has-product-name': showProductName,
} );
};
export const getDataAttrs = ( 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,
};
if ( productId ) {
data[ 'data-product-id' ] = productId;
}
if ( Array.isArray( categoryIds ) ) {
data[ 'data-category-ids' ] = categoryIds.join( ',' );
}
return data;
};