Add restore functionality to comments for admins

This commit is contained in:
Paul Kaplan 2018-10-16 10:00:38 -04:00
parent b0ac4018ee
commit 592c0e5703
7 changed files with 87 additions and 16 deletions

View file

@ -206,6 +206,7 @@
"comments.report": "Report", "comments.report": "Report",
"comments.delete": "Delete", "comments.delete": "Delete",
"comments.restore": "Restore",
"comments.reportModal.title": "Report Comment", "comments.reportModal.title": "Report Comment",
"comments.reportModal.reported": "The comment has been reported, and the Scratch Team has been notified.", "comments.reportModal.reported": "The comment has been reported, and the Scratch Team has been notified.",
"comments.reportModal.prompt": "Are you sure you want to report this comment?", "comments.reportModal.prompt": "Are you sure you want to report this comment?",

View file

@ -246,6 +246,15 @@ module.exports.setCommentReported = (commentId, topLevelCommentId) => ({
} }
}); });
module.exports.setCommentRestored = (commentId, topLevelCommentId) => ({
type: 'UPDATE_COMMENT',
commentId: commentId,
topLevelCommentId: topLevelCommentId,
comment: {
visibility: 'visible'
}
});
module.exports.addNewComment = (comment, topLevelCommentId) => ({ module.exports.addNewComment = (comment, topLevelCommentId) => ({
type: 'ADD_NEW_COMMENT', type: 'ADD_NEW_COMMENT',
comment: comment, comment: comment,
@ -659,6 +668,22 @@ module.exports.reportComment = (projectId, commentId, topLevelCommentId, token)
}); });
}); });
module.exports.restoreComment = (projectId, commentId, topLevelCommentId, token) => (dispatch => {
api({
uri: `/proxy/admin/project/${projectId}/comment/${commentId}/undelete`,
authentication: token,
withCredentials: true,
method: 'PUT',
useCsrf: true
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
log.error(err || res.body);
return;
}
dispatch(module.exports.setCommentRestored(commentId, topLevelCommentId));
});
});
module.exports.reportProject = (id, jsonData, token) => (dispatch => { module.exports.reportProject = (id, jsonData, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('report', module.exports.Status.FETCHING)); dispatch(module.exports.setFetchStatus('report', module.exports.Status.FETCHING));
// scratchr2 will fail if no thumbnail base64 string provided. We don't yet have // scratchr2 will fail if no thumbnail base64 string provided. We don't yet have

View file

@ -13,10 +13,6 @@ const ReportCommentModal = require('../../../components/modal/comments/report-co
require('./comment.scss'); require('./comment.scss');
const CommentVisibility = {
VISIBLE: 'visible' // Has to match the server response for visibility
};
class Comment extends React.Component { class Comment extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
@ -28,7 +24,8 @@ class Comment extends React.Component {
'handleConfirmReport', 'handleConfirmReport',
'handleCancelReport', 'handleCancelReport',
'handlePostReply', 'handlePostReply',
'handleToggleReplying' 'handleToggleReplying',
'handleRestore'
]); ]);
this.state = { this.state = {
deleting: false, deleting: false,
@ -64,6 +61,10 @@ class Comment extends React.Component {
this.setState({reporting: true}); this.setState({reporting: true});
} }
handleRestore () {
this.props.onRestore(this.props.id);
}
handleConfirmReport () { handleConfirmReport () {
this.setState({ this.setState({
reporting: false, reporting: false,
@ -93,7 +94,7 @@ class Comment extends React.Component {
visibility visibility
} = this.props; } = this.props;
const visible = visibility === CommentVisibility.VISIBLE; const visible = visibility === 'visible';
return ( return (
<div <div
@ -128,10 +129,19 @@ class Comment extends React.Component {
</span> </span>
</React.Fragment> </React.Fragment>
) : ( ) : (
<span className="comment-visibility"> <React.Fragment>
<FormattedMessage id={`comments.status.${visibility}`} /> <span className="comment-visibility">
{/* TODO restore action will go here */} <FormattedMessage id={`comments.status.${visibility}`} />
</span> </span>
{this.props.onRestore && (
<span
className="comment-restore"
onClick={this.handleRestore}
>
<FormattedMessage id="comments.restore" />
</span>
)}
</React.Fragment>
)} )}
</div> </div>
</FlexRow> </FlexRow>
@ -210,6 +220,7 @@ Comment.propTypes = {
onAddComment: PropTypes.func, onAddComment: PropTypes.func,
onDelete: PropTypes.func, onDelete: PropTypes.func,
onReport: PropTypes.func, onReport: PropTypes.func,
onRestore: PropTypes.func,
projectId: PropTypes.string, projectId: PropTypes.string,
visibility: PropTypes.string visibility: PropTypes.string
}; };

View file

@ -78,7 +78,8 @@
} }
.comment-delete, .comment-delete,
.comment-report { .comment-report,
.comment-restore {
opacity: .5; opacity: .5;
font-size: .75rem; font-size: .75rem;
font-weight: 500; font-weight: 500;
@ -119,6 +120,18 @@
vertical-align: -.125rem; vertical-align: -.125rem;
} }
} }
.comment-restore {
margin-left: 1rem;
&:before {
margin-right: .25rem;
background-image: url("/svgs/project/restore-gray.svg");
width: .75rem;
height: .75rem;
vertical-align: -.125rem;
}
}
} }
.avatar { .avatar {

View file

@ -16,7 +16,8 @@ class TopLevelComment extends React.Component {
'handleExpandThread', 'handleExpandThread',
'handleAddComment', 'handleAddComment',
'handleDeleteReply', 'handleDeleteReply',
'handleReportReply' 'handleReportReply',
'handleRestoreReply'
]); ]);
this.state = { this.state = {
expanded: false expanded: false
@ -29,16 +30,20 @@ class TopLevelComment extends React.Component {
}); });
} }
handleDeleteReply (commentId) { handleDeleteReply (replyId) {
// Only apply topLevelCommentId for deleting replies // Only apply topLevelCommentId for deleting replies
// The top level comment itself just gets passed onDelete directly // The top level comment itself just gets passed onDelete directly
this.props.onDelete(commentId, this.props.id); this.props.onDelete(replyId, this.props.id);
} }
handleReportReply (commentId) { handleReportReply (replyId) {
// Only apply topLevelCommentId for reporting replies // Only apply topLevelCommentId for reporting replies
// The top level comment itself just gets passed onReport directly // The top level comment itself just gets passed onReport directly
this.props.onReport(commentId, this.props.id); this.props.onReport(replyId, this.props.id);
}
handleRestoreReply (replyId) {
this.props.onRestore(replyId, this.props.id);
} }
handleAddComment (comment) { handleAddComment (comment) {
@ -55,6 +60,7 @@ class TopLevelComment extends React.Component {
id, id,
onDelete, onDelete,
onReport, onReport,
onRestore,
replies, replies,
projectId, projectId,
visibility visibility
@ -74,6 +80,7 @@ class TopLevelComment extends React.Component {
id, id,
onDelete, onDelete,
onReport, onReport,
onRestore,
visibility visibility
}} }}
/> />
@ -100,6 +107,7 @@ class TopLevelComment extends React.Component {
onAddComment={this.handleAddComment} onAddComment={this.handleAddComment}
onDelete={this.handleDeleteReply} onDelete={this.handleDeleteReply}
onReport={this.handleReportReply} onReport={this.handleReportReply}
onRestore={this.handleRestoreReply}
/> />
))} ))}
</FlexRow> </FlexRow>
@ -136,6 +144,7 @@ TopLevelComment.propTypes = {
onAddComment: PropTypes.func, onAddComment: PropTypes.func,
onDelete: PropTypes.func, onDelete: PropTypes.func,
onReport: PropTypes.func, onReport: PropTypes.func,
onRestore: PropTypes.func,
parentId: PropTypes.number, parentId: PropTypes.number,
projectId: PropTypes.string, projectId: PropTypes.string,
replies: PropTypes.arrayOf(PropTypes.object), replies: PropTypes.arrayOf(PropTypes.object),

View file

@ -78,6 +78,7 @@ const PreviewPresentation = ({
onReportClose, onReportClose,
onReportComment, onReportComment,
onReportSubmit, onReportSubmit,
onRestoreComment,
onAddToStudioClicked, onAddToStudioClicked,
onAddToStudioClosed, onAddToStudioClosed,
onToggleStudio, onToggleStudio,
@ -375,6 +376,7 @@ const PreviewPresentation = ({
onAddComment={onAddComment} onAddComment={onAddComment}
onDelete={onDeleteComment} onDelete={onDeleteComment}
onReport={onReportComment} onReport={onReportComment}
onRestore={onRestoreComment}
/> />
))} ))}
{comments.length < projectInfo.stats.comments && {comments.length < projectInfo.stats.comments &&
@ -431,6 +433,7 @@ PreviewPresentation.propTypes = {
onReportClose: PropTypes.func.isRequired, onReportClose: PropTypes.func.isRequired,
onReportComment: PropTypes.func.isRequired, onReportComment: PropTypes.func.isRequired,
onReportSubmit: PropTypes.func.isRequired, onReportSubmit: PropTypes.func.isRequired,
onRestoreComment: PropTypes.func,
onSeeInside: PropTypes.func, onSeeInside: PropTypes.func,
onShare: PropTypes.func, onShare: PropTypes.func,
onToggleComments: PropTypes.func, onToggleComments: PropTypes.func,

View file

@ -44,6 +44,7 @@ class Preview extends React.Component {
'handleReportClose', 'handleReportClose',
'handleReportComment', 'handleReportComment',
'handleReportSubmit', 'handleReportSubmit',
'handleRestoreComment',
'handleAddToStudioClick', 'handleAddToStudioClick',
'handleAddToStudioClose', 'handleAddToStudioClose',
'handleSeeInside', 'handleSeeInside',
@ -188,6 +189,9 @@ class Preview extends React.Component {
handleReportComment (id, topLevelCommentId) { handleReportComment (id, topLevelCommentId) {
this.props.handleReportComment(this.state.projectId, id, topLevelCommentId, this.props.user.token); this.props.handleReportComment(this.state.projectId, id, topLevelCommentId, this.props.user.token);
} }
handleRestoreComment (id, topLevelCommentId) {
this.props.handleRestoreComment(this.state.projectId, id, topLevelCommentId, this.props.user.token);
}
handleReportClick () { handleReportClick () {
this.setState({reportOpen: true}); this.setState({reportOpen: true});
} }
@ -379,6 +383,7 @@ class Preview extends React.Component {
onReportClose={this.handleReportClose} onReportClose={this.handleReportClose}
onReportComment={this.handleReportComment} onReportComment={this.handleReportComment}
onReportSubmit={this.handleReportSubmit} onReportSubmit={this.handleReportSubmit}
onRestoreComment={this.props.isAdmin && this.handleRestoreComment}
onSeeInside={this.handleSeeInside} onSeeInside={this.handleSeeInside}
onShare={this.handleShare} onShare={this.handleShare}
onToggleComments={this.handleToggleComments} onToggleComments={this.handleToggleComments}
@ -448,6 +453,7 @@ Preview.propTypes = {
handleLogOut: PropTypes.func, handleLogOut: PropTypes.func,
handleOpenRegistration: PropTypes.func, handleOpenRegistration: PropTypes.func,
handleReportComment: PropTypes.func, handleReportComment: PropTypes.func,
handleRestoreComment: PropTypes.func,
handleToggleLoginOpen: PropTypes.func, handleToggleLoginOpen: PropTypes.func,
isAdmin: PropTypes.bool, isAdmin: PropTypes.bool,
isEditable: PropTypes.bool, isEditable: PropTypes.bool,
@ -588,6 +594,9 @@ const mapDispatchToProps = dispatch => ({
handleReportComment: (projectId, commentId, topLevelCommentId, token) => { handleReportComment: (projectId, commentId, topLevelCommentId, token) => {
dispatch(previewActions.reportComment(projectId, commentId, topLevelCommentId, token)); dispatch(previewActions.reportComment(projectId, commentId, topLevelCommentId, token));
}, },
handleRestoreComment: (projectId, commentId, topLevelCommentId, token) => {
dispatch(previewActions.restoreComment(projectId, commentId, topLevelCommentId, token));
},
handleOpenRegistration: event => { handleOpenRegistration: event => {
event.preventDefault(); event.preventDefault();
dispatch(navigationActions.setRegistrationOpen(true)); dispatch(navigationActions.setRegistrationOpen(true));