-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Store: Add the ability to reply to product reviews. (#18377)
* Add the ability to reply to product reviews. * Handle PR Feedback: Single quotes for deleted string, more accurate comparison on text area height, and make the submit box padding a bit wider to make it easier to hit.
- Loading branch information
1 parent
bccc3d3
commit 7dd54ad
Showing
10 changed files
with
444 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
175 changes: 175 additions & 0 deletions
175
client/extensions/woocommerce/app/reviews/review-reply-create.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
/** | ||
* External depedencies | ||
*/ | ||
import React, { Component } from 'react'; | ||
import { bindActionCreators } from 'redux'; | ||
import classNames from 'classnames'; | ||
import { connect } from 'react-redux'; | ||
import { localize } from 'i18n-calypso'; | ||
import PropTypes from 'prop-types'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { createReviewReply } from 'woocommerce/state/sites/review-replies/actions'; | ||
import { editReviewReply, clearReviewReplyEdits } from 'woocommerce/state/ui/review-replies/actions'; | ||
import { getCurrentUser } from 'state/current-user/selectors'; | ||
import { getReviewReplyEdits } from 'woocommerce/state/ui/review-replies/selectors'; | ||
import Gravatar from 'components/gravatar'; | ||
import { successNotice } from 'state/notices/actions'; | ||
|
||
// Matches comments reply box heights | ||
const TEXTAREA_HEIGHT_COLLAPSED = 47; // 1 line | ||
const TEXTAREA_HEIGHT_FOCUSED = 68; // 2 lines | ||
const TEXTAREA_MAX_HEIGHT = 236; // 10 lines | ||
const TEXTAREA_VERTICAL_BORDER = 2; | ||
|
||
class ReviewReplyCreate extends Component { | ||
static propTypes = { | ||
siteId: PropTypes.number.isRequired, | ||
review: PropTypes.shape( { | ||
status: PropTypes.string, | ||
} ).isRequired, | ||
}; | ||
|
||
state = { | ||
hasFocus: false, | ||
textareaHeight: TEXTAREA_HEIGHT_COLLAPSED, | ||
}; | ||
|
||
bindTextareaRef = ( textarea ) => { | ||
this.textarea = textarea; | ||
} | ||
|
||
calculateTextareaHeight = () => { | ||
const textareaScrollHeight = this.textarea.scrollHeight; | ||
const textareaHeight = Math.min( TEXTAREA_MAX_HEIGHT, textareaScrollHeight + TEXTAREA_VERTICAL_BORDER ); | ||
return Math.max( TEXTAREA_HEIGHT_FOCUSED, textareaHeight ); | ||
} | ||
|
||
getTextareaPlaceholder = () => { | ||
const { review, translate } = this.props; | ||
if ( 'approved' === review.status ) { | ||
return translate( 'Reply to %(reviewAuthor)s…', { args: { reviewAuthor: review.name } } ); | ||
} | ||
return translate( 'Approve and reply to %(reviewAuthor)s…', { args: { reviewAuthor: review.name } } ); | ||
} | ||
|
||
onTextChange = ( event ) => { | ||
const { siteId, review } = this.props; | ||
const { value } = event.target; | ||
|
||
this.props.editReviewReply( siteId, review.id, { content: value } ); | ||
|
||
const textareaHeight = this.calculateTextareaHeight(); | ||
this.setState( { | ||
textareaHeight, | ||
} ); | ||
} | ||
|
||
setFocus = () => this.setState( { | ||
hasFocus: true, | ||
textareaHeight: this.calculateTextareaHeight(), | ||
} ); | ||
|
||
unsetFocus = () => this.setState( { | ||
hasFocus: false, | ||
textareaHeight: TEXTAREA_HEIGHT_COLLAPSED, | ||
} ); | ||
|
||
onSubmit = ( event ) => { | ||
event.preventDefault(); | ||
const { siteId, review, commentText, translate } = this.props; | ||
const { product } = review; | ||
|
||
const shouldApprove = 'pending' === review.status ? true : false; | ||
|
||
this.props.createReviewReply( siteId, product.id, review.id, commentText, shouldApprove ); | ||
this.props.clearReviewReplyEdits( siteId ); | ||
|
||
this.props.successNotice( | ||
translate( 'Reply submitted.' ), | ||
{ duration: 5000 } | ||
); | ||
} | ||
|
||
render() { | ||
const { translate, currentUser, commentText } = this.props; | ||
const { hasFocus, textareaHeight } = this.state; | ||
|
||
const hasCommentText = commentText.trim().length > 0; | ||
|
||
// Only show the scrollbar if the textarea content exceeds the max height | ||
const hasScrollbar = textareaHeight >= TEXTAREA_MAX_HEIGHT; | ||
|
||
const buttonClasses = classNames( 'reviews__reply-submit', { | ||
'has-scrollbar': hasScrollbar, | ||
'is-active': hasCommentText, | ||
'is-visible': hasFocus || hasCommentText, | ||
} ); | ||
const gravatarClasses = classNames( { 'is-visible': ! hasFocus } ); | ||
const textareaClasses = classNames( { | ||
'has-content': hasCommentText, | ||
'has-focus': hasFocus, | ||
'has-scrollbar': hasScrollbar, | ||
} ); | ||
|
||
// Without focus, force the textarea to collapse even if it was manually resized | ||
const textareaStyle = { | ||
height: hasFocus ? textareaHeight : TEXTAREA_HEIGHT_COLLAPSED, | ||
}; | ||
|
||
return ( | ||
<form className="reviews__reply-textarea"> | ||
<textarea | ||
className={ textareaClasses } | ||
onBlur={ this.unsetFocus } | ||
onChange={ this.onTextChange } | ||
onFocus={ this.setFocus } | ||
placeholder={ this.getTextareaPlaceholder() } | ||
ref={ this.bindTextareaRef } | ||
style={ textareaStyle } | ||
value={ commentText } | ||
/> | ||
|
||
<Gravatar | ||
className={ gravatarClasses } | ||
size={ 24 } | ||
user={ currentUser } | ||
/> | ||
|
||
<button | ||
className={ buttonClasses } | ||
disabled={ ! hasCommentText } | ||
onClick={ this.onSubmit } | ||
> | ||
{ translate( 'Send' ) } | ||
</button> | ||
</form> | ||
); | ||
} | ||
|
||
} | ||
|
||
function mapStateToProps( state ) { | ||
const replyEdits = getReviewReplyEdits( state ); | ||
const commentText = replyEdits.content || ''; | ||
return { | ||
currentUser: getCurrentUser( state ), | ||
commentText, | ||
}; | ||
} | ||
|
||
function mapDispatchToProps( dispatch ) { | ||
return bindActionCreators( | ||
{ | ||
editReviewReply, | ||
clearReviewReplyEdits, | ||
createReviewReply, | ||
successNotice, | ||
}, | ||
dispatch | ||
); | ||
} | ||
|
||
export default connect( mapStateToProps, mapDispatchToProps )( localize( ReviewReplyCreate ) ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.