Преглед изворни кода

Merge pull request #1192 from weseek/Re-edit-comment-for-master-merge

Re edit comment for master merge
Yuki Takei пре 6 година
родитељ
комит
1ad5b697c8

+ 64 - 19
src/client/js/components/PageComment/Comment.jsx

@@ -15,6 +15,7 @@ import { createSubscribedElement } from '../UnstatedUtils';
 import RevisionBody from '../Page/RevisionBody';
 import UserPicture from '../User/UserPicture';
 import Username from '../User/Username';
+import CommentEditor from './CommentEditor';
 
 /**
  *
@@ -32,8 +33,11 @@ class Comment extends React.Component {
     this.state = {
       html: '',
       isOlderRepliesShown: false,
+      showReEditorIds: new Set(),
     };
 
+    this.growiRenderer = this.props.appContainer.getRenderer('comment');
+
     this.isCurrentUserIsAuthor = this.isCurrentUserEqualsToAuthor.bind(this);
     this.isCurrentRevision = this.isCurrentRevision.bind(this);
     this.getRootClassName = this.getRootClassName.bind(this);
@@ -41,6 +45,7 @@ class Comment extends React.Component {
     this.deleteBtnClickedHandler = this.deleteBtnClickedHandler.bind(this);
     this.renderText = this.renderText.bind(this);
     this.renderHtml = this.renderHtml.bind(this);
+    this.commentButtonClickedHandler = this.commentButtonClickedHandler.bind(this);
   }
 
   componentWillMount() {
@@ -56,6 +61,10 @@ class Comment extends React.Component {
     this.renderHtml(markdown);
   }
 
+  checkPermissionToControlComment() {
+    return this.props.appContainer.isAdmin || this.isCurrentUserEqualsToAuthor();
+  }
+
   isCurrentUserEqualsToAuthor() {
     return this.props.comment.creator.username === this.props.appContainer.me;
   }
@@ -90,6 +99,20 @@ class Comment extends React.Component {
       this.isCurrentRevision() ? 'label-primary' : 'label-default'}`;
   }
 
+  editBtnClickedHandler(commentId) {
+    const ids = this.state.showReEditorIds.add(commentId);
+    this.setState({ showReEditorIds: ids });
+  }
+
+  commentButtonClickedHandler(commentId) {
+    this.setState((prevState) => {
+      prevState.showReEditorIds.delete(commentId);
+      return {
+        showReEditorIds: prevState.showReEditorIds,
+      };
+    });
+  }
+
   deleteBtnClickedHandler() {
     this.props.deleteBtnClicked(this.props.comment);
   }
@@ -214,12 +237,28 @@ class Comment extends React.Component {
     );
   }
 
+  renderCommentControl(comment) {
+    return (
+      <div className="page-comment-control">
+        <button type="button" className="btn btn-link p-2" onClick={() => { this.editBtnClickedHandler(comment._id) }}>
+          <i className="ti-pencil"></i>
+        </button>
+        <button type="button" className="btn btn-link p-2 mr-2" onClick={this.deleteBtnClickedHandler}>
+          <i className="ti-close"></i>
+        </button>
+      </div>
+    );
+  }
+
   render() {
     const comment = this.props.comment;
+    const commentId = comment._id;
     const creator = comment.creator;
     const isMarkdown = comment.isMarkdown;
     const createdAt = new Date(comment.createdAt);
 
+    const showReEditor = this.state.showReEditorIds.has(commentId);
+
     const rootClassName = this.getRootClassName(comment);
     const commentDate = formatDistanceStrict(createdAt, new Date());
     const commentBody = isMarkdown ? this.renderRevisionBody() : this.renderText(comment.comment);
@@ -236,27 +275,33 @@ class Comment extends React.Component {
     return (
       <React.Fragment>
 
-        <div className={rootClassName}>
-          <UserPicture user={creator} />
-          <div className="page-comment-main">
-            <div className="page-comment-creator">
-              <Username user={creator} />
-            </div>
-            <div className="page-comment-body">{commentBody}</div>
-            <div className="page-comment-meta">
-              <OverlayTrigger overlay={commentDateTooltip} placement="bottom">
-                <span>{commentDate}</span>
-              </OverlayTrigger>
-              <span className="ml-2"><a className={revisionLavelClassName} href={revHref}>{revFirst8Letters}</a></span>
-            </div>
-            <div className="page-comment-control">
-              <button type="button" className="btn btn-link" onClick={this.deleteBtnClickedHandler}>
-                <i className="ti-close"></i>
-              </button>
+        {showReEditor ? (
+          <CommentEditor
+            growiRenderer={this.growiRenderer}
+            currentCommentId={commentId}
+            commentBody={comment.comment}
+            replyTo={undefined}
+            commentButtonClickedHandler={this.commentButtonClickedHandler}
+          />
+        ) : (
+          <div className={rootClassName}>
+            <UserPicture user={creator} />
+            <div className="page-comment-main">
+              <div className="page-comment-creator">
+                <Username user={creator} />
+              </div>
+              <div className="page-comment-body">{commentBody}</div>
+              <div className="page-comment-meta">
+                <OverlayTrigger overlay={commentDateTooltip} placement="bottom">
+                  <span>{commentDate}</span>
+                </OverlayTrigger>
+                <span className="ml-2"><a className={revisionLavelClassName} href={revHref}>{revFirst8Letters}</a></span>
+              </div>
+              { this.checkPermissionToControlComment() && this.renderCommentControl(comment) }
             </div>
           </div>
-        </div>
-
+        )
+      }
         {this.renderReplies()}
 
       </React.Fragment>

+ 42 - 28
src/client/js/components/PageComment/CommentEditor.jsx

@@ -36,7 +36,7 @@ class CommentEditor extends React.Component {
     const isUploadableFile = config.upload.file;
 
     this.state = {
-      comment: '',
+      comment: this.props.commentBody || '',
       isMarkdown: true,
       html: '',
       key: 1,
@@ -84,42 +84,54 @@ class CommentEditor extends React.Component {
   }
 
   toggleEditor() {
-    this.props.commentButtonClickedHandler(this.props.replyTo);
+    const targetId = this.props.replyTo || this.props.currentCommentId;
+    this.props.commentButtonClickedHandler(targetId);
+  }
+
+  initializeEditor() {
+    this.setState({
+      comment: '',
+      isMarkdown: true,
+      html: '',
+      key: 1,
+      errorMessage: undefined,
+    });
+    // reset value
+    this.editor.setValue('');
+    this.toggleEditor();
   }
 
   /**
    * Post comment with CommentContainer and update state
    */
-  postHandler(event) {
+  async postHandler(event) {
     if (event != null) {
       event.preventDefault();
     }
 
-    const { commentContainer } = this.props;
-
-    this.props.commentContainer.postComment(
-      this.state.comment,
-      this.state.isMarkdown,
-      this.props.replyTo,
-      commentContainer.state.isSlackEnabled,
-      commentContainer.state.slackChannels,
-    )
-      .then((res) => {
-        this.setState({
-          comment: '',
-          isMarkdown: true,
-          html: '',
-          key: 1,
-          errorMessage: undefined,
-        });
-        // reset value
-        this.editor.setValue('');
-        this.toggleEditor();
-      })
-      .catch((err) => {
-        const errorMessage = err.message || 'An unknown error occured when posting comment';
-        this.setState({ errorMessage });
-      });
+    try {
+      if (this.props.currentCommentId != null) {
+        await this.props.commentContainer.putComment(
+          this.state.comment,
+          this.state.isMarkdown,
+          this.props.currentCommentId,
+        );
+      }
+      else {
+        await this.props.commentContainer.postComment(
+          this.state.comment,
+          this.state.isMarkdown,
+          this.props.replyTo,
+          this.props.commentContainer.state.isSlackEnabled,
+          this.props.commentContainer.state.slackChannels,
+        );
+      }
+      this.initializeEditor();
+    }
+    catch (err) {
+      const errorMessage = err.message || 'An unknown error occured when posting comment';
+      this.setState({ errorMessage });
+    }
   }
 
   uploadHandler(file) {
@@ -321,6 +333,8 @@ CommentEditor.propTypes = {
 
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
   replyTo: PropTypes.string,
+  currentCommentId: PropTypes.string,
+  commentBody: PropTypes.string,
   commentButtonClickedHandler: PropTypes.func.isRequired,
 };
 

+ 1 - 0
src/client/js/components/PageComments.jsx

@@ -138,6 +138,7 @@ class PageComments extends React.Component {
       <div key={commentId} className={`mb-5 ${rootClassNames}`}>
         <Comment
           comment={comment}
+          editBtnClicked={this.confirmToEditComment}
           deleteBtnClicked={this.confirmToDeleteComment}
           growiRenderer={this.growiRenderer}
           replyList={replies}

+ 23 - 0
src/client/js/services/CommentContainer.js

@@ -100,6 +100,29 @@ export default class CommentContainer extends Container {
       });
   }
 
+  /**
+   * Load data of comments and rerender <PageComments />
+   */
+  putComment(comment, isMarkdown, commentId) {
+    const { pageId, revisionId } = this.getPageContainer().state;
+
+    return this.appContainer.apiPost('/comments.update', {
+      commentForm: {
+        comment,
+        page_id: pageId,
+        revision_id: revisionId,
+        is_markdown: isMarkdown,
+        comment_id: commentId,
+        author: this.appContainer.me,
+      },
+    })
+      .then((res) => {
+        if (res.ok) {
+          return this.retrieveComments();
+        }
+      });
+  }
+
   deleteComment(comment) {
     return this.appContainer.apiPost('/comments.remove', { comment_id: comment._id })
       .then((res) => {

+ 10 - 0
src/server/models/comment.js

@@ -64,6 +64,16 @@ module.exports = function(crowi) {
     }));
   };
 
+  commentSchema.statics.updateCommentsByPageId = function(comment, isMarkdown, commentId) {
+    const Comment = this;
+
+    return Comment.findOneAndUpdate(
+      { _id: commentId },
+      { $set: { comment, isMarkdown } },
+    );
+
+  };
+
   commentSchema.statics.removeCommentsByPageId = function(pageId) {
     const Comment = this;
 

+ 53 - 0
src/server/routes/comment.js

@@ -159,6 +159,59 @@ module.exports = function(crowi, app) {
     }
   };
 
+  /**
+   * @api {post} /comments.update Update comment dody
+   * @apiName UpdateComment
+   * @apiGroup Comment
+   */
+  api.update = async function(req, res) {
+    const { commentForm } = req.body;
+
+    const pageId = commentForm.page_id;
+    const revisionId = commentForm.revision_id;
+    const comment = commentForm.comment;
+    const isMarkdown = commentForm.is_markdown;
+    const commentId = commentForm.comment_id;
+    const author = commentForm.author;
+
+    if (comment === '') {
+      return res.json(ApiResponse.error('Comment text is required'));
+    }
+
+    if (commentId == null) {
+      return res.json(ApiResponse.error('\'comment_id\' is undefined'));
+    }
+
+    if (author !== req.user.username) {
+      return res.json(ApiResponse.error('Only the author can edit'));
+    }
+
+    // check whether accessible
+    const isAccessible = await Page.isAccessiblePageByViewer(pageId, req.user._id, revisionId, comment, isMarkdown, req.user);
+    if (!isAccessible) {
+      return res.json(ApiResponse.error('Current user is not accessible to this page.'));
+    }
+
+    try {
+      const updatedComment = await Comment.updateCommentsByPageId(comment, isMarkdown, commentId);
+
+      const page = await Page.findOneAndUpdate({ _id: pageId }, {
+        lastUpdateUser: req.user,
+        updatedAt: new Date(),
+      });
+
+      res.json(ApiResponse.success({ comment: updatedComment }));
+
+      const path = page.path;
+
+      // global notification
+      globalNotificationService.notifyComment(updatedComment, path);
+    }
+    catch (err) {
+      return res.json(ApiResponse.error(err));
+    }
+  };
+
   /**
    * @api {post} /comments.remove Remove specified comment
    * @apiName RemoveComment

+ 1 - 0
src/server/routes/index.js

@@ -215,6 +215,7 @@ module.exports = function(crowi, app) {
   app.post('/_api/tags.update'        , accessTokenParser, loginRequired(false), tag.api.update);
   app.get('/_api/comments.get'        , accessTokenParser , loginRequired(false) , comment.api.get);
   app.post('/_api/comments.add'       , comment.api.validators.add(), accessTokenParser , loginRequired() , csrf, comment.api.add);
+  app.post('/_api/comments.update'       , comment.api.validators.add(), accessTokenParser , loginRequired() , csrf, comment.api.update);
   app.post('/_api/comments.remove'    , accessTokenParser , loginRequired() , csrf, comment.api.remove);
   app.get('/_api/bookmarks.get'       , accessTokenParser , loginRequired(false) , bookmark.api.get);
   app.post('/_api/bookmarks.add'      , accessTokenParser , loginRequired() , csrf, bookmark.api.add);