woocommerce/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/index.js

408 lines
9.3 KiB
JavaScript
Raw Normal View History

/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import classnames from 'classnames';
import { Component, Fragment } from '@wordpress/element';
import { Button, Tooltip } from '@wordpress/components';
import { compose } from '@wordpress/compose';
import { withSelect, withDispatch } from '@wordpress/data';
import PropTypes from 'prop-types';
import StarIcon from 'gridicons/dist/star';
import StarOutlineIcon from 'gridicons/dist/star-outline';
import interpolateComponents from '@automattic/interpolate-components';
import {
EmptyContent,
Link,
ReviewRating,
ProductImage,
Section,
} from '@woocommerce/components';
import { getAdminLink } from '@woocommerce/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 '~/activity-panel/activity-card';
import CheckmarkCircleIcon from './checkmark-circle-icon';
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' ),
{
actions: [
{
label: __( 'Undo', 'woocommerce' ),
onClick: () => {
updateReview(
reviewId,
{
status: 'untrash',
},
{
_embed: 1,
}
).then( () => clearReviewsCache() );
},
},
],
}
);
} )
.catch( () => {
createNotice(
'error',
__( 'Review could not be deleted.', 'woocommerce' )
);
} );
}
}
updateReviewStatus( reviewId, newStatus, oldStatus ) {
const { createNotice, updateReview, clearReviewsCache } = this.props;
if ( reviewId ) {
updateReview( reviewId, { status: newStatus } )
.then( () => {
clearReviewsCache();
createNotice(
'success',
__( 'Review successfully updated.', 'woocommerce' ),
{
actions: [
{
label: __( 'Undo', 'woocommerce' ),
onClick: () => {
updateReview(
reviewId,
{
status: oldStatus,
},
{
_embed: 1,
}
).then( () => clearReviewsCache() );
},
},
],
}
);
} )
.catch( () => {
createNotice(
'error',
__( 'Review could not be updated.', 'woocommerce' )
);
} );
}
}
renderReview( review ) {
const product =
( review &&
review._embedded &&
review._embedded.up &&
review._embedded.up[ 0 ] ) ||
null;
if ( review.isUpdating ) {
return (
<ActivityCardPlaceholder
key={ review.id }
className="woocommerce-review-activity-card"
hasAction
hasDate
lines={ 1 }
/>
);
}
if ( isNull( product ) || review.status !== reviewsQuery.status ) {
return null;
}
const title = interpolateComponents( {
mixedString: sprintf(
__(
'{{authorLink}}%s{{/authorLink}}{{verifiedCustomerIcon/}} reviewed {{productLink}}%s{{/productLink}}',
'woocommerce'
),
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"
/>
),
verifiedCustomerIcon: review.verified ? (
<span className="woocommerce-review-activity-card__verified">
<Tooltip text={ __( 'Verified owner', 'woocommerce' ) }>
<span>
<CheckmarkCircleIcon />
</span>
</Tooltip>
</span>
) : null,
},
} );
const subtitle = (
<Fragment>
<ReviewRating
review={ review }
icon={ StarOutlineIcon }
outlineIcon={ StarIcon }
size={ 13 }
/>
</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' ) }
</Button>,
<Button
key="spam-action"
isTertiary
onClick={ () => {
this.recordReviewEvent( 'mark_as_spam', manageReviewEvent );
this.updateReviewStatus( review.id, 'spam', review.status );
} }
>
{ __( 'Mark as spam', 'woocommerce' ) }
</Button>,
<Button
key="delete-action"
isDestructive
isTertiary
onClick={ () => {
this.recordReviewEvent( 'delete', manageReviewEvent );
this.deleteReview( review.id );
} }
>
{ __( 'Delete', 'woocommerce' ) }
</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 ) {
const renderedReviews = reviews.map( ( review ) =>
this.renderReview( review, this.props )
);
if ( renderedReviews.filter( Boolean ).length === 0 ) {
return <></>;
}
return (
<>
{ renderedReviews }
<Link
href={ getAdminLink(
'edit.php?post_type=product&page=product-reviews'
) }
onClick={ () => this.recordReviewEvent( 'reviews_manage' ) }
className="woocommerce-layout__activity-panel-outbound-link woocommerce-layout__activity-panel-empty"
type="wp-admin"
>
{ __( 'Manage all reviews', 'woocommerce' ) }
</Link>
</>
);
}
render() {
const { isRequesting, isError, reviews } = this.props;
if ( isError ) {
const title = __(
'There was an error getting your reviews. Please try again.',
'woocommerce'
);
const actionLabel = __( 'Reload', 'woocommerce' );
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>
Show task and activity notifications in the Inbox panel (https://github.com/woocommerce/woocommerce-admin/pull/7017) * Added abbreviated panels This commit adds abbreviated panels * Added notifications getter * Variables renamed * Added unread-indicators refactor * Open panel by default * Refactor unread-indicators * Renamed a few files and added event recording * Modified "critical alert" presentation * Removed useless control * Renamed const * Added control to InboxPanel component * Multiple critical alerts handling * Fixed styles * Moved Inbox panel styles # Conflicts: # packages/experimental/src/inbox-note/style.scss * Added tests * Inbox panel width reduced * Small refactor for unread notifications * Renamed abbreviated card component * Added changelog # Conflicts: # readme.txt # Conflicts: # readme.txt * Renamed inbox-panel and the cards config file * Renamed unread notifications variable * Fixed abbreviated card box-shadow * Small refactor to unread-indicators file * Refactored method getInitialState * Added scroll to task list # Conflicts: # client/task-list/task-list.js * Small CSS changes to titles * Fixed changelog # Conflicts: # readme.txt # Conflicts: # readme.txt * Added param to filter `woocommerce_admin_onboarding_task_list` * Removed extensibility from `getAbbreviatedNotifications` * Fixed chunk name * Removed `critical` prop from `AbbreviatedCard` comopnent * Moved AbbreviatedCard component to `packages` This commit moves the component `AbbreviatedCard` to `packages` # Conflicts: # docs/components/_sidebar.md # packages/components/CHANGELOG.md # packages/components/src/index.js # Conflicts: # packages/components/CHANGELOG.md * Removed `critical alerts` tag from abbreviated card This commit removes the tag `critical alerts` from the `Things to do next` abbreviated card * Removed filter `woocommerce_admin_abbreviated_card_list` * Fixed icon * Added defaut value to `hasUnreadNotifications` * Fix mapSelect error when the dismissed tasks option isn't populated. * Added AbbreviatedNotificationsPanel * Added tests * Renamed `getUnreadNotes` to `isNotesPanelVisible` * Removed abbreviated-card.js * Added singular/plural copy handling * Renamed method `getInitialState` to `getInitialOpenState` * Fixed Link prop * Revert "Fixed Link prop" This reverts commit 74e6a7fae030766eb5d6be098caa15478f2cb2c6. * Fixed Link prop * Added task list visibility check * Fixed scroll after redirect # Conflicts: # client/task-list/index.js * Added propType to `AbbreviatedCard` * Fixed `Add-task doc example * Removed default values from ActivityPanel * Fixed multiple calls to a filter Co-authored-by: Fernando Marichal <contacto@fernandomarichal.com> Co-authored-by: Jeff Stieler <jeff.m.stieler@gmail.com>
2021-06-09 13:56:45 +00:00
{ isRequesting || ! reviews.length ? (
<ActivityCardPlaceholder
className="woocommerce-review-activity-card"
hasAction
hasDate
lines={ 1 }
/>
) : (
<>{ this.renderReviews( reviews ) }</>
) }
</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 );