Przeglądaj źródła

commit codes temporarily to share PageComment.jsx and PageCommentList.tsx

yuto-oweseek 4 lat temu
rodzic
commit
2b2642120d

+ 2 - 2
packages/app/src/client/app.jsx

@@ -120,8 +120,8 @@ Object.assign(componentMappings, {
 // additional definitions if data exists
 if (pageContainer.state.pageId != null) {
   Object.assign(componentMappings, {
-    'page-comments-list': <PageCommentList appContainer={appContainer} pageId={pageContainer.state.pageId} />,
-    'page-comment-write': <CommentEditorLazyRenderer appContainer={appContainer} />,
+    'page-comments-list': <PageCommentList appContainer={appContainer} pageId={pageContainer.state.pageId} isReadOnly={false} titleAlign="left" />,
+    'page-comment-write': <CommentEditorLazyRenderer appContainer={appContainer} pageId={pageContainer.state.pageId} />,
     'page-content-footer': <PageContentFooter
       createdAt={new Date(pageContainer.state.createdAt)}
       updatedAt={new Date(pageContainer.state.updatedAt)}

+ 9 - 5
packages/app/src/components/PageComment/Comment.jsx

@@ -147,8 +147,7 @@ class Comment extends React.PureComponent {
   }
 
   render() {
-    const { t } = this.props;
-    const comment = this.props.comment;
+    const { t, comment, isReadOnly } = this.props;
     const commentId = comment._id;
     const creator = comment.creator;
     const isMarkdown = comment.isMarkdown;
@@ -167,7 +166,7 @@ class Comment extends React.PureComponent {
 
     return (
       <React.Fragment>
-        {this.state.isReEdit ? (
+        {(this.state.isReEdit && !isReadOnly) ? (
           <CommentEditor
             growiRenderer={this.props.growiRenderer}
             currentCommentId={commentId}
@@ -175,7 +174,10 @@ class Comment extends React.PureComponent {
             replyTo={undefined}
             commentCreator={creator?.username}
             onCancelButtonClicked={() => this.setState({ isReEdit: false })}
-            onCommentButtonClicked={() => this.setState({ isReEdit: false })}
+            onCommentButtonClicked={() => {
+              this.setState({ isReEdit: false });
+              if (this.props.onUpdate != null) this.props.onUpdate();
+            }}
           />
         ) : (
           <div id={commentId} className={rootClassName}>
@@ -206,7 +208,7 @@ class Comment extends React.PureComponent {
                   </UncontrolledTooltip>
                 </span>
               </div>
-              {this.isCurrentUserEqualsToAuthor() && (
+              {(this.isCurrentUserEqualsToAuthor() && !isReadOnly) && (
                 <CommentControl
                   onClickDeleteBtn={this.deleteBtnClickedHandler}
                   onClickEditBtn={() => this.setState({ isReEdit: true })}
@@ -233,8 +235,10 @@ Comment.propTypes = {
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
   comment: PropTypes.object.isRequired,
+  isReadOnly: PropTypes.bool.isRequired,
   growiRenderer: PropTypes.object.isRequired,
   deleteBtnClicked: PropTypes.func.isRequired,
+  onUpdate: PropTypes.func,
 };
 
 export default withTranslation()(CommentWrapper);

+ 1 - 1
packages/app/src/components/PageComment/CommentEditor.jsx

@@ -171,7 +171,7 @@ class CommentEditor extends React.Component {
       this.initializeEditor();
 
       if (onCommentButtonClicked != null) {
-        onCommentButtonClicked(replyTo || currentCommentId);
+        onCommentButtonClicked();
       }
     }
     catch (err) {

+ 7 - 0
packages/app/src/components/PageComment/CommentEditorLazyRenderer.tsx

@@ -1,15 +1,21 @@
 import React, { FC } from 'react';
 
+import { useSWRxPageComment } from '../../stores/comment';
+
 import AppContainer from '~/client/services/AppContainer';
 
 import CommentEditor from './CommentEditor';
 
 type Props = {
   appContainer: AppContainer,
+  pageId: string,
 }
 
 const CommentEditorLazyRenderer:FC<Props> = (props:Props):JSX.Element => {
 
+  const { pageId } = props;
+  const { mutate } = useSWRxPageComment(pageId);
+
   const { appContainer } = props;
   const growiRenderer = appContainer.getRenderer('comment');
 
@@ -18,6 +24,7 @@ const CommentEditorLazyRenderer:FC<Props> = (props:Props):JSX.Element => {
       appContainer={appContainer}
       growiRenderer={growiRenderer}
       replyTo={undefined}
+      onCommentButtonClicked={mutate}
       isForNewComment
     />
   );

+ 2 - 0
packages/app/src/components/PageComment/ReplayComments.jsx

@@ -33,6 +33,7 @@ class ReplayComments extends React.PureComponent {
           comment={reply}
           deleteBtnClicked={this.props.deleteBtnClicked}
           growiRenderer={this.props.growiRenderer}
+          isReadOnly={this.props.isReadOnly}
         />
       </div>
     );
@@ -108,6 +109,7 @@ ReplayComments.propTypes = {
 
   growiRenderer: PropTypes.object.isRequired,
   deleteBtnClicked: PropTypes.func.isRequired,
+  isReadOnly: PropTypes.bool.isRequired,
   replyList: PropTypes.array,
 };
 

+ 154 - 151
packages/app/src/components/PageCommentList.tsx

@@ -2,38 +2,44 @@ import React, {
   FC, useEffect, useState, useMemo, memo, useCallback,
 } from 'react';
 
-import { UncontrolledTooltip } from 'reactstrap';
-import { useTranslation } from 'react-i18next';
+import { Button } from 'reactstrap';
 
-import { UserPicture } from '@growi/ui';
-import AppContainer from '~/client/services/AppContainer';
-
-import RevisionBody from './Page/RevisionBody';
-import Username from './User/Username';
-import FormattedDistanceDate from './FormattedDistanceDate';
-import HistoryIcon from './Icons/HistoryIcon';
+import CommentEditor from './PageComment/CommentEditor';
+import CommentAny from './PageComment/Comment';
+import ReplayComments from './PageComment/ReplayComments';
+import DeleteCommentModal from './PageComment/DeleteCommentModal';
 
-import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
+import AppContainer from '~/client/services/AppContainer';
+import { toastError } from '~/client/util/apiNotification';
 
 import { useSWRxPageComment } from '../stores/comment';
 
-import MathJaxConfigurer from '~/client/util/markdown-it/mathjax';
+import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
 
-const COMMENT_BOTTOM_MARGIN = 'mb-5';
+// todo: Comment component will be updated to typescript
+// the below any is workaround to avoid WithTranslation IntrinsicAttributes & Omit error
+const Comment = CommentAny as any;
 
 type Props = {
   appContainer: AppContainer,
   pageId: string,
+  isReadOnly : boolean,
+  titleAlign?: 'center' | 'left' | 'right',
   highlightKeywords?:string[],
 }
 
-// todo: update this component to shared PageComment component
+
 const PageCommentList:FC<Props> = memo((props:Props):JSX.Element => {
 
-  const { appContainer, pageId, highlightKeywords } = props;
+  const {
+    appContainer, pageId, highlightKeywords, isReadOnly, titleAlign,
+  } = props;
 
-  const { t } = useTranslation();
   const { data: comments, mutate } = useSWRxPageComment(pageId);
+
+  const [commentToBeDeleted, setCommentToBeDeleted] = useState<null | ICommentHasId>(null);
+  const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState<boolean>(false);
+  const [showEditorIds, setShowEditorIds] = useState<Set<string>>(new Set());
   const [formatedComments, setFormatedComments] = useState<ICommentHasIdList | null>(null);
 
   const commentsFromOldest = useMemo(() => (formatedComments != null ? [...formatedComments].reverse() : null), [formatedComments]);
@@ -42,34 +48,6 @@ const PageCommentList:FC<Props> = memo((props:Props):JSX.Element => {
   );
   const allReplies = {};
 
-  /**
-   * preprocess:
-   * parse, sanitize, convert markdown to html
-   */
-  const preprocessComment = useCallback(async(comment:string):Promise<string> => {
-
-    const { interceptorManager } = appContainer;
-    const growiRenderer = appContainer.getRenderer('comment');
-
-    const context: {markdown: string, parsedHTML: string} = { markdown: comment, parsedHTML: '' };
-
-    if (interceptorManager != null) {
-      await interceptorManager.process('preRenderComment', context);
-      await interceptorManager.process('prePreProcess', context);
-      context.markdown = await growiRenderer.preProcess(context.markdown);
-      await interceptorManager.process('postPreProcess', context);
-      context.parsedHTML = await growiRenderer.process(context.markdown);
-      await interceptorManager.process('prePostProcess', context);
-      context.parsedHTML = await growiRenderer.postProcess(context.parsedHTML);
-      await interceptorManager.process('postPostProcess', context);
-      await interceptorManager.process('preRenderCommentHtml', context);
-      await interceptorManager.process('postRenderCommentHtml', context);
-    }
-
-    return context.parsedHTML;
-
-  }, [appContainer]);
-
   const highlightComment = useCallback((comment: string):string => {
     if (highlightKeywords == null) return comment;
 
@@ -83,24 +61,21 @@ const PageCommentList:FC<Props> = memo((props:Props):JSX.Element => {
   useEffect(() => { mutate() }, [pageId, mutate]);
 
   useEffect(() => {
-    const formatAndHighlightComments = async() => {
-
-      if (comments != null) {
-        const preprocessedCommentList: string[] = await Promise.all(comments.map((comment) => {
-          const highlightedComment: string = highlightComment(comment.comment);
-          return preprocessComment(highlightedComment);
-        }));
-        const preprocessedComments: ICommentHasIdList = comments.map((comment, index) => {
-          return { ...comment, comment: preprocessedCommentList[index] };
-        });
-        setFormatedComments(preprocessedComments);
-      }
 
-    };
+    if (comments != null) {
+      const preprocessedCommentList: string[] = comments.map((comment) => {
+        const highlightedComment: string = highlightComment(comment.comment);
+
+        return highlightedComment;
+      });
+      const preprocessedComments: ICommentHasIdList = comments.map((comment, index) => {
+        return { ...comment, comment: preprocessedCommentList[index] };
+      });
+      setFormatedComments(preprocessedComments);
+    }
 
-    formatAndHighlightComments();
 
-  }, [comments, highlightComment, preprocessComment]);
+  }, [comments, highlightComment]);
 
   if (commentsFromOldest != null) {
     commentsFromOldest.forEach((comment) => {
@@ -110,107 +85,135 @@ const PageCommentList:FC<Props> = memo((props:Props):JSX.Element => {
     });
   }
 
-  const generateMarkdownBody = (comment: string): JSX.Element => {
-    const isMathJaxEnabled: boolean = (new MathJaxConfigurer(appContainer)).isEnabled;
-    return (
-      <RevisionBody
-        html={comment}
-        isMathJaxEnabled={isMathJaxEnabled}
-        renderMathJaxOnInit
-        additionalClassName="comment"
-      />
-    );
-  };
-
-  const generateBodyFromPlainText = (comment: string): JSX.Element => {
-    return <span style={{ whiteSpace: 'pre-wrap' }}>{comment}</span>;
-  };
-
-  const generateCommentInnerElement = (comment: ICommentHasId) => {
-    const revisionHref = `/${comment.page}?revision=${comment.revision}`;
-    const commentBody: string = comment.comment;
-    const formatedCommentBody = comment.isMarkdown ? generateMarkdownBody(commentBody) : generateBodyFromPlainText(commentBody);
-
-    return (
-      <div key={comment._id} className="page-comment flex-column">
-        <div className="page-comment-writer">
-          <UserPicture user={comment.creator} />
-        </div>
-        <div className="page-comment-main">
-          <div className="page-comment-creator">
-            <Username user={comment.creator} />
-          </div>
-          <div className="page-comment-body">
-            {formatedCommentBody}
-          </div>
-          <div className="page-comment-meta">
-            <a href={`/${comment.page}#${comment._id}`}>
-              <FormattedDistanceDate id={comment._id} date={comment.createdAt} />
-            </a>
-            <span className="ml-2">
-              <a id={`page-comment-revision-${comment._id}`} className="page-comment-revision" href={revisionHref}>
-                <HistoryIcon />
-              </a>
-              <UncontrolledTooltip placement="bottom" fade={false} target={`page-comment-revision-${comment._id}`}>
-                {t('page_comment.display_the_page_when_posting_this_comment')}
-              </UncontrolledTooltip>
-            </span>
-          </div>
-        </div>
-      </div>
-    );
-  };
-
-  const generateAllRepliesElement = (replyComments: ICommentHasIdList) => {
-    return (
-      replyComments.map((comment: ICommentHasId, index: number) => {
-        const lastIndex: number = replyComments.length - 1;
-        const isLastIndex: boolean = index === lastIndex;
-        const defaultReplyClasses = 'page-comment-reply ml-4 ml-sm-5 mr-3';
-        const replyClasses: string = isLastIndex ? `${defaultReplyClasses} ${COMMENT_BOTTOM_MARGIN}` : defaultReplyClasses;
-
-        return (
-          <div key={comment._id} className={replyClasses}>
-            {generateCommentInnerElement(comment)}
-          </div>
-        );
+  const onClickDeleteButton = useCallback((comment: ICommentHasId) => {
+    setCommentToBeDeleted(comment);
+    setIsDeleteConfirmModalShown(true);
+  }, []);
+
+  const onCancelDeleteComment = useCallback(() => {
+    setCommentToBeDeleted(null);
+    setIsDeleteConfirmModalShown(false);
+  }, []);
+
+  const onDeleteCommentAfterOperation = useCallback(() => {
+    onCancelDeleteComment();
+    mutate();
+  }, [mutate, onCancelDeleteComment]);
+
+  const onDeleteComment = useCallback(async() => {
+    if (commentToBeDeleted == null) return;
+    try {
+      await appContainer.apiPost('/comments.remove', { comment_id: commentToBeDeleted._id });
+      onDeleteCommentAfterOperation();
+    }
+    catch (error:unknown) {
+      toastError(`error: ${error}`);
+    }
+  }, [appContainer, commentToBeDeleted, onDeleteCommentAfterOperation]);
 
-      })
-    );
-  };
+
+  const generateCommentInnerElement = (comment: ICommentHasId) => (
+    <Comment
+      growiRenderer={appContainer.getRenderer('comment')}
+      deleteBtnClicked={onClickDeleteButton}
+      comment={comment}
+      onUpdate={mutate}
+      isReadOnly={isReadOnly}
+    />
+  );
+
+  const generateAllRepliesElement = (replyComments: ICommentHasIdList) => (
+    <ReplayComments
+      replyList={replyComments}
+      deleteBtnClicked={onClickDeleteButton}
+      growiRenderer={appContainer.getRenderer('comment')}
+      isReadOnly={isReadOnly}
+    />
+  );
 
 
   if (commentsFromOldest == null || commentsExceptReply == null) return <></>;
 
+  let commentTitleClasses = 'border-bottom py-3 mb-3';
+  commentTitleClasses = titleAlign != null ? `${commentTitleClasses} text-${titleAlign}` : `${commentTitleClasses} text-center`;
+
   return (
-    <div className="page-comments-row comment-list border border-top mt-5 px-2">
-      <div className="page-comments">
-        <h2 className="text-center border-bottom my-4 pb-2"><i className="icon-fw icon-bubbles"></i>Comments</h2>
-        <div className="page-comments-list" id="page-comments-list">
-          { commentsExceptReply.map((comment, index) => {
-
-            const defaultCommentThreadClasses = 'page-comment-thread';
-            const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
-            const isLastComment: boolean = index === commentsExceptReply.length - 1;
-
-            let commentThreadClasses = '';
-            commentThreadClasses = hasReply ? `${defaultCommentThreadClasses} page-comment-thread-no-replies` : defaultCommentThreadClasses;
-            commentThreadClasses = isLastComment ? `${commentThreadClasses} ${COMMENT_BOTTOM_MARGIN}` : commentThreadClasses;
-
-            return (
-              <div key={comment._id} className={commentThreadClasses}>
-                {/* display comment */}
-                {generateCommentInnerElement(comment)}
-                {/* display reply comment */}
-                {hasReply && generateAllRepliesElement(allReplies[comment._id])}
-              </div>
-            );
-
-          })}
+    <>
+      <div className="page-comments-row comment-list">
+        <div className="container-lg">
+          <div className="page-comments">
+            <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>
+            <div className="page-comments-list" id="page-comments-list">
+              { commentsExceptReply.map((comment) => {
+
+                const defaultCommentThreadClasses = 'page-comment-thread pb-5';
+                const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
+
+                let commentThreadClasses = '';
+                commentThreadClasses = hasReply ? `${defaultCommentThreadClasses} page-comment-thread-no-replies` : defaultCommentThreadClasses;
+                return (
+                  <div key={comment._id} className={commentThreadClasses}>
+                    {/* display comment */}
+                    {generateCommentInnerElement(comment)}
+                    {/* display reply comment */}
+                    {hasReply && generateAllRepliesElement(allReplies[comment._id])}
+                    {/* display reply button */}
+                    {(!isReadOnly && !showEditorIds.has(comment._id)) && (
+                      <div className="text-right">
+                        <Button
+                          outline
+                          color="secondary"
+                          size="sm"
+                          className="btn-comment-reply"
+                          onClick={() => {
+                            setShowEditorIds(previousState => new Set(previousState.add(comment._id)));
+                          }}
+                        >
+                          <i className="icon-fw icon-action-undo"></i> Reply
+                        </Button>
+                      </div>
+                    )}
+                    {/* display reply editor */}
+                    {(!isReadOnly && showEditorIds.has(comment._id)) && (
+                      <CommentEditor
+                        growiRenderer={appContainer.getRenderer('comment')}
+                        replyTo={comment._id}
+                        onCancelButtonClicked={() => {
+                          setShowEditorIds((previousState) => {
+                            const tmp = new Set(...previousState);
+                            tmp.delete(comment._id);
+                            return tmp;
+                          });
+                        }}
+                        onCommentButtonClicked={() => {
+                          setShowEditorIds((previousState) => {
+                            const tmp = new Set(...previousState);
+                            tmp.delete(comment._id);
+                            return tmp;
+                          });
+                          mutate();
+
+                        }}
+                      />
+                    )}
+                  </div>
+                );
+
+              })}
+            </div>
+          </div>
         </div>
       </div>
-
-    </div>
+      {(!isReadOnly && commentToBeDeleted != null) && (
+        <DeleteCommentModal
+          isShown={isDeleteConfirmModalShown}
+          comment={commentToBeDeleted}
+          errorMessage=""
+          cancel={onCancelDeleteComment}
+          confirmedToDelete={onDeleteComment}
+        />
+      )}
+    </>
   );
 });
 

+ 1 - 1
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -218,7 +218,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
           revisionId={page.revision}
           highlightKeywords={highlightKeywords}
         />
-        <PageCommentList appContainer={appContainer} pageId={page._id} highlightKeywords={highlightKeywords} />
+        <PageCommentList appContainer={appContainer} pageId={page._id} isReadOnly highlightKeywords={highlightKeywords} />
         <PageContentFooter
           createdAt={new Date(pageWithMeta.data.createdAt)}
           updatedAt={new Date(pageWithMeta.data.updatedAt)}

+ 0 - 3
packages/app/src/server/views/layout-growi/widget/comments.html

@@ -2,9 +2,6 @@
   <div class="container-lg">
 
     <div class="page-comments">
-
-      <h2 class="border-bottom pb-2 mb-3"><i class="icon-fw icon-bubbles"></i> Comments</h2>
-
       <div class="page-comments-list" id="page-comments-list"></div>
 
       {% if page and not page.isDeleted() %}