Migrate reviews panel to home screen (https://github.com/woocommerce/woocommerce-admin/pull/5706)
* Create initial reviews panel and displaying it on the home screen * Update reviews package to support updating and deleting reviews * Allow custom icons to be defined for rating component * Add approve, spam, and delete actions to home screen review panel * Show entire list as updating when items are still in the store * Update rating to only import the required icons, and allow icons to be passed in instead of strings * Prune out reviews header panel, as we are not using it anymore * Showing just a header if collapsible is false for activity panel * Add tests for reviews panel and accordion changes * Fix undoing a deleted item by using status - untrash * Several styling changes to match wireframe as mentioned in PR review * Moved review rating into the subtitle in relation to new design * Update clear cache logic for last item * Remove activity panel unused css * Use invalideResolution instead of invalidateResolutionForStoreSelector
This commit is contained in:
parent
e778a3c0fc
commit
98a55aaeb9
|
@ -1,8 +1,7 @@
|
||||||
Activity Panel
|
# Activity Panel
|
||||||
======
|
|
||||||
|
|
||||||
This component contains the Activity Panel. This is shown on every page and is rendered as part of the header.
|
This component contains the Activity Panel. This is shown on every page and is rendered as part of the header.
|
||||||
It provides access to the notices system and actionable items like reviews and stock.
|
It provides access to the notices system and actionable items like stock.
|
||||||
|
|
||||||
## Components
|
## Components
|
||||||
|
|
||||||
|
|
|
@ -142,6 +142,11 @@
|
||||||
height: 24px;
|
height: 24px;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
@include font-size( 11 );
|
@include font-size( 11 );
|
||||||
|
&.is-destructive {
|
||||||
|
&:not(:hover) {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,61 +261,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-review-activity-card {
|
|
||||||
.woocommerce-activity-card__body > span > p {
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.woocommerce-review-activity-card__verified {
|
|
||||||
margin-left: $gap-small;
|
|
||||||
display: inline-flex;
|
|
||||||
position: relative;
|
|
||||||
top: $gap-smallest;
|
|
||||||
color: $valid-green;
|
|
||||||
@include font-size( 12 );
|
|
||||||
|
|
||||||
.gridicon {
|
|
||||||
margin-right: $gap-smallest;
|
|
||||||
fill: $valid-green;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.woocommerce-review-activity-card__image-overlay {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
img.woocommerce-gravatar {
|
|
||||||
border: 2px solid $studio-white;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
top: -6px;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include breakpoint( '<782px' ) {
|
|
||||||
.woocommerce-review-activity-card__image-overlay {
|
|
||||||
margin-top: $gap-smallest;
|
|
||||||
}
|
|
||||||
|
|
||||||
.woocommerce-review-activity-card__image-overlay__product
|
|
||||||
.woocommerce-gravatar {
|
|
||||||
margin-left: 0;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
left: 32px;
|
|
||||||
top: -28px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.woocommerce-review-activity-card__image-overlay__product,
|
|
||||||
.woocommerce-stock-activity-card__image-overlay__product {
|
.woocommerce-stock-activity-card__image-overlay__product {
|
||||||
height: 33px;
|
height: 33px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { uniqueId, find } from 'lodash';
|
||||||
import CrossIcon from 'gridicons/dist/cross-small';
|
import CrossIcon from 'gridicons/dist/cross-small';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Icon, help as helpIcon } from '@wordpress/icons';
|
import { Icon, help as helpIcon } from '@wordpress/icons';
|
||||||
import { getSetting, getAdminLink } from '@woocommerce/wc-admin-settings';
|
import { getAdminLink } from '@woocommerce/wc-admin-settings';
|
||||||
import { H, Section, Spinner } from '@woocommerce/components';
|
import { H, Section, Spinner } from '@woocommerce/components';
|
||||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||||
import { getHistory, getNewPath } from '@woocommerce/navigation';
|
import { getHistory, getNewPath } from '@woocommerce/navigation';
|
||||||
|
@ -21,7 +21,7 @@ import { getHistory, getNewPath } from '@woocommerce/navigation';
|
||||||
*/
|
*/
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
import ActivityPanelToggleBubble from './toggle-bubble';
|
import ActivityPanelToggleBubble from './toggle-bubble';
|
||||||
import { getUnreadNotes, getUnapprovedReviews } from './unread-indicators';
|
import { getUnreadNotes } from './unread-indicators';
|
||||||
import { isWCAdmin } from '../../dashboard/utils';
|
import { isWCAdmin } from '../../dashboard/utils';
|
||||||
import { Tabs } from './tabs';
|
import { Tabs } from './tabs';
|
||||||
import { SetupProgress } from './setup-progress';
|
import { SetupProgress } from './setup-progress';
|
||||||
|
@ -37,11 +37,6 @@ const InboxPanel = lazy( () =>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const ReviewsPanel = lazy( () =>
|
|
||||||
import( /* webpackChunkName: "activity-panels-inbox" */ './panels/reviews' )
|
|
||||||
);
|
|
||||||
|
|
||||||
const reviewsEnabled = getSetting( 'reviewsEnabled', 'no' );
|
|
||||||
export class ActivityPanel extends Component {
|
export class ActivityPanel extends Component {
|
||||||
constructor( props ) {
|
constructor( props ) {
|
||||||
super( props );
|
super( props );
|
||||||
|
@ -132,7 +127,6 @@ export class ActivityPanel extends Component {
|
||||||
getTabs() {
|
getTabs() {
|
||||||
const {
|
const {
|
||||||
hasUnreadNotes,
|
hasUnreadNotes,
|
||||||
hasUnapprovedReviews,
|
|
||||||
isEmbedded,
|
isEmbedded,
|
||||||
taskListComplete,
|
taskListComplete,
|
||||||
taskListHidden,
|
taskListHidden,
|
||||||
|
@ -150,9 +144,6 @@ export class ActivityPanel extends Component {
|
||||||
const showDisplayOptions =
|
const showDisplayOptions =
|
||||||
! isEmbedded && this.isHomescreen() && ! isPerformingSetupTask;
|
! isEmbedded && this.isHomescreen() && ! isPerformingSetupTask;
|
||||||
|
|
||||||
const showReviews =
|
|
||||||
( taskListComplete || taskListHidden ) && ! isPerformingSetupTask;
|
|
||||||
|
|
||||||
const showStoreSetup =
|
const showStoreSetup =
|
||||||
! taskListComplete && ! taskListHidden && ! isPerformingSetupTask;
|
! taskListComplete && ! taskListHidden && ! isPerformingSetupTask;
|
||||||
|
|
||||||
|
@ -173,20 +164,6 @@ export class ActivityPanel extends Component {
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const reviews =
|
|
||||||
showReviews && reviewsEnabled === 'yes'
|
|
||||||
? {
|
|
||||||
name: 'reviews',
|
|
||||||
title: __( 'Reviews', 'woocommerce-admin' ),
|
|
||||||
icon: (
|
|
||||||
<i className="material-icons-outlined">
|
|
||||||
star_border
|
|
||||||
</i>
|
|
||||||
),
|
|
||||||
unread: hasUnapprovedReviews,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const help = showHelp
|
const help = showHelp
|
||||||
? {
|
? {
|
||||||
name: 'help',
|
name: 'help',
|
||||||
|
@ -201,24 +178,16 @@ export class ActivityPanel extends Component {
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return [ inbox, reviews, setup, displayOptions, help ].filter(
|
return [ inbox, setup, displayOptions, help ].filter( Boolean );
|
||||||
Boolean
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPanelContent( tab ) {
|
getPanelContent( tab ) {
|
||||||
const { query, hasUnapprovedReviews } = this.props;
|
const { query } = this.props;
|
||||||
const { task } = query;
|
const { task } = query;
|
||||||
|
|
||||||
switch ( tab ) {
|
switch ( tab ) {
|
||||||
case 'inbox':
|
case 'inbox':
|
||||||
return <InboxPanel />;
|
return <InboxPanel />;
|
||||||
case 'reviews':
|
|
||||||
return (
|
|
||||||
<ReviewsPanel
|
|
||||||
hasUnapprovedReviews={ hasUnapprovedReviews }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'help':
|
case 'help':
|
||||||
return <HelpPanel taskName={ task } />;
|
return <HelpPanel taskName={ task } />;
|
||||||
default:
|
default:
|
||||||
|
@ -378,7 +347,6 @@ ActivityPanel.defaultProps = {
|
||||||
export default compose(
|
export default compose(
|
||||||
withSelect( ( select ) => {
|
withSelect( ( select ) => {
|
||||||
const hasUnreadNotes = getUnreadNotes( select );
|
const hasUnreadNotes = getUnreadNotes( select );
|
||||||
const hasUnapprovedReviews = getUnapprovedReviews( select );
|
|
||||||
const { getOption, isResolving } = select( OPTIONS_STORE_NAME );
|
const { getOption, isResolving } = select( OPTIONS_STORE_NAME );
|
||||||
|
|
||||||
const taskListComplete =
|
const taskListComplete =
|
||||||
|
@ -391,7 +359,6 @@ export default compose(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasUnreadNotes,
|
hasUnreadNotes,
|
||||||
hasUnapprovedReviews,
|
|
||||||
requestingTaskListOptions,
|
requestingTaskListOptions,
|
||||||
taskListComplete,
|
taskListComplete,
|
||||||
taskListHidden,
|
taskListHidden,
|
||||||
|
|
|
@ -1,370 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { __, sprintf } from '@wordpress/i18n';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import { Component, Fragment } from '@wordpress/element';
|
|
||||||
import { withSelect } from '@wordpress/data';
|
|
||||||
import { Button } from '@wordpress/components';
|
|
||||||
import CheckmarkIcon from 'gridicons/dist/checkmark';
|
|
||||||
import TimeIcon from 'gridicons/dist/time';
|
|
||||||
import interpolateComponents from 'interpolate-components';
|
|
||||||
import { get, isNull } from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import {
|
|
||||||
EmptyContent,
|
|
||||||
Gravatar,
|
|
||||||
Link,
|
|
||||||
ProductImage,
|
|
||||||
ReviewRating,
|
|
||||||
Section,
|
|
||||||
} from '@woocommerce/components';
|
|
||||||
import { getAdminLink } from '@woocommerce/wc-admin-settings';
|
|
||||||
import { REVIEWS_STORE_NAME, QUERY_DEFAULTS } from '@woocommerce/data';
|
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import { ActivityCard, ActivityCardPlaceholder } from '../activity-card';
|
|
||||||
import ActivityHeader from '../activity-header';
|
|
||||||
import sanitizeHTML from '../../../lib/sanitize-html';
|
|
||||||
|
|
||||||
class ReviewsPanel extends Component {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.mountTime = new Date().getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
recordReviewEvent( eventName ) {
|
|
||||||
recordEvent( `activity_panel_reviews_${ eventName }`, {} );
|
|
||||||
}
|
|
||||||
|
|
||||||
renderReview( review, props ) {
|
|
||||||
const { lastRead } = props;
|
|
||||||
const product =
|
|
||||||
( review &&
|
|
||||||
review._embedded &&
|
|
||||||
review._embedded.up &&
|
|
||||||
review._embedded.up[ 0 ] ) ||
|
|
||||||
null;
|
|
||||||
|
|
||||||
if ( isNull( product ) ) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = interpolateComponents( {
|
|
||||||
mixedString: sprintf(
|
|
||||||
__(
|
|
||||||
'{{productLink}}%s{{/productLink}} reviewed by {{authorLink}}%s{{/authorLink}}',
|
|
||||||
'woocommerce-admin'
|
|
||||||
),
|
|
||||||
product.name,
|
|
||||||
review.reviewer
|
|
||||||
),
|
|
||||||
components: {
|
|
||||||
productLink: (
|
|
||||||
<Link
|
|
||||||
href={ product.permalink }
|
|
||||||
onClick={ () => this.recordReviewEvent( 'product' ) }
|
|
||||||
type="external"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
authorLink: (
|
|
||||||
<Link
|
|
||||||
href={ 'mailto:' + review.reviewer_email }
|
|
||||||
onClick={ () => this.recordReviewEvent( 'customer' ) }
|
|
||||||
type="external"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
} );
|
|
||||||
|
|
||||||
const subtitle = (
|
|
||||||
<Fragment>
|
|
||||||
<ReviewRating review={ review } />
|
|
||||||
{ review.verified && (
|
|
||||||
<span className="woocommerce-review-activity-card__verified">
|
|
||||||
<CheckmarkIcon size={ 18 } />
|
|
||||||
{ __( 'Verified customer', 'woocommerce-admin' ) }
|
|
||||||
</span>
|
|
||||||
) }
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
|
|
||||||
const productImage =
|
|
||||||
get( product, [ 'images', 0 ] ) || get( product, [ 'image' ] );
|
|
||||||
const productImageClasses = classnames(
|
|
||||||
'woocommerce-review-activity-card__image-overlay__product',
|
|
||||||
{
|
|
||||||
'is-placeholder': ! productImage || ! productImage.src,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const icon = (
|
|
||||||
<div className="woocommerce-review-activity-card__image-overlay">
|
|
||||||
<Gravatar user={ review.reviewer_email } size={ 24 } />
|
|
||||||
<div className={ productImageClasses }>
|
|
||||||
<ProductImage product={ product } />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const manageReviewEvent = {
|
|
||||||
date: review.date_created_gmt,
|
|
||||||
status: review.status,
|
|
||||||
};
|
|
||||||
|
|
||||||
const cardActions = (
|
|
||||||
<Button
|
|
||||||
isSecondary
|
|
||||||
onClick={ () =>
|
|
||||||
recordEvent( 'review_manage_click', manageReviewEvent )
|
|
||||||
}
|
|
||||||
href={ getAdminLink(
|
|
||||||
'comment.php?action=editcomment&c=' + review.id
|
|
||||||
) }
|
|
||||||
>
|
|
||||||
{ __( 'Manage', 'woocommerce-admin' ) }
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ActivityCard
|
|
||||||
className="woocommerce-review-activity-card"
|
|
||||||
key={ review.id }
|
|
||||||
title={ title }
|
|
||||||
subtitle={ subtitle }
|
|
||||||
date={ review.date_created_gmt }
|
|
||||||
icon={ icon }
|
|
||||||
actions={ cardActions }
|
|
||||||
unread={
|
|
||||||
review.status === 'hold' ||
|
|
||||||
! lastRead ||
|
|
||||||
! review.date_created_gmt ||
|
|
||||||
new Date( review.date_created_gmt + 'Z' ).getTime() >
|
|
||||||
lastRead
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
dangerouslySetInnerHTML={ sanitizeHTML( review.review ) }
|
|
||||||
/>
|
|
||||||
</ActivityCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderEmptyMessage() {
|
|
||||||
const { lastApprovedReviewTime } = this.props;
|
|
||||||
|
|
||||||
const title = __(
|
|
||||||
'You have no reviews to moderate',
|
|
||||||
'woocommerce-admin'
|
|
||||||
);
|
|
||||||
let buttonUrl = '';
|
|
||||||
let buttonTarget = '';
|
|
||||||
let buttonText = '';
|
|
||||||
let content = '';
|
|
||||||
let eventName = 'learn_more';
|
|
||||||
|
|
||||||
if ( lastApprovedReviewTime ) {
|
|
||||||
const now = new Date();
|
|
||||||
const DAY = 24 * 60 * 60 * 1000;
|
|
||||||
if ( ( now.getTime() - lastApprovedReviewTime ) / DAY > 30 ) {
|
|
||||||
buttonUrl =
|
|
||||||
'https://woocommerce.com/posts/reviews-woocommerce-best-practices/';
|
|
||||||
buttonTarget = '_blank';
|
|
||||||
buttonText = __( 'Learn more', 'woocommerce-admin' );
|
|
||||||
content = (
|
|
||||||
<Fragment>
|
|
||||||
<p>
|
|
||||||
{ __(
|
|
||||||
"We noticed that it's been a while since your products had any reviews.",
|
|
||||||
'woocommerce-admin'
|
|
||||||
) }
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{ __(
|
|
||||||
'Take some time to learn about best practices for collecting and using your reviews.',
|
|
||||||
'woocommerce-admin'
|
|
||||||
) }
|
|
||||||
</p>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
buttonUrl = getAdminLink(
|
|
||||||
'edit-comments.php?comment_type=review'
|
|
||||||
);
|
|
||||||
buttonText = __( 'View all Reviews', 'woocommerce-admin' );
|
|
||||||
content = (
|
|
||||||
<p>
|
|
||||||
{ __(
|
|
||||||
/* eslint-disable max-len */
|
|
||||||
"Awesome, you've moderated all of your product reviews. How about responding to some of those negative reviews?",
|
|
||||||
'woocommerce-admin'
|
|
||||||
/* eslint-enable */
|
|
||||||
) }
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
eventName = 'view_reviews';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
buttonUrl =
|
|
||||||
'https://woocommerce.com/posts/reviews-woocommerce-best-practices/';
|
|
||||||
buttonTarget = '_blank';
|
|
||||||
buttonText = __( 'Learn more', 'woocommerce-admin' );
|
|
||||||
content = (
|
|
||||||
<Fragment>
|
|
||||||
<p>
|
|
||||||
{ __(
|
|
||||||
"Your customers haven't started reviewing your products.",
|
|
||||||
'woocommerce-admin'
|
|
||||||
) }
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{ __(
|
|
||||||
'Take some time to learn about best practices for collecting and using your reviews.',
|
|
||||||
'woocommerce-admin'
|
|
||||||
) }
|
|
||||||
</p>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ActivityCard
|
|
||||||
className="woocommerce-empty-activity-card"
|
|
||||||
title={ title }
|
|
||||||
icon={ <TimeIcon size={ 48 } /> }
|
|
||||||
actions={
|
|
||||||
<Button
|
|
||||||
href={ buttonUrl }
|
|
||||||
target={ buttonTarget }
|
|
||||||
isSecondary
|
|
||||||
onClick={ () => this.recordReviewEvent( eventName ) }
|
|
||||||
>
|
|
||||||
{ buttonText }
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{ content }
|
|
||||||
</ActivityCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { isError, isRequesting, reviews } = this.props;
|
|
||||||
|
|
||||||
if ( isError ) {
|
|
||||||
const title = __(
|
|
||||||
'There was an error getting your reviews. Please try again.',
|
|
||||||
'woocommerce-admin'
|
|
||||||
);
|
|
||||||
const actionLabel = __( 'Reload', 'woocommerce-admin' );
|
|
||||||
const actionCallback = () => {
|
|
||||||
window.location.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<EmptyContent
|
|
||||||
title={ title }
|
|
||||||
actionLabel={ actionLabel }
|
|
||||||
actionURL={ null }
|
|
||||||
actionCallback={ actionCallback }
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const title =
|
|
||||||
isRequesting || reviews.length
|
|
||||||
? __( 'Reviews', 'woocommerce-admin' )
|
|
||||||
: __( 'No reviews to moderate', 'woocommerce-admin' );
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<ActivityHeader title={ title } />
|
|
||||||
<Section>
|
|
||||||
{ isRequesting ? (
|
|
||||||
<ActivityCardPlaceholder
|
|
||||||
className="woocommerce-review-activity-card"
|
|
||||||
hasAction
|
|
||||||
hasDate
|
|
||||||
lines={ 2 }
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Fragment>
|
|
||||||
{ reviews.length
|
|
||||||
? reviews.map( ( review ) =>
|
|
||||||
this.renderReview( review, this.props )
|
|
||||||
)
|
|
||||||
: this.renderEmptyMessage() }
|
|
||||||
</Fragment>
|
|
||||||
) }
|
|
||||||
</Section>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ReviewsPanel.propTypes = {
|
|
||||||
reviews: PropTypes.array.isRequired,
|
|
||||||
isError: PropTypes.bool,
|
|
||||||
isRequesting: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
ReviewsPanel.defaultProps = {
|
|
||||||
reviews: [],
|
|
||||||
isError: false,
|
|
||||||
isRequesting: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withSelect( ( select, props ) => {
|
|
||||||
const { hasUnapprovedReviews } = props;
|
|
||||||
const { getReviews, getReviewsError, isResolving } = select(
|
|
||||||
REVIEWS_STORE_NAME
|
|
||||||
);
|
|
||||||
let reviews = [];
|
|
||||||
let isError = false;
|
|
||||||
let isRequesting = false;
|
|
||||||
let lastApprovedReviewTime = null;
|
|
||||||
if ( hasUnapprovedReviews ) {
|
|
||||||
const reviewsQuery = {
|
|
||||||
page: 1,
|
|
||||||
per_page: QUERY_DEFAULTS.pageSize,
|
|
||||||
status: 'hold',
|
|
||||||
_embed: 1,
|
|
||||||
};
|
|
||||||
reviews = getReviews( reviewsQuery );
|
|
||||||
isError = Boolean( getReviewsError( reviewsQuery ) );
|
|
||||||
isRequesting = isResolving( 'getReviews', [ reviewsQuery ] );
|
|
||||||
} else {
|
|
||||||
const approvedReviewsQuery = {
|
|
||||||
page: 1,
|
|
||||||
per_page: 1,
|
|
||||||
status: 'approved',
|
|
||||||
_embed: 1,
|
|
||||||
};
|
|
||||||
const approvedReviews = getReviews( approvedReviewsQuery );
|
|
||||||
if ( approvedReviews.length ) {
|
|
||||||
const lastApprovedReview = approvedReviews[ 0 ];
|
|
||||||
if ( lastApprovedReview.date_created_gmt ) {
|
|
||||||
const creationDate = new Date(
|
|
||||||
lastApprovedReview.date_created_gmt
|
|
||||||
);
|
|
||||||
lastApprovedReviewTime = creationDate.getTime();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isError = Boolean( getReviewsError( approvedReviewsQuery ) );
|
|
||||||
isRequesting = isResolving( 'getReviews', [ approvedReviewsQuery ] );
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
reviews,
|
|
||||||
isError,
|
|
||||||
isRequesting,
|
|
||||||
lastApprovedReviewTime,
|
|
||||||
};
|
|
||||||
} )( ReviewsPanel );
|
|
|
@ -3,7 +3,6 @@
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
NOTES_STORE_NAME,
|
NOTES_STORE_NAME,
|
||||||
REVIEWS_STORE_NAME,
|
|
||||||
USER_STORE_NAME,
|
USER_STORE_NAME,
|
||||||
QUERY_DEFAULTS,
|
QUERY_DEFAULTS,
|
||||||
} from '@woocommerce/data';
|
} from '@woocommerce/data';
|
||||||
|
@ -57,40 +56,6 @@ export function getUnreadNotes( select ) {
|
||||||
return unreadNotesCount > 0;
|
return unreadNotesCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUnapprovedReviews( select ) {
|
|
||||||
const { getReviewsTotalCount, getReviewsError, isResolving } = select(
|
|
||||||
REVIEWS_STORE_NAME
|
|
||||||
);
|
|
||||||
const reviewsEnabled = getSetting( 'reviewsEnabled' );
|
|
||||||
if ( reviewsEnabled === 'yes' ) {
|
|
||||||
const actionableReviewsQuery = {
|
|
||||||
page: 1,
|
|
||||||
// @todo we are not using this review, so when the endpoint supports it,
|
|
||||||
// it could be replaced with `per_page: 0`
|
|
||||||
per_page: 1,
|
|
||||||
status: 'hold',
|
|
||||||
};
|
|
||||||
const totalActionableReviews = getReviewsTotalCount(
|
|
||||||
actionableReviewsQuery
|
|
||||||
);
|
|
||||||
|
|
||||||
const isActionableReviewsError = Boolean(
|
|
||||||
getReviewsError( actionableReviewsQuery )
|
|
||||||
);
|
|
||||||
|
|
||||||
const isActionableReviewsRequesting = isResolving(
|
|
||||||
'getReviewsTotalCount',
|
|
||||||
[ actionableReviewsQuery ]
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( ! isActionableReviewsError && ! isActionableReviewsRequesting ) {
|
|
||||||
return totalActionableReviews > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLowStockCount() {
|
export function getLowStockCount() {
|
||||||
return getSetting( 'lowStockCount', 0 );
|
return getSetting( 'lowStockCount', 0 );
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,14 +16,17 @@ import {
|
||||||
getUnreadOrders,
|
getUnreadOrders,
|
||||||
} from './orders/utils';
|
} from './orders/utils';
|
||||||
import { getAllPanels } from './panels';
|
import { getAllPanels } from './panels';
|
||||||
|
import { getUnapprovedReviews } from './reviews/utils';
|
||||||
|
|
||||||
export const ActivityPanel = () => {
|
export const ActivityPanel = () => {
|
||||||
const panelsData = useSelect( ( select ) => {
|
const panelsData = useSelect( ( select ) => {
|
||||||
const totalOrderCount = getSetting( 'orderCount', 0 );
|
const totalOrderCount = getSetting( 'orderCount', 0 );
|
||||||
const orderStatuses = getOrderStatuses( select );
|
const orderStatuses = getOrderStatuses( select );
|
||||||
|
const reviewsEnabled = getSetting( 'reviewsEnabled', 'no' );
|
||||||
const countUnreadOrders = getUnreadOrders( select, orderStatuses );
|
const countUnreadOrders = getUnreadOrders( select, orderStatuses );
|
||||||
const manageStock = getSetting( 'manageStock', 'no' );
|
const manageStock = getSetting( 'manageStock', 'no' );
|
||||||
const countLowStockProducts = getLowStockCount( select );
|
const countLowStockProducts = getLowStockCount( select );
|
||||||
|
const countUnapprovedReviews = getUnapprovedReviews( select );
|
||||||
|
|
||||||
return {
|
return {
|
||||||
countLowStockProducts,
|
countLowStockProducts,
|
||||||
|
@ -31,6 +34,8 @@ export const ActivityPanel = () => {
|
||||||
manageStock,
|
manageStock,
|
||||||
orderStatuses,
|
orderStatuses,
|
||||||
totalOrderCount,
|
totalOrderCount,
|
||||||
|
reviewsEnabled,
|
||||||
|
countUnapprovedReviews,
|
||||||
};
|
};
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
@ -55,6 +60,7 @@ export const ActivityPanel = () => {
|
||||||
count={ count }
|
count={ count }
|
||||||
initialOpen={ initialOpen }
|
initialOpen={ initialOpen }
|
||||||
title={ title }
|
title={ title }
|
||||||
|
collapsible={ count !== 0 }
|
||||||
>
|
>
|
||||||
{ panel }
|
{ panel }
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { __ } from '@wordpress/i18n';
|
||||||
*/
|
*/
|
||||||
import OrdersPanel from './orders';
|
import OrdersPanel from './orders';
|
||||||
import StockPanel from './stock';
|
import StockPanel from './stock';
|
||||||
|
import ReviewsPanel from './reviews';
|
||||||
|
|
||||||
export function getAllPanels( {
|
export function getAllPanels( {
|
||||||
countLowStockProducts,
|
countLowStockProducts,
|
||||||
|
@ -15,6 +16,8 @@ export function getAllPanels( {
|
||||||
manageStock,
|
manageStock,
|
||||||
orderStatuses,
|
orderStatuses,
|
||||||
totalOrderCount,
|
totalOrderCount,
|
||||||
|
reviewsEnabled,
|
||||||
|
countUnapprovedReviews,
|
||||||
} ) {
|
} ) {
|
||||||
return [
|
return [
|
||||||
totalOrderCount > 0 && {
|
totalOrderCount > 0 && {
|
||||||
|
@ -40,6 +43,18 @@ export function getAllPanels( {
|
||||||
),
|
),
|
||||||
title: __( 'Stock', 'woocommerce-admin' ),
|
title: __( 'Stock', 'woocommerce-admin' ),
|
||||||
},
|
},
|
||||||
|
reviewsEnabled === 'yes' && {
|
||||||
|
className: 'woocommerce-homescreen-card',
|
||||||
|
id: 'reviews-panel',
|
||||||
|
count: countUnapprovedReviews,
|
||||||
|
initialOpen: false,
|
||||||
|
panel: (
|
||||||
|
<ReviewsPanel
|
||||||
|
hasUnapprovedReviews={ countUnapprovedReviews > 0 }
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
title: __( 'Reviews', 'woocommerce-admin' ),
|
||||||
|
},
|
||||||
// Add another panel row here
|
// Add another panel row here
|
||||||
].filter( Boolean );
|
].filter( Boolean );
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,421 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { Component, Fragment } from '@wordpress/element';
|
||||||
|
import { Button } from '@wordpress/components';
|
||||||
|
import { compose } from '@wordpress/compose';
|
||||||
|
import { withSelect, withDispatch } from '@wordpress/data';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import CheckmarkIcon from 'gridicons/dist/checkmark';
|
||||||
|
import StarIcon from 'gridicons/dist/star';
|
||||||
|
import StarOutlineIcon from 'gridicons/dist/star-outline';
|
||||||
|
import interpolateComponents from 'interpolate-components';
|
||||||
|
import {
|
||||||
|
EmptyContent,
|
||||||
|
Link,
|
||||||
|
ReviewRating,
|
||||||
|
ProductImage,
|
||||||
|
Section,
|
||||||
|
} from '@woocommerce/components';
|
||||||
|
import { getAdminLink } from '@woocommerce/wc-admin-settings';
|
||||||
|
import { get, isNull } from 'lodash';
|
||||||
|
import { REVIEWS_STORE_NAME } from '@woocommerce/data';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './style.scss';
|
||||||
|
import {
|
||||||
|
ActivityCard,
|
||||||
|
ActivityCardPlaceholder,
|
||||||
|
} from '../../../header/activity-panel/activity-card';
|
||||||
|
import { CurrencyContext } from '../../../lib/currency-context';
|
||||||
|
import sanitizeHTML from '../../../lib/sanitize-html';
|
||||||
|
import { REVIEW_PAGE_LIMIT, unapprovedReviewsQuery } from './utils';
|
||||||
|
|
||||||
|
const reviewsQuery = {
|
||||||
|
page: 1,
|
||||||
|
per_page: REVIEW_PAGE_LIMIT,
|
||||||
|
status: 'hold',
|
||||||
|
_embed: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
class ReviewsPanel extends Component {
|
||||||
|
recordReviewEvent( eventName, eventData ) {
|
||||||
|
recordEvent( `reviews_${ eventName }`, eventData || {} );
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteReview( reviewId ) {
|
||||||
|
const {
|
||||||
|
deleteReview,
|
||||||
|
createNotice,
|
||||||
|
updateReview,
|
||||||
|
clearReviewsCache,
|
||||||
|
} = this.props;
|
||||||
|
if ( reviewId ) {
|
||||||
|
deleteReview( reviewId )
|
||||||
|
.then( () => {
|
||||||
|
clearReviewsCache();
|
||||||
|
createNotice(
|
||||||
|
'success',
|
||||||
|
__(
|
||||||
|
'Review successfully deleted.',
|
||||||
|
'woocommerce-admin'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __( 'Undo', 'woocommerce-admin' ),
|
||||||
|
onClick: () => {
|
||||||
|
updateReview(
|
||||||
|
reviewId,
|
||||||
|
{
|
||||||
|
status: 'untrash',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_embed: 1,
|
||||||
|
}
|
||||||
|
).then( () => clearReviewsCache() );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} )
|
||||||
|
.catch( () => {
|
||||||
|
createNotice(
|
||||||
|
'error',
|
||||||
|
__(
|
||||||
|
'Review could not be deleted.',
|
||||||
|
'woocommerce-admin'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateReviewStatus( reviewId, newStatus, oldStatus ) {
|
||||||
|
const { createNotice, updateReview, clearReviewsCache } = this.props;
|
||||||
|
if ( reviewId ) {
|
||||||
|
updateReview( reviewId, { status: newStatus } )
|
||||||
|
.then( () => {
|
||||||
|
clearReviewsCache();
|
||||||
|
createNotice(
|
||||||
|
'success',
|
||||||
|
__(
|
||||||
|
'Review successfully updated.',
|
||||||
|
'woocommerce-admin'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __( 'Undo', 'woocommerce-admin' ),
|
||||||
|
onClick: () => {
|
||||||
|
updateReview(
|
||||||
|
reviewId,
|
||||||
|
{
|
||||||
|
status: oldStatus,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_embed: 1,
|
||||||
|
}
|
||||||
|
).then( () => clearReviewsCache() );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} )
|
||||||
|
.catch( () => {
|
||||||
|
createNotice(
|
||||||
|
'error',
|
||||||
|
__(
|
||||||
|
'Review could not be updated.',
|
||||||
|
'woocommerce-admin'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderReview( review ) {
|
||||||
|
const product =
|
||||||
|
( review &&
|
||||||
|
review._embedded &&
|
||||||
|
review._embedded.up &&
|
||||||
|
review._embedded.up[ 0 ] ) ||
|
||||||
|
null;
|
||||||
|
|
||||||
|
if ( review.isUpdating || this.props.isRequesting ) {
|
||||||
|
return (
|
||||||
|
<ActivityCardPlaceholder
|
||||||
|
key={ review.id }
|
||||||
|
className="woocommerce-review-activity-card"
|
||||||
|
hasAction
|
||||||
|
hasDate
|
||||||
|
lines={ 1 }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ( isNull( product ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = interpolateComponents( {
|
||||||
|
mixedString: sprintf(
|
||||||
|
__(
|
||||||
|
'{{authorLink}}%s{{/authorLink}} reviewed {{productLink}}%s{{/productLink}}',
|
||||||
|
'woocommerce-admin'
|
||||||
|
),
|
||||||
|
review.reviewer,
|
||||||
|
product.name
|
||||||
|
),
|
||||||
|
components: {
|
||||||
|
productLink: (
|
||||||
|
<Link
|
||||||
|
href={ product.permalink }
|
||||||
|
onClick={ () => this.recordReviewEvent( 'product' ) }
|
||||||
|
type="external"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
authorLink: (
|
||||||
|
<Link
|
||||||
|
href={ getAdminLink(
|
||||||
|
'admin.php?page=wc-admin&path=%2Fcustomers&search=' +
|
||||||
|
review.reviewer
|
||||||
|
) }
|
||||||
|
onClick={ () => this.recordReviewEvent( 'customer' ) }
|
||||||
|
type="external"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
const subtitle = (
|
||||||
|
<Fragment>
|
||||||
|
<ReviewRating
|
||||||
|
review={ review }
|
||||||
|
icon={ StarOutlineIcon }
|
||||||
|
outlineIcon={ StarIcon }
|
||||||
|
size={ 13 }
|
||||||
|
/>
|
||||||
|
{ review.verified && (
|
||||||
|
<span className="woocommerce-review-activity-card__verified">
|
||||||
|
<CheckmarkIcon size={ 18 } />
|
||||||
|
{ __( 'Verified customer', 'woocommerce-admin' ) }
|
||||||
|
</span>
|
||||||
|
) }
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
const productImage =
|
||||||
|
get( product, [ 'images', 0 ] ) || get( product, [ 'image' ] );
|
||||||
|
const productImageClasses = classnames(
|
||||||
|
'woocommerce-review-activity-card__image-overlay__product',
|
||||||
|
{
|
||||||
|
'is-placeholder': ! productImage || ! productImage.src,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const icon = (
|
||||||
|
<div className="woocommerce-review-activity-card__image-overlay">
|
||||||
|
<div className={ productImageClasses }>
|
||||||
|
<ProductImage
|
||||||
|
product={ product }
|
||||||
|
width={ 33 }
|
||||||
|
height={ 33 }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const manageReviewEvent = {
|
||||||
|
date: review.date_created_gmt,
|
||||||
|
status: review.status,
|
||||||
|
};
|
||||||
|
|
||||||
|
const cardActions = [
|
||||||
|
<Button
|
||||||
|
key="approve-action"
|
||||||
|
isSecondary
|
||||||
|
onClick={ () => {
|
||||||
|
this.recordReviewEvent( 'approve', manageReviewEvent );
|
||||||
|
this.updateReviewStatus(
|
||||||
|
review.id,
|
||||||
|
'approved',
|
||||||
|
review.status
|
||||||
|
);
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
{ __( 'Approve', 'woocommerce-admin' ) }
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="spam-action"
|
||||||
|
isTertiary
|
||||||
|
onClick={ () => {
|
||||||
|
this.recordReviewEvent( 'mark_as_spam', manageReviewEvent );
|
||||||
|
this.updateReviewStatus( review.id, 'spam', review.status );
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
{ __( 'Mark as spam', 'woocommerce-admin' ) }
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="delete-action"
|
||||||
|
isDestructive
|
||||||
|
isTertiary
|
||||||
|
onClick={ () => {
|
||||||
|
this.recordReviewEvent( 'delete', manageReviewEvent );
|
||||||
|
this.deleteReview( review.id );
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
{ __( 'Delete', 'woocommerce-admin' ) }
|
||||||
|
</Button>,
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActivityCard
|
||||||
|
className="woocommerce-review-activity-card"
|
||||||
|
key={ review.id }
|
||||||
|
title={ title }
|
||||||
|
subtitle={ subtitle }
|
||||||
|
date={ review.date_created_gmt }
|
||||||
|
icon={ icon }
|
||||||
|
actions={ cardActions }
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
dangerouslySetInnerHTML={ sanitizeHTML( review.review ) }
|
||||||
|
/>
|
||||||
|
</ActivityCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderReviews( reviews ) {
|
||||||
|
if ( reviews.length === 0 ) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ reviews.map( ( review ) =>
|
||||||
|
this.renderReview( review, this.props )
|
||||||
|
) }
|
||||||
|
<Link
|
||||||
|
href={ getAdminLink(
|
||||||
|
'edit-comments.php?comment_type=review'
|
||||||
|
) }
|
||||||
|
onClick={ () => this.recordReviewEvent( 'reviews_manage' ) }
|
||||||
|
className="woocommerce-layout__activity-panel-outbound-link woocommerce-layout__activity-panel-empty"
|
||||||
|
type="wp-admin"
|
||||||
|
>
|
||||||
|
{ __( 'Manage all reviews', 'woocommerce-admin' ) }
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isRequesting, isError, reviews } = this.props;
|
||||||
|
|
||||||
|
if ( isError ) {
|
||||||
|
const title = __(
|
||||||
|
'There was an error getting your reviews. Please try again.',
|
||||||
|
'woocommerce-admin'
|
||||||
|
);
|
||||||
|
const actionLabel = __( 'Reload', 'woocommerce-admin' );
|
||||||
|
const actionCallback = () => {
|
||||||
|
// @todo Add tracking for how often an error is displayed, and the reload action is clicked.
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<EmptyContent
|
||||||
|
title={ title }
|
||||||
|
actionLabel={ actionLabel }
|
||||||
|
actionURL={ null }
|
||||||
|
actionCallback={ actionCallback }
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Section>
|
||||||
|
{ isRequesting && ! reviews.length ? (
|
||||||
|
<ActivityCardPlaceholder
|
||||||
|
className="woocommerce-review-activity-card"
|
||||||
|
hasAction
|
||||||
|
hasDate
|
||||||
|
lines={ 1 }
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>{ this.renderReviews( reviews, this.props ) }</>
|
||||||
|
) }
|
||||||
|
</Section>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReviewsPanel.propTypes = {
|
||||||
|
reviews: PropTypes.array.isRequired,
|
||||||
|
isError: PropTypes.bool,
|
||||||
|
isRequesting: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
ReviewsPanel.defaultProps = {
|
||||||
|
reviews: [],
|
||||||
|
isError: false,
|
||||||
|
isRequesting: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
ReviewsPanel.contextType = CurrencyContext;
|
||||||
|
|
||||||
|
export { ReviewsPanel };
|
||||||
|
|
||||||
|
export default compose( [
|
||||||
|
withSelect( ( select, props ) => {
|
||||||
|
const { hasUnapprovedReviews } = props;
|
||||||
|
const { getReviews, getReviewsError, isResolving } = select(
|
||||||
|
REVIEWS_STORE_NAME
|
||||||
|
);
|
||||||
|
let reviews = [];
|
||||||
|
let isError = false;
|
||||||
|
let isRequesting = false;
|
||||||
|
if ( hasUnapprovedReviews ) {
|
||||||
|
reviews = getReviews( reviewsQuery );
|
||||||
|
isError = Boolean( getReviewsError( reviewsQuery ) );
|
||||||
|
isRequesting = isResolving( 'getReviews', [ reviewsQuery ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
reviews,
|
||||||
|
isError,
|
||||||
|
isRequesting,
|
||||||
|
};
|
||||||
|
} ),
|
||||||
|
withDispatch( ( dispatch, props ) => {
|
||||||
|
const { deleteReview, updateReview, invalidateResolution } = dispatch(
|
||||||
|
REVIEWS_STORE_NAME
|
||||||
|
);
|
||||||
|
const { createNotice } = dispatch( 'core/notices' );
|
||||||
|
|
||||||
|
const clearReviewsCache = () => {
|
||||||
|
invalidateResolution( 'getReviews', [ reviewsQuery ] );
|
||||||
|
if ( props.reviews && props.reviews.length < 2 ) {
|
||||||
|
invalidateResolution( 'getReviewsTotalCount', [
|
||||||
|
unapprovedReviewsQuery,
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleteReview,
|
||||||
|
createNotice,
|
||||||
|
updateReview,
|
||||||
|
clearReviewsCache,
|
||||||
|
};
|
||||||
|
} ),
|
||||||
|
] )( ReviewsPanel );
|
|
@ -0,0 +1,45 @@
|
||||||
|
.woocommerce-review-activity-card {
|
||||||
|
.woocommerce-rating {
|
||||||
|
margin-top: $gap-smallest;
|
||||||
|
.gridicon {
|
||||||
|
fill: $notice-yellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-activity-card__body > span > p {
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-review-activity-card__verified {
|
||||||
|
margin-left: $gap-small;
|
||||||
|
display: inline-flex;
|
||||||
|
position: relative;
|
||||||
|
top: $gap-smallest;
|
||||||
|
color: $valid-green;
|
||||||
|
@include font-size( 12 );
|
||||||
|
|
||||||
|
.gridicon {
|
||||||
|
margin-right: $gap-smallest;
|
||||||
|
fill: $valid-green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include breakpoint( '<782px' ) {
|
||||||
|
.woocommerce-review-activity-card__image-overlay {
|
||||||
|
margin-top: $gap-smallest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.woocommerce-activity-card {
|
||||||
|
padding: $gap $gap-large;
|
||||||
|
}
|
||||||
|
.woocommerce-activity-card__header .woocommerce-activity-card__title {
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { ReviewsPanel } from '../';
|
||||||
|
|
||||||
|
const REVIEW = {
|
||||||
|
id: 10,
|
||||||
|
date_created: '2020-11-20T18:24:41',
|
||||||
|
date_created_gmt: '2020-11-20T18:24:41',
|
||||||
|
product_id: 45,
|
||||||
|
status: 'hold',
|
||||||
|
reviewer: 'Reviewer',
|
||||||
|
reviewer_email: 'test@test.ca',
|
||||||
|
review: '<p>It is an average hat</p>\n',
|
||||||
|
rating: 3,
|
||||||
|
verified: false,
|
||||||
|
_embedded: {
|
||||||
|
up: [
|
||||||
|
{
|
||||||
|
id: 45,
|
||||||
|
name: 'Cap',
|
||||||
|
slug: 'cap',
|
||||||
|
permalink: 'https://one.wordpress.test/product/cap/',
|
||||||
|
description:
|
||||||
|
'Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.',
|
||||||
|
short_description: 'This is a simple product.',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
id: 74,
|
||||||
|
date_created: '2020-11-20T17:28:47',
|
||||||
|
date_created_gmt: '2020-11-20T17:28:47',
|
||||||
|
date_modified: '2020-11-20T17:28:47',
|
||||||
|
date_modified_gmt: '2020-11-20T17:28:47',
|
||||||
|
src:
|
||||||
|
'https://one.wordpress.test/wp-content/uploads/2020/11/cap-2-1.jpg',
|
||||||
|
name: 'cap-2-1.jpg',
|
||||||
|
alt: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock( '@woocommerce/components', () => ( {
|
||||||
|
...require.requireActual( '@woocommerce/components' ),
|
||||||
|
Link: ( { children } ) => {
|
||||||
|
return <>{ children }</>;
|
||||||
|
},
|
||||||
|
} ) );
|
||||||
|
|
||||||
|
describe( 'ReviewsPanel', () => {
|
||||||
|
it( 'should render an empty review card', () => {
|
||||||
|
render(
|
||||||
|
<ReviewsPanel
|
||||||
|
hasUnapprovedReviews={ false }
|
||||||
|
isError={ false }
|
||||||
|
isRequesting={ false }
|
||||||
|
reviews={ [] }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect( screen.queryByRole( 'section' ) ).toBeNull();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should render a review card with title <name> reviewed <product name>', () => {
|
||||||
|
render(
|
||||||
|
<ReviewsPanel
|
||||||
|
hasUnapprovedReviews={ true }
|
||||||
|
isError={ false }
|
||||||
|
isRequesting={ false }
|
||||||
|
reviews={ [ REVIEW ] }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect( screen.getByText( 'Reviewer reviewed Cap' ) ).not.toBeNull();
|
||||||
|
} );
|
||||||
|
|
||||||
|
describe( 'review actions', () => {
|
||||||
|
it( 'should render a review card with approve, mark as spam, and delete buttons', () => {
|
||||||
|
render(
|
||||||
|
<ReviewsPanel
|
||||||
|
hasUnapprovedReviews={ true }
|
||||||
|
isError={ false }
|
||||||
|
isRequesting={ false }
|
||||||
|
reviews={ [ REVIEW ] }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect( screen.queryByText( 'Approve' ) ).toBeInTheDocument();
|
||||||
|
expect( screen.queryByText( 'Mark as spam' ) ).toBeInTheDocument();
|
||||||
|
expect( screen.queryByText( 'Delete' ) ).toBeInTheDocument();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should trigger updateReview with status approved when Approve is clicked', () => {
|
||||||
|
const clickHandler = jest.fn( () => {
|
||||||
|
return Promise.resolve();
|
||||||
|
} );
|
||||||
|
render(
|
||||||
|
<ReviewsPanel
|
||||||
|
hasUnapprovedReviews={ true }
|
||||||
|
isError={ false }
|
||||||
|
isRequesting={ false }
|
||||||
|
reviews={ [ REVIEW ] }
|
||||||
|
updateReview={ clickHandler }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
fireEvent.click( screen.getByText( 'Approve' ) );
|
||||||
|
expect( clickHandler ).toHaveBeenCalledWith( REVIEW.id, {
|
||||||
|
status: 'approved',
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should trigger updateReview with status spam when Mark as spam is clicked', () => {
|
||||||
|
const clickHandler = jest.fn( () => {
|
||||||
|
return Promise.resolve();
|
||||||
|
} );
|
||||||
|
render(
|
||||||
|
<ReviewsPanel
|
||||||
|
hasUnapprovedReviews={ true }
|
||||||
|
isError={ false }
|
||||||
|
isRequesting={ false }
|
||||||
|
reviews={ [ REVIEW ] }
|
||||||
|
updateReview={ clickHandler }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
fireEvent.click( screen.getByText( 'Mark as spam' ) );
|
||||||
|
expect( clickHandler ).toHaveBeenCalledWith( REVIEW.id, {
|
||||||
|
status: 'spam',
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should trigger deleteReview with review id when delete is clicked', () => {
|
||||||
|
const clickHandler = jest.fn( () => {
|
||||||
|
return Promise.resolve();
|
||||||
|
} );
|
||||||
|
render(
|
||||||
|
<ReviewsPanel
|
||||||
|
hasUnapprovedReviews={ true }
|
||||||
|
isError={ false }
|
||||||
|
isRequesting={ false }
|
||||||
|
reviews={ [ REVIEW ] }
|
||||||
|
deleteReview={ clickHandler }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
fireEvent.click( screen.getByText( 'Delete' ) );
|
||||||
|
expect( clickHandler ).toHaveBeenCalledWith( REVIEW.id );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { REVIEWS_STORE_NAME } from '@woocommerce/data';
|
||||||
|
|
||||||
|
export const REVIEW_PAGE_LIMIT = 5;
|
||||||
|
|
||||||
|
export const unapprovedReviewsQuery = {
|
||||||
|
page: 1,
|
||||||
|
per_page: 1,
|
||||||
|
status: 'hold',
|
||||||
|
_embed: 1,
|
||||||
|
_fields: [ 'id' ],
|
||||||
|
};
|
||||||
|
export function getUnapprovedReviews( select ) {
|
||||||
|
const { getReviewsTotalCount, getReviewsError, isResolving } = select(
|
||||||
|
REVIEWS_STORE_NAME
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @wordpress/no-unused-vars-before-return
|
||||||
|
const totalReviews = getReviewsTotalCount( unapprovedReviewsQuery );
|
||||||
|
const isError = Boolean( getReviewsError( unapprovedReviewsQuery ) );
|
||||||
|
const isRequesting = isResolving( 'getReviewsTotalCount', [
|
||||||
|
unapprovedReviewsQuery,
|
||||||
|
] );
|
||||||
|
|
||||||
|
if ( isError || ( isRequesting && totalReviews === undefined ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalReviews;
|
||||||
|
}
|
|
@ -1,30 +1,47 @@
|
||||||
|
@mixin accordion-header {
|
||||||
|
.woocommerce-accordion-header {
|
||||||
|
order: 1;
|
||||||
|
text-align: left;
|
||||||
|
.woocommerce-accordion-title {
|
||||||
|
margin-right: $gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.woocommerce-accordion-card {
|
.woocommerce-accordion-card {
|
||||||
|
.components-card__header,
|
||||||
|
.components-panel__body-title .components-button {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 28px;
|
||||||
|
color: $gray-900;
|
||||||
|
&:focus {
|
||||||
|
box-shadow: unset;
|
||||||
|
outline: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-card__header {
|
||||||
|
height: 60px;
|
||||||
|
@include accordion-header();
|
||||||
|
}
|
||||||
|
|
||||||
.components-panel__body-title {
|
.components-panel__body-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 1em 0;
|
padding: $gap-small 0;
|
||||||
|
height: 60px;
|
||||||
.components-button {
|
.components-button {
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-left: 24px;
|
padding-left: 24px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 20px;
|
display: flex;
|
||||||
line-height: 28px;
|
flex-direction: row;
|
||||||
color: $gray-900;
|
|
||||||
&:focus {
|
@include accordion-header();
|
||||||
box-shadow: unset;
|
|
||||||
outline: unset;
|
|
||||||
}
|
|
||||||
.woocommerce-accordion-header {
|
.woocommerce-accordion-header {
|
||||||
order: 1;
|
|
||||||
text-align: left;
|
|
||||||
width: 50%;
|
width: 50%;
|
||||||
.woocommerce-accordion-title {
|
|
||||||
vertical-align: bottom;
|
|
||||||
margin-right: $gap;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& > span {
|
& > span {
|
||||||
order: 2;
|
order: 2;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { Card, PanelBody, PanelRow } from '@wordpress/components';
|
import { Card, CardHeader, PanelBody, PanelRow } from '@wordpress/components';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { useState } from '@wordpress/element';
|
import { useState, useEffect } from '@wordpress/element';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -20,6 +20,7 @@ import { Badge } from '../badge';
|
||||||
* @param {string} props.children
|
* @param {string} props.children
|
||||||
* @param {string} props.title
|
* @param {string} props.title
|
||||||
* @param {string} props.initialOpen
|
* @param {string} props.initialOpen
|
||||||
|
* @param {boolean} props.collapsible
|
||||||
* @return {Object} -
|
* @return {Object} -
|
||||||
*/
|
*/
|
||||||
const AccordionPanel = ( {
|
const AccordionPanel = ( {
|
||||||
|
@ -27,9 +28,17 @@ const AccordionPanel = ( {
|
||||||
count,
|
count,
|
||||||
title,
|
title,
|
||||||
initialOpen,
|
initialOpen,
|
||||||
|
collapsible,
|
||||||
children,
|
children,
|
||||||
} ) => {
|
} ) => {
|
||||||
const [ isPanelOpen, setIsPanelOpen ] = useState( null );
|
const [ isPanelOpen, setIsPanelOpen ] = useState( null );
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
if ( ! collapsible && isPanelOpen ) {
|
||||||
|
setIsPanelOpen( ! isPanelOpen );
|
||||||
|
}
|
||||||
|
}, [ collapsible ] );
|
||||||
|
|
||||||
const getTitleAndCount = ( titleText, countUnread ) => {
|
const getTitleAndCount = ( titleText, countUnread ) => {
|
||||||
return (
|
return (
|
||||||
<span className="woocommerce-accordion-header">
|
<span className="woocommerce-accordion-header">
|
||||||
|
@ -54,13 +63,19 @@ const AccordionPanel = ( {
|
||||||
'is-panel-opened': opened,
|
'is-panel-opened': opened,
|
||||||
} ) }
|
} ) }
|
||||||
>
|
>
|
||||||
<PanelBody
|
{ collapsible ? (
|
||||||
title={ getTitleAndCount( title, count ) }
|
<PanelBody
|
||||||
opened={ opened }
|
title={ getTitleAndCount( title, count ) }
|
||||||
onToggle={ onToggle }
|
initialOpen={ opened }
|
||||||
>
|
onToggle={ onToggle }
|
||||||
<PanelRow> { children } </PanelRow>
|
>
|
||||||
</PanelBody>
|
<PanelRow> { children } </PanelRow>
|
||||||
|
</PanelBody>
|
||||||
|
) : (
|
||||||
|
<CardHeader size="medium">
|
||||||
|
{ getTitleAndCount( title, count ) }
|
||||||
|
</CardHeader>
|
||||||
|
) }
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -82,10 +97,15 @@ AccordionPanel.propTypes = {
|
||||||
* Whether or not the panel will start open.
|
* Whether or not the panel will start open.
|
||||||
*/
|
*/
|
||||||
initialOpen: PropTypes.bool,
|
initialOpen: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* Whether or not the panel can be collapsed or not.
|
||||||
|
*/
|
||||||
|
collapsible: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
AccordionPanel.defaultProps = {
|
AccordionPanel.defaultProps = {
|
||||||
initialOpen: true,
|
initialOpen: true,
|
||||||
|
collapsible: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AccordionPanel;
|
export default AccordionPanel;
|
||||||
|
|
|
@ -50,4 +50,22 @@ describe( 'Accordion', () => {
|
||||||
expect( screen.queryByText( '10000' ) ).toBeInTheDocument();
|
expect( screen.queryByText( '10000' ) ).toBeInTheDocument();
|
||||||
expect( screen.queryByText( '20000' ) ).toBeInTheDocument();
|
expect( screen.queryByText( '20000' ) ).toBeInTheDocument();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
it( 'should only render title if collapsible is false', () => {
|
||||||
|
render(
|
||||||
|
<Accordion>
|
||||||
|
{ ' ' }
|
||||||
|
<AccordionPanel
|
||||||
|
title="empty title"
|
||||||
|
initialOpen={ false }
|
||||||
|
collapsible={ false }
|
||||||
|
>
|
||||||
|
<span>Custom panel 1</span>
|
||||||
|
</AccordionPanel>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
expect( screen.queryByText( 'empty title' ) ).toBeInTheDocument();
|
||||||
|
expect( screen.queryByRole( 'button' ) ).toBeNull();
|
||||||
|
expect( screen.queryByText( 'Custom panel 1' ) ).toBeNull();
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -12,7 +12,7 @@ import PropTypes from 'prop-types';
|
||||||
* rating in a scale between 0 and the prop `totalStars` (default 5).
|
* rating in a scale between 0 and the prop `totalStars` (default 5).
|
||||||
*/
|
*/
|
||||||
class Rating extends Component {
|
class Rating extends Component {
|
||||||
stars() {
|
stars( icon ) {
|
||||||
const { size, totalStars } = this.props;
|
const { size, totalStars } = this.props;
|
||||||
|
|
||||||
const starStyles = {
|
const starStyles = {
|
||||||
|
@ -22,13 +22,14 @@ class Rating extends Component {
|
||||||
|
|
||||||
const stars = [];
|
const stars = [];
|
||||||
for ( let i = 0; i < totalStars; i++ ) {
|
for ( let i = 0; i < totalStars; i++ ) {
|
||||||
stars.push( <StarIcon key={ 'star-' + i } style={ starStyles } /> );
|
const Icon = icon || StarIcon;
|
||||||
|
stars.push( <Icon key={ 'star-' + i } style={ starStyles } /> );
|
||||||
}
|
}
|
||||||
return stars;
|
return stars;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { rating, totalStars, className } = this.props;
|
const { rating, totalStars, className, icon, outlineIcon } = this.props;
|
||||||
|
|
||||||
const classes = classnames( 'woocommerce-rating', className );
|
const classes = classnames( 'woocommerce-rating', className );
|
||||||
const perStar = 100 / totalStars;
|
const perStar = 100 / totalStars;
|
||||||
|
@ -43,12 +44,12 @@ class Rating extends Component {
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className={ classes } aria-label={ label }>
|
<div className={ classes } aria-label={ label }>
|
||||||
{ this.stars() }
|
{ this.stars( icon ) }
|
||||||
<div
|
<div
|
||||||
className="woocommerce-rating__star-outline"
|
className="woocommerce-rating__star-outline"
|
||||||
style={ outlineStyles }
|
style={ outlineStyles }
|
||||||
>
|
>
|
||||||
{ this.stars() }
|
{ this.stars( outlineIcon || icon ) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -72,6 +73,14 @@ Rating.propTypes = {
|
||||||
* Additional CSS classes.
|
* Additional CSS classes.
|
||||||
*/
|
*/
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Icon used, defaults to StarIcon
|
||||||
|
*/
|
||||||
|
icon: PropTypes.elementType,
|
||||||
|
/**
|
||||||
|
* Outline icon used, the not selected rating. Defaults to props.icon or StarIcon
|
||||||
|
*/
|
||||||
|
outlineIcon: PropTypes.elementType,
|
||||||
};
|
};
|
||||||
|
|
||||||
Rating.defaultProps = {
|
Rating.defaultProps = {
|
||||||
|
|
|
@ -155,6 +155,161 @@ exports[`ProductRating should render rating based on product object 1`] = `
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Rating should render different icons if specified 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
aria-label="2 out of 5 stars."
|
||||||
|
class="woocommerce-rating"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="gridicon gridicons-star-outline"
|
||||||
|
height="24"
|
||||||
|
style="width: 18px; height: 18px;"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M12 6.308l1.176 3.167.347.936.997.042 3.374.14-2.647 2.09-.784.62.27.963.91 3.25-2.813-1.872-.83-.553-.83.552-2.814 1.87.91-3.248.27-.962-.783-.62-2.648-2.092 3.374-.14.996-.04.347-.936L12 6.308M12 2L9.418 8.953 2 9.257l5.822 4.602L5.82 21 12 16.89 18.18 21l-2.002-7.14L22 9.256l-7.418-.305L12 2z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="gridicon gridicons-star-outline"
|
||||||
|
height="24"
|
||||||
|
style="width: 18px; height: 18px;"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M12 6.308l1.176 3.167.347.936.997.042 3.374.14-2.647 2.09-.784.62.27.963.91 3.25-2.813-1.872-.83-.553-.83.552-2.814 1.87.91-3.248.27-.962-.783-.62-2.648-2.092 3.374-.14.996-.04.347-.936L12 6.308M12 2L9.418 8.953 2 9.257l5.822 4.602L5.82 21 12 16.89 18.18 21l-2.002-7.14L22 9.256l-7.418-.305L12 2z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="gridicon gridicons-star-outline"
|
||||||
|
height="24"
|
||||||
|
style="width: 18px; height: 18px;"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M12 6.308l1.176 3.167.347.936.997.042 3.374.14-2.647 2.09-.784.62.27.963.91 3.25-2.813-1.872-.83-.553-.83.552-2.814 1.87.91-3.248.27-.962-.783-.62-2.648-2.092 3.374-.14.996-.04.347-.936L12 6.308M12 2L9.418 8.953 2 9.257l5.822 4.602L5.82 21 12 16.89 18.18 21l-2.002-7.14L22 9.256l-7.418-.305L12 2z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="gridicon gridicons-star-outline"
|
||||||
|
height="24"
|
||||||
|
style="width: 18px; height: 18px;"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M12 6.308l1.176 3.167.347.936.997.042 3.374.14-2.647 2.09-.784.62.27.963.91 3.25-2.813-1.872-.83-.553-.83.552-2.814 1.87.91-3.248.27-.962-.783-.62-2.648-2.092 3.374-.14.996-.04.347-.936L12 6.308M12 2L9.418 8.953 2 9.257l5.822 4.602L5.82 21 12 16.89 18.18 21l-2.002-7.14L22 9.256l-7.418-.305L12 2z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="gridicon gridicons-star-outline"
|
||||||
|
height="24"
|
||||||
|
style="width: 18px; height: 18px;"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M12 6.308l1.176 3.167.347.936.997.042 3.374.14-2.647 2.09-.784.62.27.963.91 3.25-2.813-1.872-.83-.553-.83.552-2.814 1.87.91-3.248.27-.962-.783-.62-2.648-2.092 3.374-.14.996-.04.347-.936L12 6.308M12 2L9.418 8.953 2 9.257l5.822 4.602L5.82 21 12 16.89 18.18 21l-2.002-7.14L22 9.256l-7.418-.305L12 2z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
class="woocommerce-rating__star-outline"
|
||||||
|
style="width: 40%;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="gridicon gridicons-star"
|
||||||
|
height="24"
|
||||||
|
style="width: 18px; height: 18px;"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M12 2l2.582 6.953L22 9.257l-5.822 4.602L18.18 21 12 16.89 5.82 21l2.002-7.14L2 9.256l7.418-.304"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="gridicon gridicons-star"
|
||||||
|
height="24"
|
||||||
|
style="width: 18px; height: 18px;"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M12 2l2.582 6.953L22 9.257l-5.822 4.602L18.18 21 12 16.89 5.82 21l2.002-7.14L2 9.256l7.418-.304"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="gridicon gridicons-star"
|
||||||
|
height="24"
|
||||||
|
style="width: 18px; height: 18px;"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M12 2l2.582 6.953L22 9.257l-5.822 4.602L18.18 21 12 16.89 5.82 21l2.002-7.14L2 9.256l7.418-.304"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="gridicon gridicons-star"
|
||||||
|
height="24"
|
||||||
|
style="width: 18px; height: 18px;"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M12 2l2.582 6.953L22 9.257l-5.822 4.602L18.18 21 12 16.89 5.82 21l2.002-7.14L2 9.256l7.418-.304"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="gridicon gridicons-star"
|
||||||
|
height="24"
|
||||||
|
style="width: 18px; height: 18px;"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
d="M12 2l2.582 6.953L22 9.257l-5.822 4.602L18.18 21 12 16.89 5.82 21l2.002-7.14L2 9.256l7.418-.304"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Rating should render stars at a different size 1`] = `
|
exports[`Rating should render stars at a different size 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
|
import StarIcon from 'gridicons/dist/star';
|
||||||
|
import StarOutlineIcon from 'gridicons/dist/star-outline';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -27,6 +29,17 @@ describe( 'Rating', () => {
|
||||||
const { container } = render( <Rating rating={ 1 } size={ 36 } /> );
|
const { container } = render( <Rating rating={ 1 } size={ 36 } /> );
|
||||||
expect( container ).toMatchSnapshot();
|
expect( container ).toMatchSnapshot();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
test( 'should render different icons if specified', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Rating
|
||||||
|
rating={ 2 }
|
||||||
|
icon={ StarOutlineIcon }
|
||||||
|
outlineIcon={ StarIcon }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect( container ).toMatchSnapshot();
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
describe( 'ReviewRating', () => {
|
describe( 'ReviewRating', () => {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
const TYPES = {
|
const TYPES = {
|
||||||
UPDATE_REVIEWS: 'UPDATE_REVIEWS',
|
UPDATE_REVIEWS: 'UPDATE_REVIEWS',
|
||||||
|
SET_REVIEW: 'SET_REVIEW',
|
||||||
SET_ERROR: 'SET_ERROR',
|
SET_ERROR: 'SET_ERROR',
|
||||||
|
SET_REVIEW_IS_UPDATING: 'SET_REVIEW_IS_UPDATING',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TYPES;
|
export default TYPES;
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { apiFetch } from '@wordpress/data-controls';
|
||||||
|
import { addQueryArgs } from '@wordpress/url';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import TYPES from './action-types';
|
import TYPES from './action-types';
|
||||||
|
import { NAMESPACE } from '../constants';
|
||||||
|
|
||||||
export function updateReviews( query, reviews, totalCount ) {
|
export function updateReviews( query, reviews, totalCount ) {
|
||||||
return {
|
return {
|
||||||
|
@ -12,6 +19,60 @@ export function updateReviews( query, reviews, totalCount ) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* updateReview( reviewId, reviewFields, query ) {
|
||||||
|
yield setReviewIsUpdating( reviewId, true );
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = addQueryArgs(
|
||||||
|
`${ NAMESPACE }/products/reviews/${ reviewId }`,
|
||||||
|
query || {}
|
||||||
|
);
|
||||||
|
const review = yield apiFetch( {
|
||||||
|
path: url,
|
||||||
|
method: 'PUT',
|
||||||
|
data: reviewFields,
|
||||||
|
} );
|
||||||
|
yield setReview( reviewId, review );
|
||||||
|
yield setReviewIsUpdating( reviewId, false );
|
||||||
|
} catch ( error ) {
|
||||||
|
yield setError( 'updateReview', error );
|
||||||
|
yield setReviewIsUpdating( reviewId, false );
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* deleteReview( reviewId ) {
|
||||||
|
yield setReviewIsUpdating( reviewId, true );
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `${ NAMESPACE }/products/reviews/${ reviewId }`;
|
||||||
|
const response = yield apiFetch( { path: url, method: 'DELETE' } );
|
||||||
|
yield setReview( reviewId, response );
|
||||||
|
yield setReviewIsUpdating( reviewId, false );
|
||||||
|
return response;
|
||||||
|
} catch ( error ) {
|
||||||
|
yield setError( 'deleteReview', error );
|
||||||
|
yield setReviewIsUpdating( reviewId, false );
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setReviewIsUpdating( reviewId, isUpdating ) {
|
||||||
|
return {
|
||||||
|
type: TYPES.SET_REVIEW_IS_UPDATING,
|
||||||
|
reviewId,
|
||||||
|
isUpdating,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setReview( reviewId, reviewData ) {
|
||||||
|
return {
|
||||||
|
type: TYPES.SET_REVIEW,
|
||||||
|
reviewId,
|
||||||
|
reviewData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function setError( query, error ) {
|
export function setError( query, error ) {
|
||||||
return {
|
return {
|
||||||
type: TYPES.SET_ERROR,
|
type: TYPES.SET_ERROR,
|
||||||
|
|
|
@ -9,7 +9,16 @@ const reducer = (
|
||||||
errors: {},
|
errors: {},
|
||||||
data: {},
|
data: {},
|
||||||
},
|
},
|
||||||
{ type, query, reviews, totalCount, error }
|
{
|
||||||
|
type,
|
||||||
|
query,
|
||||||
|
reviews,
|
||||||
|
reviewId,
|
||||||
|
reviewData,
|
||||||
|
totalCount,
|
||||||
|
error,
|
||||||
|
isUpdating,
|
||||||
|
}
|
||||||
) => {
|
) => {
|
||||||
switch ( type ) {
|
switch ( type ) {
|
||||||
case TYPES.UPDATE_REVIEWS:
|
case TYPES.UPDATE_REVIEWS:
|
||||||
|
@ -30,6 +39,14 @@ const reducer = (
|
||||||
...nextReviews,
|
...nextReviews,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
case TYPES.SET_REVIEW:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
data: {
|
||||||
|
...state.data,
|
||||||
|
[ reviewId ]: reviewData,
|
||||||
|
},
|
||||||
|
};
|
||||||
case TYPES.SET_ERROR:
|
case TYPES.SET_ERROR:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -38,6 +55,17 @@ const reducer = (
|
||||||
[ JSON.stringify( query ) ]: error,
|
[ JSON.stringify( query ) ]: error,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
case TYPES.SET_REVIEW_IS_UPDATING:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
data: {
|
||||||
|
...state.data,
|
||||||
|
[ reviewId ]: {
|
||||||
|
...state.data[ reviewId ],
|
||||||
|
isUpdating,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,8 @@ export const getReviews = ( state, query ) => {
|
||||||
export const getReviewsTotalCount = ( state, query ) => {
|
export const getReviewsTotalCount = ( state, query ) => {
|
||||||
const stringifiedQuery = JSON.stringify( query );
|
const stringifiedQuery = JSON.stringify( query );
|
||||||
return (
|
return (
|
||||||
( state.reviews[ stringifiedQuery ] &&
|
state.reviews[ stringifiedQuery ] &&
|
||||||
state.reviews[ stringifiedQuery ].totalCount ) ||
|
state.reviews[ stringifiedQuery ].totalCount
|
||||||
0
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -62,4 +62,50 @@ describe( 'reviews reducer', () => {
|
||||||
const stringifiedQuery = JSON.stringify( query );
|
const stringifiedQuery = JSON.stringify( query );
|
||||||
expect( state.errors[ stringifiedQuery ] ).toBe( error );
|
expect( state.errors[ stringifiedQuery ] ).toBe( error );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
it( 'should handle SET_REVIEW', () => {
|
||||||
|
const state = reducer(
|
||||||
|
{
|
||||||
|
...defaultState,
|
||||||
|
data: {
|
||||||
|
4: { title: 'test' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: TYPES.SET_REVIEW,
|
||||||
|
reviewId: 4,
|
||||||
|
reviewData: {
|
||||||
|
title: 'test updated',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect( state.data[ 4 ].title ).toEqual( 'test updated' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should handle SET_REVIEW_IS_UPDATING', () => {
|
||||||
|
const state = reducer(
|
||||||
|
{
|
||||||
|
...defaultState,
|
||||||
|
data: {
|
||||||
|
4: { title: 'test' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: TYPES.SET_REVIEW_IS_UPDATING,
|
||||||
|
reviewId: 4,
|
||||||
|
isUpdating: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect( state.data[ 4 ].isUpdating ).toEqual( true );
|
||||||
|
|
||||||
|
const newstate = reducer( state, {
|
||||||
|
type: TYPES.SET_REVIEW_IS_UPDATING,
|
||||||
|
reviewId: 4,
|
||||||
|
isUpdating: false,
|
||||||
|
} );
|
||||||
|
|
||||||
|
expect( newstate.data[ 4 ].isUpdating ).toEqual( false );
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
Loading…
Reference in New Issue