aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>2022-08-02 14:55:42 +0200
committersonartech <sonartech@sonarsource.com>2022-08-03 20:03:24 +0000
commitaafb356bce29f3c5aa037a661d8e4deddb3ec940 (patch)
tree02848c65e058e9313bf7b5b523b80ded7465b481 /server/sonar-web
parent4c176a2f16fccfbe876cfa6f3a1703026fbc986b (diff)
downloadsonarqube-aafb356bce29f3c5aa037a661d8e4deddb3ec940.tar.gz
sonarqube-aafb356bce29f3c5aa037a661d8e4deddb3ec940.zip
SONAR-16538 Adding new comment list ui for issue comment popup
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts54
-rw-r--r--server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx41
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx13
-rw-r--r--server/sonar-web/src/main/js/components/issue/Issue.css9
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx9
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.tsx23
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueActionsBar-test.tsx10
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueActionsBar-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/CommentList.tsx49
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx18
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/CommentTile.tsx153
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.tsx2
14 files changed, 382 insertions, 7 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
index e42cdf6eef8..f40a56052a6 100644
--- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
@@ -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()] });
};
diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
index 204bb4be7ff..12eba71e38f 100644
--- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx
@@ -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'));
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
index 2fe66f71d80..355e5e5586e 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssueHeader.tsx
@@ -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}
/>
</>
);
diff --git a/server/sonar-web/src/main/js/components/issue/Issue.css b/server/sonar-web/src/main/js/components/issue/Issue.css
index 7567b6de9ae..4437af36bad 100644
--- a/server/sonar-web/src/main/js/components/issue/Issue.css
+++ b/server/sonar-web/src/main/js/components/issue/Issue.css
@@ -271,3 +271,12 @@
.issue-get-perma-link {
flex-shrink: 0;
}
+
+.issue-comment-list-wrapper {
+ max-height: 400px;
+ overflow-y: scroll;
+}
+
+.issue-comment-tile {
+ background-color: var(--barBackgroundColor);
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx
index e50dc6a900e..b144ef317c6 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.tsx
index 4a4bb9d8151..43332b92dae 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.tsx
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.tsx
@@ -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>
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueActionsBar-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueActionsBar-test.tsx
index 00655ccdaa2..84cd9848c54 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueActionsBar-test.tsx
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueActionsBar-test.tsx
@@ -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()}
/>
);
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.tsx
index 1af12e13bf6..cafec1f9c41 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.tsx
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.tsx
@@ -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}
/>
);
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueActionsBar-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueActionsBar-test.tsx.snap
index 574359b316b..5bf3b691fb5 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueActionsBar-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueActionsBar-test.tsx.snap
@@ -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>
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.tsx.snap
index 786ce9ab8ea..602e0060008 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.tsx.snap
@@ -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
index 00000000000..5c7f570eeb9
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/CommentList.tsx
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx
index 8b0bae9e518..e5fcc5f7411 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx
+++ b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx
@@ -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
index 00000000000..10f8908dc33
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/CommentTile.tsx
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.tsx b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.tsx
index 95ed70f1af0..bffeda3387a 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.tsx
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.tsx
@@ -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}
/>
);