initial commit
This commit is contained in:
@ -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;
|
@ -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,
|
||||
} );
|
@ -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;
|
@ -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,
|
||||
},
|
||||
};
|
@ -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 }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
@ -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 );
|
@ -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;
|
@ -0,0 +1,3 @@
|
||||
.wc-block-reviews__selection {
|
||||
width: 100%;
|
||||
}
|
@ -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,
|
||||
},
|
||||
};
|
@ -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 );
|
@ -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;
|
@ -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 } );
|
@ -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 );
|
@ -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,
|
||||
} );
|
@ -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;
|
@ -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 );
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
} );
|
@ -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 );
|
21
packages/woocommerce-blocks/assets/js/blocks/reviews/save.js
Normal file
21
packages/woocommerce-blocks/assets/js/blocks/reviews/save.js
Normal 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 ) }
|
||||
/>
|
||||
);
|
||||
};
|
114
packages/woocommerce-blocks/assets/js/blocks/reviews/utils.js
Normal file
114
packages/woocommerce-blocks/assets/js/blocks/reviews/utils.js
Normal 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;
|
||||
};
|
Reference in New Issue
Block a user