]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16538 Adding new comment list ui for issue comment popup
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Tue, 2 Aug 2022 12:55:42 +0000 (14:55 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 3 Aug 2022 20:03:24 +0000 (20:03 +0000)
14 files changed:
server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
server/sonar-web/src/main/js/components/issue/Issue.css
server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx
server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.tsx
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueActionsBar-test.tsx
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.tsx
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueActionsBar-test.tsx.snap
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.tsx.snap
server/sonar-web/src/main/js/components/issue/popups/CommentList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx
server/sonar-web/src/main/js/components/issue/popups/CommentTile.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.tsx

index e42cdf6eef8e05bb5d632c97b9c0abb3eb9fc06d..f40a56052a6d762035dcde9c8884f1ce3e758546 100644 (file)
@@ -52,6 +52,9 @@ import {
 import { NoticeType } from '../../types/users';
 import { getComponentForSourceViewer, getSources } from '../components';
 import {
+  addIssueComment,
+  deleteIssueComment,
+  editIssueComment,
   getIssueFlowSnippets,
   searchIssues,
   searchIssueTags,
@@ -224,6 +227,9 @@ export default class IssuesServiceMock {
     (setIssueSeverity as jest.Mock).mockImplementation(this.handleSetIssueSeverity);
     (setIssueTransition as jest.Mock).mockImplementation(this.handleSetIssueTransition);
     (setIssueTags as jest.Mock).mockImplementation(this.handleSetIssueTags);
+    (addIssueComment as jest.Mock).mockImplementation(this.handleAddComment);
+    (editIssueComment as jest.Mock).mockImplementation(this.handleEditComment);
+    (deleteIssueComment as jest.Mock).mockImplementation(this.handleDeleteComment);
     (searchUsers as jest.Mock).mockImplementation(this.handleSearchUsers);
     (searchIssueTags as jest.Mock).mockImplementation(this.handleSearchIssueTags);
   }
@@ -387,6 +393,54 @@ export default class IssuesServiceMock {
     return this.getActionsResponse({ tags: tagsArr }, data.issue);
   };
 
+  handleAddComment = (data: { issue: string; text: string }) => {
+    // For comment its little more complex to get comment Id
+    return this.getActionsResponse(
+      {
+        comments: [
+          {
+            createdAt: '2022-07-28T11:30:04+0200',
+            htmlText: data.text,
+            key: '1234',
+            login: 'admin',
+            markdown: data.text,
+            updatable: true
+          }
+        ]
+      },
+      data.issue
+    );
+  };
+
+  handleEditComment = (data: { comment: string; text: string }) => {
+    // For comment its little more complex to get comment Id
+    return this.getActionsResponse(
+      {
+        comments: [
+          {
+            createdAt: '2022-07-28T11:30:04+0200',
+            htmlText: data.text,
+            key: '1234',
+            login: 'admin',
+            markdown: data.text,
+            updatable: true
+          }
+        ]
+      },
+      'issue2'
+    );
+  };
+
+  handleDeleteComment = () => {
+    // For comment its little more complex to get comment Id
+    return this.getActionsResponse(
+      {
+        comments: []
+      },
+      'issue2'
+    );
+  };
+
   handleSearchUsers = () => {
     return this.reply({ users: [mockLoggedInUser()] });
   };
index 204bb4be7ffb2e63ea4b0e4bb6da2263bf2b2bea..12eba71e38f1e6dfa735e015081e0234411d80cc 100644 (file)
@@ -254,6 +254,47 @@ it('should be able to perform action on issues', async () => {
   await user.keyboard('{ArrowUp}{enter}');
   expect(screen.getByText('luke')).toBeInTheDocument();
 
+  // adding comment to the issue
+  expect(
+    screen.getByRole('button', {
+      name: `issue.comment.add_comment`
+    })
+  ).toBeInTheDocument();
+
+  await user.click(
+    screen.getByRole('button', {
+      name: `issue.comment.add_comment`
+    })
+  );
+  expect(screen.getByText('issue.comment.submit')).toBeInTheDocument();
+  await user.keyboard('comment');
+  await user.click(screen.getByText('issue.comment.submit'));
+  expect(screen.getByText('comment')).toBeInTheDocument();
+
+  // editing the comment
+  expect(screen.getByRole('button', { name: 'issue.comment.edit' })).toBeInTheDocument();
+  await user.click(screen.getByRole('button', { name: 'issue.comment.edit' }));
+  await user.keyboard('New ');
+  await user.click(screen.getByText('save'));
+  expect(screen.getByText('New comment')).toBeInTheDocument();
+
+  // deleting the comment
+  expect(screen.getByRole('button', { name: 'issue.comment.delete' })).toBeInTheDocument();
+  await user.click(screen.getByRole('button', { name: 'issue.comment.delete' }));
+  expect(screen.queryByText('New comment')).not.toBeInTheDocument();
+
+  // adding comment using keyboard
+  await user.click(screen.getByRole('textbox'));
+  await user.keyboard('comment');
+  await user.keyboard('{Control>}{enter}{/Control}');
+  expect(screen.getByText('comment')).toBeInTheDocument();
+
+  // editing the comment using keyboard
+  await user.click(screen.getByRole('button', { name: 'issue.comment.edit' }));
+  await user.keyboard('New ');
+  await user.keyboard('{Control>}{enter}{/Control}');
+  expect(screen.getByText('New comment')).toBeInTheDocument();
+
   // changing tags
   expect(screen.getByText('issue.no_tag')).toBeInTheDocument();
   await user.click(screen.getByText('issue.no_tag'));
index 2fe66f71d8064748eb1a28a77e801aa15403e24e..355e5e5586e24477bd07871be7b7ac605c88768f 100644 (file)
@@ -19,7 +19,7 @@
  */
 import * as React from 'react';
 import { Link } from 'react-router-dom';
-import { setIssueAssignee } from '../../../api/issues';
+import { deleteIssueComment, editIssueComment, setIssueAssignee } from '../../../api/issues';
 import LinkIcon from '../../../components/icons/LinkIcon';
 import { updateIssue } from '../../../components/issue/actions';
 import IssueActionsBar from '../../../components/issue/components/IssueActionsBar';
@@ -65,6 +65,14 @@ export default class IssueHeader extends React.PureComponent<Props, State> {
     this.setState({ issuePopupName: name });
   };
 
+  deleteComment = (comment: string) => {
+    updateIssue(this.props.onIssueChange, deleteIssueComment({ comment }));
+  };
+
+  editComment = (comment: string, text: string) => {
+    updateIssue(this.props.onIssueChange, editIssueComment({ comment, text }));
+  };
+
   handleAssignement = (login: string) => {
     const { issue } = this.props;
     if (issue.assignee !== login) {
@@ -160,6 +168,9 @@ export default class IssueHeader extends React.PureComponent<Props, State> {
           onAssign={this.handleAssignement}
           onChange={this.props.onIssueChange}
           togglePopup={this.handleIssuePopupToggle}
+          deleteComment={this.deleteComment}
+          onEdit={this.editComment}
+          showCommentsInPopup={true}
         />
       </>
     );
index 7567b6de9aecb2dcb11a260cddea3104f2f30efa..4437af36bad18c04286ae4a8a816a921c8b35ff1 100644 (file)
 .issue-get-perma-link {
   flex-shrink: 0;
 }
+
+.issue-comment-list-wrapper {
+  max-height: 400px;
+  overflow-y: scroll;
+}
+
+.issue-comment-tile {
+  background-color: var(--barBackgroundColor);
+}
index e50dc6a900e9907d892495d2dc358033c50f5da5..b144ef317c651de23c2a5828d839659810c5720a 100644 (file)
@@ -36,7 +36,10 @@ interface Props {
   onAssign: (login: string) => void;
   onChange: (issue: Issue) => void;
   togglePopup: (popup: string, show?: boolean) => void;
+  deleteComment?: (comment: string) => void;
+  onEdit?: (comment: string, text: string) => void;
   className?: string;
+  showCommentsInPopup?: boolean;
 }
 
 interface State {
@@ -88,7 +91,7 @@ export default class IssueActionsBar extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { issue, className } = this.props;
+    const { issue, className, showCommentsInPopup } = this.props;
     const canAssign = issue.actions.includes('assign');
     const canComment = issue.actions.includes('comment');
     const canSetSeverity = issue.actions.includes('set_severity');
@@ -153,6 +156,10 @@ export default class IssueActionsBar extends React.PureComponent<Props, State> {
               issueKey={issue.key}
               onChange={this.props.onChange}
               toggleComment={this.toggleComment}
+              comments={issue.comments}
+              deleteComment={this.props.deleteComment}
+              onEdit={this.props.onEdit}
+              showCommentsInPopup={showCommentsInPopup}
             />
           )}
         </div>
index 4a4bb9d81511e7f90242751b651d412d168f5036..43332b92daef4cd2f9fcb3cfac4f308f8346d573 100644 (file)
@@ -22,7 +22,7 @@ import { addIssueComment } from '../../../api/issues';
 import { ButtonLink } from '../../../components/controls/buttons';
 import Toggler from '../../../components/controls/Toggler';
 import { translate } from '../../../helpers/l10n';
-import { Issue } from '../../../types/types';
+import { Issue, IssueComment } from '../../../types/types';
 import { updateIssue } from '../actions';
 import CommentPopup from '../popups/CommentPopup';
 
@@ -33,12 +33,19 @@ interface Props {
   issueKey: string;
   onChange: (issue: Issue) => void;
   toggleComment: (open?: boolean, placeholder?: string, autoTriggered?: boolean) => void;
+  deleteComment?: (comment: string) => void;
+  onEdit?: (comment: string, text: string) => void;
+  comments?: IssueComment[];
+  showCommentsInPopup?: boolean;
 }
 
 export default class IssueCommentAction extends React.PureComponent<Props> {
   addComment = (text: string) => {
+    const { showCommentsInPopup } = this.props;
     updateIssue(this.props.onChange, addIssueComment({ issue: this.props.issueKey, text }));
-    this.props.toggleComment(false);
+    if (!showCommentsInPopup) {
+      this.props.toggleComment(false);
+    }
   };
 
   handleCommentClick = () => {
@@ -50,6 +57,7 @@ export default class IssueCommentAction extends React.PureComponent<Props> {
   };
 
   render() {
+    const { comments, showCommentsInPopup } = this.props;
     return (
       <div className="issue-meta dropdown">
         <Toggler
@@ -62,6 +70,10 @@ export default class IssueCommentAction extends React.PureComponent<Props> {
               onComment={this.addComment}
               placeholder={this.props.commentPlaceholder}
               toggleComment={this.props.toggleComment}
+              comments={comments}
+              deleteComment={this.props.deleteComment}
+              onEdit={this.props.onEdit}
+              showCommentsInPopup={showCommentsInPopup}
             />
           }>
           <ButtonLink
@@ -69,7 +81,12 @@ export default class IssueCommentAction extends React.PureComponent<Props> {
             aria-label={translate('issue.comment.add_comment')}
             className="issue-action js-issue-comment"
             onClick={this.handleCommentClick}>
-            <span className="issue-meta-label">{translate('issue.comment.formlink')}</span>
+            <span className="issue-meta-label">
+              {showCommentsInPopup && comments && comments.length > 0 && (
+                <span className="little-spacer-right">{comments.length}</span>
+              )}
+              {translate('issue.comment.formlink')}
+            </span>
           </ButtonLink>
         </Toggler>
       </div>
index 00655ccdaa2fbd32cc618a95a529971f7780da6d..84cd9848c54c4e368278678875ab94e80706ef5c 100644 (file)
@@ -32,6 +32,8 @@ it('should render issue correctly', () => {
       onAssign={jest.fn()}
       onChange={jest.fn()}
       togglePopup={jest.fn()}
+      deleteComment={jest.fn()}
+      onEdit={jest.fn()}
     />
   );
   expect(element).toMatchSnapshot();
@@ -44,6 +46,8 @@ it('should render security hotspot correctly', () => {
       onAssign={jest.fn()}
       onChange={jest.fn()}
       togglePopup={jest.fn()}
+      deleteComment={jest.fn()}
+      onEdit={jest.fn()}
     />
   );
   expect(element).toMatchSnapshot();
@@ -56,6 +60,8 @@ it('should render commentable correctly', () => {
       onAssign={jest.fn()}
       onChange={jest.fn()}
       togglePopup={jest.fn()}
+      deleteComment={jest.fn()}
+      onEdit={jest.fn()}
     />
   );
   expect(element).toMatchSnapshot();
@@ -68,6 +74,8 @@ it('should render effort correctly', () => {
       onAssign={jest.fn()}
       onChange={jest.fn()}
       togglePopup={jest.fn()}
+      deleteComment={jest.fn()}
+      onEdit={jest.fn()}
     />
   );
   expect(element).toMatchSnapshot();
@@ -84,6 +92,8 @@ describe('callback', () => {
       onAssign={jest.fn()}
       onChange={onChangeMock}
       togglePopup={togglePopupMock}
+      deleteComment={jest.fn()}
+      onEdit={jest.fn()}
     />
   );
 
index 1af12e13bf670d5e7b8a8928d2bbb5296597eac7..cafec1f9c41552552fa07e9dd570ebb892fb97b1 100644 (file)
@@ -42,6 +42,8 @@ function shallowRender(props: Partial<IssueCommentAction['props']> = {}) {
       issueKey="issue-key"
       onChange={jest.fn()}
       toggleComment={jest.fn()}
+      deleteComment={jest.fn()}
+      onEdit={jest.fn()}
       {...props}
     />
   );
index 574359b316b57119da2099d945577ba0ce6d5fb8..5bf3b691fb5c921f7a196f45988e8bda58e848f5 100644 (file)
@@ -182,8 +182,10 @@ exports[`should render commentable correctly 1`] = `
     <IssueCommentAction
       commentAutoTriggered={false}
       commentPlaceholder=""
+      deleteComment={[MockFunction]}
       issueKey="AVsae-CQS-9G3txfbFN2"
       onChange={[MockFunction]}
+      onEdit={[MockFunction]}
       toggleComment={[Function]}
     />
   </div>
index 786ce9ab8eaea8523fff1986b35104e6098a6c77..602e0060008ef1568f2eccf1d4d0cedc9e50a140 100644 (file)
@@ -10,7 +10,9 @@ exports[`should open the popup when the button is clicked 1`] = `
     open={true}
     overlay={
       <CommentPopup
+        deleteComment={[MockFunction]}
         onComment={[Function]}
+        onEdit={[MockFunction]}
         placeholder=""
         toggleComment={
           [MockFunction] {
@@ -54,7 +56,9 @@ exports[`should render correctly 1`] = `
     open={false}
     overlay={
       <CommentPopup
+        deleteComment={[MockFunction]}
         onComment={[Function]}
+        onEdit={[MockFunction]}
         placeholder=""
         toggleComment={[MockFunction]}
       />
diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentList.tsx b/server/sonar-web/src/main/js/components/issue/popups/CommentList.tsx
new file mode 100644 (file)
index 0000000..5c7f570
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { IssueComment } from '../../../types/types';
+import CommentTile from './CommentTile';
+
+interface CommentListProps {
+  comments?: IssueComment[];
+  deleteComment: (comment: string) => void;
+  onEdit: (comment: string, text: string) => void;
+}
+
+export default function CommentList(props: CommentListProps) {
+  const { comments } = props;
+  // sorting comment i.e showing newest on top
+  const sortedComments = comments?.sort(
+    (com1, com2) =>
+      new Date(com2.createdAt || '').getTime() - new Date(com1.createdAt || '').getTime()
+  );
+  return (
+    <div className="issue-comment-list-wrapper">
+      {sortedComments?.map(c => (
+        <CommentTile
+          comment={c}
+          key={c.key}
+          handleDelete={props.deleteComment}
+          onEdit={props.onEdit}
+        />
+      ))}
+    </div>
+  );
+}
index 8b0bae9e518fb4a5ed6268496ef6af853474a5c5..e5fcc5f7411687323008a8478396f4f08790249e 100644 (file)
@@ -25,14 +25,19 @@ import { KeyboardKeys } from '../../../helpers/keycodes';
 import { translate } from '../../../helpers/l10n';
 import { IssueComment } from '../../../types/types';
 import FormattingTips from '../../common/FormattingTips';
+import CommentList from './CommentList';
 
 export interface CommentPopupProps {
   comment?: Pick<IssueComment, 'markdown'>;
   onComment: (text: string) => void;
   toggleComment: (visible: boolean) => void;
+  deleteComment?: (comment: string) => void;
+  onEdit?: (comment: string, text: string) => void;
   placeholder: string;
   placement?: PopupPlacement;
   autoTriggered?: boolean;
+  comments?: IssueComment[];
+  showCommentsInPopup?: boolean;
 }
 
 interface State {
@@ -54,6 +59,7 @@ export default class CommentPopup extends React.PureComponent<CommentPopupProps,
   handleCommentClick = () => {
     if (this.state.textComment.trim().length > 0) {
       this.props.onComment(this.state.textComment);
+      this.setState({ textComment: '' });
     }
   };
 
@@ -78,10 +84,18 @@ export default class CommentPopup extends React.PureComponent<CommentPopupProps,
   };
 
   render() {
-    const { comment, autoTriggered } = this.props;
+    const { comment, autoTriggered, comments, showCommentsInPopup } = this.props;
+
     return (
       <DropdownOverlay placement={this.props.placement}>
         <div className="issue-comment-bubble-popup">
+          {showCommentsInPopup && this.props.deleteComment && this.props.onEdit && (
+            <CommentList
+              comments={comments}
+              deleteComment={this.props.deleteComment}
+              onEdit={this.props.onEdit}
+            />
+          )}
           <div className="issue-comment-form-text">
             <textarea
               autoFocus={true}
@@ -102,7 +116,7 @@ export default class CommentPopup extends React.PureComponent<CommentPopupProps,
                 {!comment && translate('issue.comment.submit')}
               </Button>
               <ResetButtonLink className="js-issue-comment-cancel" onClick={this.handleCancelClick}>
-                {autoTriggered ? translate('skip') : translate('cancel')}
+                {autoTriggered && !showCommentsInPopup ? translate('skip') : translate('cancel')}
               </ResetButtonLink>
             </div>
             <div className="issue-comment-form-tips">
diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentTile.tsx b/server/sonar-web/src/main/js/components/issue/popups/CommentTile.tsx
new file mode 100644 (file)
index 0000000..10f8908
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { KeyboardKeys } from '../../../helpers/keycodes';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { sanitizeString } from '../../../helpers/sanitize';
+import { IssueComment } from '../../../types/types';
+import { Button, DeleteButton, EditButton, ResetButtonLink } from '../../controls/buttons';
+import DateTimeFormatter from '../../intl/DateTimeFormatter';
+import Avatar from '../../ui/Avatar';
+
+interface CommentTileProps {
+  comment: IssueComment;
+  handleDelete: (commentKey: string) => void;
+  onEdit: (comment: string, text: string) => void;
+}
+
+interface CommentTileState {
+  showEditArea: boolean;
+  editedComment: string;
+}
+
+export default class CommentTile extends React.PureComponent<CommentTileProps, CommentTileState> {
+  state = {
+    showEditArea: false,
+    editedComment: ''
+  };
+
+  handleEditClick = () => {
+    const { comment } = this.props;
+    const { showEditArea } = this.state;
+    const editedComment = !showEditArea ? comment.markdown : '';
+    this.setState({ showEditArea: !showEditArea, editedComment });
+  };
+
+  handleSaveClick = () => {
+    const { comment } = this.props;
+    const { editedComment } = this.state;
+    this.props.onEdit(comment.key, editedComment);
+    this.setState({ showEditArea: false, editedComment: '' });
+  };
+
+  handleCancelClick = () => {
+    this.setState({ showEditArea: false });
+  };
+
+  handleEditCommentChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
+    this.setState({ editedComment: event.target.value });
+  };
+
+  handleKeyboard = (event: React.KeyboardEvent) => {
+    if (event.nativeEvent.key === KeyboardKeys.Enter && (event.metaKey || event.ctrlKey)) {
+      this.handleSaveClick();
+    }
+  };
+
+  render() {
+    const { comment } = this.props;
+    const { showEditArea, editedComment } = this.state;
+    const author = comment.authorName || comment.author;
+    const displayName =
+      comment.authorActive === false && author
+        ? translateWithParameters('user.x_deleted', author)
+        : author;
+    return (
+      <div className="issue-comment-tile spacer-bottom padded">
+        <div className="display-flex-center">
+          <div className="issue-comment-author display-flex-center" title={displayName}>
+            <Avatar
+              className="little-spacer-right"
+              hash={comment.authorAvatar}
+              name={author}
+              size={24}
+            />
+            {displayName}
+          </div>
+          <span className="little-spacer-left little-spacer-right">-</span>
+          <DateTimeFormatter date={comment.createdAt} />
+        </div>
+        <div className="spacer-top display-flex-space-between">
+          {!showEditArea && (
+            <div
+              className="flex-1 markdown"
+              // eslint-disable-next-line react/no-danger
+              dangerouslySetInnerHTML={{ __html: sanitizeString(comment.htmlText) }}
+            />
+          )}
+          {showEditArea && (
+            <div className="edit-form flex-1">
+              <div className="issue-comment-form-text">
+                <textarea
+                  autoFocus={true}
+                  onChange={this.handleEditCommentChange}
+                  onKeyDown={this.handleKeyboard}
+                  rows={2}
+                  value={editedComment}
+                />
+              </div>
+              <div className="issue-comment-form-footer">
+                <div className="issue-comment-form-actions little-padded-left">
+                  <Button
+                    className="js-issue-comment-submit little-spacer-right"
+                    disabled={editedComment.trim().length < 1}
+                    onClick={this.handleSaveClick}>
+                    {translate('save')}
+                  </Button>
+                  <ResetButtonLink
+                    className="js-issue-comment-cancel"
+                    onClick={this.handleCancelClick}>
+                    {translate('cancel')}
+                  </ResetButtonLink>
+                </div>
+              </div>
+            </div>
+          )}
+          {comment.updatable && (
+            <div>
+              <EditButton
+                aria-label={translate('issue.comment.edit')}
+                className="js-issue-comment-edit button-small"
+                onClick={this.handleEditClick}
+              />
+              <DeleteButton
+                aria-label={translate('issue.comment.delete')}
+                className="js-issue-comment-delete button-small"
+                onClick={() => {
+                  this.props.handleDelete(comment.key);
+                }}
+              />
+            </div>
+          )}
+        </div>
+      </div>
+    );
+  }
+}
index 95ed70f1af0548d5a1e9afeab2d5d69c39a663a5..bffeda3387a345e6cd3608ad458688db08c2e713 100644 (file)
@@ -80,6 +80,8 @@ function shallowRender(overrides: Partial<CommentPopupProps> = {}) {
       onComment={jest.fn()}
       placeholder="placeholder test"
       toggleComment={jest.fn()}
+      deleteComment={jest.fn()}
+      onEdit={jest.fn()}
       {...overrides}
     />
   );