PageComment.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import React, {
  2. FC, useState, useMemo, memo, useCallback,
  3. } from 'react';
  4. import { isPopulated, getIdForRef, type IRevisionHasId } from '@growi/core';
  5. import { Button } from 'reactstrap';
  6. import { apiPost } from '~/client/util/apiv1-client';
  7. import { toastError } from '~/client/util/toastr';
  8. import { RendererOptions } from '~/interfaces/renderer-options';
  9. import { useSWRMUTxPageInfo } from '~/stores/page';
  10. import { useCommentForCurrentPageOptions } from '~/stores/renderer';
  11. import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
  12. import { useSWRxPageComment } from '../stores/comment';
  13. import { NotAvailableForGuest } from './NotAvailableForGuest';
  14. import { NotAvailableForReadOnlyUser } from './NotAvailableForReadOnlyUser';
  15. import { Comment } from './PageComment/Comment';
  16. import { CommentEditor } from './PageComment/CommentEditor';
  17. import { DeleteCommentModal } from './PageComment/DeleteCommentModal';
  18. import { ReplyComments } from './PageComment/ReplyComments';
  19. import styles from './PageComment.module.scss';
  20. export type PageCommentProps = {
  21. rendererOptions?: RendererOptions,
  22. pageId: string,
  23. pagePath: string,
  24. revision: string | IRevisionHasId,
  25. currentUser: any,
  26. isReadOnly: boolean,
  27. }
  28. export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps): JSX.Element => {
  29. const {
  30. rendererOptions: rendererOptionsByProps,
  31. pageId, pagePath, revision, currentUser, isReadOnly,
  32. } = props;
  33. const { data: comments, mutate } = useSWRxPageComment(pageId);
  34. const { data: rendererOptionsForCurrentPage } = useCommentForCurrentPageOptions();
  35. const [commentToBeDeleted, setCommentToBeDeleted] = useState<ICommentHasId | null>(null);
  36. const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState<boolean>(false);
  37. const [showEditorIds, setShowEditorIds] = useState<Set<string>>(new Set());
  38. const [errorMessageOnDelete, setErrorMessageOnDelete] = useState<string>('');
  39. const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageId);
  40. const commentsFromOldest = useMemo(() => (comments != null ? [...comments].reverse() : null), [comments]);
  41. const commentsExceptReply: ICommentHasIdList | undefined = useMemo(
  42. () => commentsFromOldest?.filter(comment => comment.replyTo == null), [commentsFromOldest],
  43. );
  44. const allReplies = {};
  45. if (commentsFromOldest != null) {
  46. commentsFromOldest.forEach((comment) => {
  47. if (comment.replyTo != null) {
  48. allReplies[comment.replyTo] = allReplies[comment.replyTo] == null ? [comment] : [...allReplies[comment.replyTo], comment];
  49. }
  50. });
  51. }
  52. const onClickDeleteButton = useCallback((comment: ICommentHasId) => {
  53. setCommentToBeDeleted(comment);
  54. setIsDeleteConfirmModalShown(true);
  55. }, []);
  56. const onCancelDeleteComment = useCallback(() => {
  57. setCommentToBeDeleted(null);
  58. setIsDeleteConfirmModalShown(false);
  59. }, []);
  60. const onDeleteCommentAfterOperation = useCallback(() => {
  61. onCancelDeleteComment();
  62. mutate();
  63. mutatePageInfo();
  64. }, [mutate, onCancelDeleteComment, mutatePageInfo]);
  65. const onDeleteComment = useCallback(async() => {
  66. if (commentToBeDeleted == null) return;
  67. try {
  68. await apiPost('/comments.remove', { comment_id: commentToBeDeleted._id });
  69. onDeleteCommentAfterOperation();
  70. }
  71. catch (error: unknown) {
  72. setErrorMessageOnDelete(error as string);
  73. toastError(`error: ${error}`);
  74. }
  75. }, [commentToBeDeleted, onDeleteCommentAfterOperation]);
  76. const removeShowEditorId = useCallback((commentId: string) => {
  77. setShowEditorIds((previousState) => {
  78. return new Set([...previousState].filter(id => id !== commentId));
  79. });
  80. }, []);
  81. const onReplyButtonClickHandler = useCallback((commentId: string) => {
  82. setShowEditorIds(previousState => new Set([...previousState, commentId]));
  83. }, []);
  84. const onCommentButtonClickHandler = useCallback((commentId: string) => {
  85. removeShowEditorId(commentId);
  86. mutate();
  87. mutatePageInfo();
  88. }, [removeShowEditorId, mutate, mutatePageInfo]);
  89. if (comments?.length === 0) {
  90. return <></>;
  91. }
  92. const rendererOptions = rendererOptionsByProps ?? rendererOptionsForCurrentPage;
  93. if (commentsFromOldest == null || commentsExceptReply == null || rendererOptions == null) {
  94. return <></>;
  95. }
  96. const revisionId = getIdForRef(revision);
  97. const revisionCreatedAt = (isPopulated(revision)) ? revision.createdAt : undefined;
  98. const commentElement = (comment: ICommentHasId) => (
  99. <Comment
  100. rendererOptions={rendererOptions}
  101. comment={comment}
  102. revisionId={revisionId}
  103. revisionCreatedAt={revisionCreatedAt as Date}
  104. currentUser={currentUser}
  105. isReadOnly={isReadOnly}
  106. pageId={pageId}
  107. pagePath={pagePath}
  108. deleteBtnClicked={onClickDeleteButton}
  109. onComment={mutate}
  110. />
  111. );
  112. const replyCommentsElement = (replyComments: ICommentHasIdList) => (
  113. <ReplyComments
  114. rendererOptions={rendererOptions}
  115. isReadOnly={isReadOnly}
  116. revisionId={revisionId}
  117. revisionCreatedAt={revisionCreatedAt as Date}
  118. currentUser={currentUser}
  119. replyList={replyComments}
  120. pageId={pageId}
  121. pagePath={pagePath}
  122. deleteBtnClicked={onClickDeleteButton}
  123. onComment={mutate}
  124. />
  125. );
  126. return (
  127. <div className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
  128. <div className="page-comments">
  129. <div className="page-comments-list" id="page-comments-list">
  130. {commentsExceptReply.map((comment) => {
  131. const defaultCommentThreadClasses = 'page-comment-thread pb-5';
  132. const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
  133. let commentThreadClasses = '';
  134. commentThreadClasses = hasReply ? `${defaultCommentThreadClasses} page-comment-thread-no-replies` : defaultCommentThreadClasses;
  135. return (
  136. <div key={comment._id} className={commentThreadClasses}>
  137. {commentElement(comment)}
  138. {hasReply && replyCommentsElement(allReplies[comment._id])}
  139. {(!isReadOnly && !showEditorIds.has(comment._id)) && (
  140. <div className="d-flex flex-row-reverse">
  141. <NotAvailableForGuest>
  142. <NotAvailableForReadOnlyUser>
  143. <Button
  144. data-testid="comment-reply-button"
  145. outline
  146. color="secondary"
  147. size="sm"
  148. className="btn-comment-reply"
  149. onClick={() => onReplyButtonClickHandler(comment._id)}
  150. >
  151. <span className="material-symbols-outlined">replay</span> Reply
  152. </Button>
  153. </NotAvailableForReadOnlyUser>
  154. </NotAvailableForGuest>
  155. </div>
  156. )}
  157. {(!isReadOnly && showEditorIds.has(comment._id)) && (
  158. <CommentEditor
  159. pageId={pageId}
  160. replyTo={comment._id}
  161. onCancelButtonClicked={() => {
  162. removeShowEditorId(comment._id);
  163. }}
  164. onCommentButtonClicked={() => onCommentButtonClickHandler(comment._id)}
  165. revisionId={revisionId}
  166. />
  167. )}
  168. </div>
  169. );
  170. })}
  171. </div>
  172. </div>
  173. {!isReadOnly && (
  174. <DeleteCommentModal
  175. isShown={isDeleteConfirmModalShown}
  176. comment={commentToBeDeleted}
  177. errorMessage={errorMessageOnDelete}
  178. cancelToDelete={onCancelDeleteComment}
  179. confirmToDelete={onDeleteComment}
  180. />
  181. )}
  182. </div>
  183. );
  184. });
  185. PageComment.displayName = 'PageComment';