diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2022-08-04 17:46:26 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-08-08 20:03:03 +0000 |
commit | 69afe4d2004b3b8b683560be9fa4d01f67082ef9 (patch) | |
tree | 3d0386e7d2da357122f348dd5320bec09aee04a0 | |
parent | ef13264ccbe3b9f03ceeaccab2b4366cb126bb24 (diff) | |
download | sonarqube-69afe4d2004b3b8b683560be9fa4d01f67082ef9.tar.gz sonarqube-69afe4d2004b3b8b683560be9fa4d01f67082ef9.zip |
SONAR-17147 Refactoring comment popup and improving accessibilty
32 files changed, 309 insertions, 1314 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 f40a56052a6..7236a4c7272 100644 --- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts @@ -88,6 +88,7 @@ export default class IssuesServiceMock { list: IssueData[]; constructor() { + // Comment should have their own store as we can test better CRUD operation this.sourceViewerFiles = [ mockSourceViewerFile('file.foo', 'project'), mockSourceViewerFile('file.bar', 'project') @@ -383,9 +384,30 @@ export default class IssuesServiceMock { const statusMap: { [key: string]: string } = { confirm: 'CONFIRMED', unconfirm: 'REOPENED', - resolve: 'RESOLVED' + resolve: 'RESOLVED', + wontfix: 'RESOLVED', + falsepositive: 'RESOLVED' }; - return this.getActionsResponse({ status: statusMap[data.transition] }, data.issue); + const transitionMap: Dict<string[]> = { + REOPENED: ['confirm', 'resolve', 'falsepositive', 'wontfix'], + OPEN: ['confirm', 'resolve', 'falsepositive', 'wontfix'], + CONFIRMED: ['resolve', 'unconfirm', 'falsepositive', 'wontfix'], + RESOLVED: ['reopen'] + }; + + const resolutionMap: Dict<string> = { + wontfix: 'WONTFIX', + falsepositive: 'FALSE-POSITIVE' + }; + + return this.getActionsResponse( + { + status: statusMap[data.transition], + transitions: transitionMap[statusMap[data.transition]], + resolution: resolutionMap[data.transition] + }, + data.issue + ); }; handleSetIssueTags = (data: { issue: string; tags: string }) => { @@ -456,6 +478,7 @@ export default class IssuesServiceMock { ...issueDataSelected?.issue, ...overrides }; + return this.reply({ issue: issueDataSelected.issue }); 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 12eba71e38f..4514bf73532 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 @@ -232,7 +232,15 @@ it('should be able to perform action on issues', async () => { name: `issue.transition.status_x_click_to_change.issue.status.CONFIRMED` }) ).toBeInTheDocument(); - await user.keyboard('{Escape}'); + + // As won't fix + await user.click(screen.getByText('issue.status.CONFIRMED')); + await user.click(screen.getByText('issue.transition.wontfix')); + // Comment should open and close + expect(screen.getByRole('button', { name: 'issue.comment.submit' })).toBeInTheDocument(); + await user.keyboard('test'); + await user.click(screen.getByRole('button', { name: 'issue.comment.submit' })); + expect(screen.queryByRole('button', { name: 'issue.comment.submit' })).not.toBeInTheDocument(); // assigning issue to a different user expect( @@ -271,6 +279,13 @@ it('should be able to perform action on issues', async () => { await user.click(screen.getByText('issue.comment.submit')); expect(screen.getByText('comment')).toBeInTheDocument(); + // Cancel 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.getByRole('button', { name: 'issue.comment.edit.cancel' })); + expect(screen.queryByText('New comment')).not.toBeInTheDocument(); + // editing the comment expect(screen.getByRole('button', { name: 'issue.comment.edit' })).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: 'issue.comment.edit' })); @@ -303,7 +318,12 @@ it('should be able to perform action on issues', async () => { expect(screen.getByText('accessibility')).toBeInTheDocument(); await user.click(screen.getByText('accessibility')); - expect(screen.getAllByText('accessibility')).toHaveLength(2); // one in the list of selector and one selected + await user.click(screen.getByText('android')); + expect(screen.getByTitle('accessibility, android')).toBeInTheDocument(); + + // Unslect + await user.click(screen.getByText('accessibility')); + expect(screen.getByTitle('android')).toBeInTheDocument(); await user.click(screen.getByRole('searchbox', { name: 'search_verb' })); await user.keyboard('addNewTag'); @@ -327,11 +347,13 @@ it('should not allow performing actions when user does not have permission', asy name: `issue.type.type_x_click_to_change.issue.type.CODE_SMELL` }) ).not.toBeInTheDocument(); - expect( - screen.queryByRole('button', { + + await user.click( + screen.getByRole('button', { name: `issue.comment.add_comment` }) - ).not.toBeInTheDocument(); + ); + expect(screen.queryByRole('button', { name: 'issue.comment.submit' })).not.toBeInTheDocument(); expect( screen.queryByRole('button', { name: `issue.transition.status_x_click_to_change.issue.status.OPEN` 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 d2a6c9709d0..0dc77673ba1 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 @@ -20,7 +20,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; -import { deleteIssueComment, editIssueComment, setIssueAssignee } from '../../../api/issues'; +import { setIssueAssignee } from '../../../api/issues'; import DocumentationTooltip from '../../../components/common/DocumentationTooltip'; import Tooltip from '../../../components/controls/Tooltip'; import LinkIcon from '../../../components/icons/LinkIcon'; @@ -65,17 +65,21 @@ export default class IssueHeader extends React.PureComponent<Props, State> { document.removeEventListener('keydown', this.handleKeyDown, { capture: true }); } - handleIssuePopupToggle = (popupName: string, open = true) => { - const name = open ? popupName : undefined; - this.setState({ issuePopupName: name }); - }; + handleIssuePopupToggle = (popupName: string, open?: boolean) => { + const openPopupState = { issuePopupName: popupName }; - deleteComment = (comment: string) => { - updateIssue(this.props.onIssueChange, deleteIssueComment({ comment })); - }; + const closePopupState = { issuePopupName: undefined }; - editComment = (comment: string, text: string) => { - updateIssue(this.props.onIssueChange, editIssueComment({ comment, text })); + this.setState(({ issuePopupName }) => { + if (open) { + return openPopupState; + } else if (open === false) { + return closePopupState; + } + + // toggle popup + return issuePopupName === popupName ? closePopupState : openPopupState; + }); }; handleAssignement = (login: string) => { @@ -229,8 +233,6 @@ 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/components/IssueActionsBar.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx index b144ef317c6..3a59bc53f32 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,8 +36,6 @@ 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; } @@ -148,7 +146,7 @@ export default class IssueActionsBar extends React.PureComponent<Props, State> { </span> </div> )} - {canComment && ( + {(canComment || showCommentsInPopup) && ( <IssueCommentAction commentAutoTriggered={this.state.commentAutoTriggered} commentPlaceholder={this.state.commentPlaceholder} @@ -157,8 +155,7 @@ export default class IssueActionsBar extends React.PureComponent<Props, State> { onChange={this.props.onChange} toggleComment={this.toggleComment} comments={issue.comments} - deleteComment={this.props.deleteComment} - onEdit={this.props.onEdit} + canComment={canComment} showCommentsInPopup={showCommentsInPopup} /> )} 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 43332b92dae..7a234bc6fa4 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 @@ -18,23 +18,23 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { addIssueComment } from '../../../api/issues'; +import { addIssueComment, deleteIssueComment, editIssueComment } from '../../../api/issues'; import { ButtonLink } from '../../../components/controls/buttons'; import Toggler from '../../../components/controls/Toggler'; import { translate } from '../../../helpers/l10n'; import { Issue, IssueComment } from '../../../types/types'; import { updateIssue } from '../actions'; import CommentPopup from '../popups/CommentPopup'; +import CommentListPopup from '../popups/CommentsListPopup'; interface Props { + canComment: boolean; commentAutoTriggered?: boolean; commentPlaceholder: string; currentPopup?: string; 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; } @@ -48,6 +48,14 @@ export default class IssueCommentAction extends React.PureComponent<Props> { } }; + handleEditComment = (comment: string, text: string) => { + updateIssue(this.props.onChange, editIssueComment({ comment, text })); + }; + + handleDeleteComment = (comment: string) => { + updateIssue(this.props.onChange, deleteIssueComment({ comment })); + }; + handleCommentClick = () => { this.props.toggleComment(); }; @@ -57,7 +65,7 @@ export default class IssueCommentAction extends React.PureComponent<Props> { }; render() { - const { comments, showCommentsInPopup } = this.props; + const { comments, showCommentsInPopup, canComment } = this.props; return ( <div className="issue-meta dropdown"> <Toggler @@ -65,16 +73,25 @@ export default class IssueCommentAction extends React.PureComponent<Props> { onRequestClose={this.handleClose} open={this.props.currentPopup === 'comment'} overlay={ - <CommentPopup - autoTriggered={this.props.commentAutoTriggered} - onComment={this.addComment} - placeholder={this.props.commentPlaceholder} - toggleComment={this.props.toggleComment} - comments={comments} - deleteComment={this.props.deleteComment} - onEdit={this.props.onEdit} - showCommentsInPopup={showCommentsInPopup} - /> + showCommentsInPopup ? ( + <CommentListPopup + comments={comments} + deleteComment={this.handleDeleteComment} + onAddComment={this.addComment} + onEdit={this.handleEditComment} + placeholder={this.props.commentPlaceholder} + toggleComment={this.props.toggleComment} + autoTriggered={this.props.commentAutoTriggered} + canComment={canComment} + /> + ) : ( + <CommentPopup + autoTriggered={this.props.commentAutoTriggered} + onComment={this.addComment} + placeholder={this.props.commentPlaceholder} + toggleComment={this.props.toggleComment} + /> + ) }> <ButtonLink aria-expanded={this.props.currentPopup === 'comment'} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx index 7a4fe3515dc..e3bc90d7681 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx @@ -42,7 +42,7 @@ export default class IssueTransition extends React.PureComponent<Props> { this.props.onChange, setIssueTransition({ issue: this.props.issue.key, transition }) ); - this.toggleSetTransition(); + this.toggleSetTransition(false); }; toggleSetTransition = (open?: boolean) => { 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 84cd9848c54..00655ccdaa2 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,8 +32,6 @@ it('should render issue correctly', () => { onAssign={jest.fn()} onChange={jest.fn()} togglePopup={jest.fn()} - deleteComment={jest.fn()} - onEdit={jest.fn()} /> ); expect(element).toMatchSnapshot(); @@ -46,8 +44,6 @@ it('should render security hotspot correctly', () => { onAssign={jest.fn()} onChange={jest.fn()} togglePopup={jest.fn()} - deleteComment={jest.fn()} - onEdit={jest.fn()} /> ); expect(element).toMatchSnapshot(); @@ -60,8 +56,6 @@ it('should render commentable correctly', () => { onAssign={jest.fn()} onChange={jest.fn()} togglePopup={jest.fn()} - deleteComment={jest.fn()} - onEdit={jest.fn()} /> ); expect(element).toMatchSnapshot(); @@ -74,8 +68,6 @@ it('should render effort correctly', () => { onAssign={jest.fn()} onChange={jest.fn()} togglePopup={jest.fn()} - deleteComment={jest.fn()} - onEdit={jest.fn()} /> ); expect(element).toMatchSnapshot(); @@ -92,8 +84,6 @@ 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 deleted file mode 100644 index cafec1f9c41..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { click } from '../../../../helpers/testUtils'; -import IssueCommentAction from '../IssueCommentAction'; - -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should open the popup when the button is clicked', () => { - const toggleComment = jest.fn(); - const element = shallowRender({ toggleComment }); - click(element.find('ButtonLink')); - expect(toggleComment.mock.calls.length).toBe(1); - element.setProps({ currentPopup: 'comment' }); - expect(element).toMatchSnapshot(); -}); - -function shallowRender(props: Partial<IssueCommentAction['props']> = {}) { - return shallow( - <IssueCommentAction - commentPlaceholder="" - 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__/IssueTags-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.tsx deleted file mode 100644 index 1502fc53721..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { click } from '../../../../helpers/testUtils'; -import IssueTags from '../IssueTags'; - -const issue = { key: 'issuekey', tags: ['mytag', 'test'] }; - -it('should render without the action when the correct rights are missing', () => { - expect(shallowRender({ canSetTags: false, issue: { ...issue, tags: [] } })).toMatchSnapshot(); -}); - -it('should render with the action', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should open the popup when the button is clicked', () => { - const togglePopup = jest.fn(); - const element = shallowRender({ togglePopup }); - click(element.find('ButtonLink')); - expect(togglePopup.mock.calls).toMatchSnapshot(); - element.setProps({ isOpen: true }); - expect(element).toMatchSnapshot(); -}); - -function shallowRender(props: Partial<IssueTags['props']> = {}) { - return shallow( - <IssueTags - canSetTags={true} - isOpen={false} - issue={issue} - onChange={jest.fn()} - togglePopup={jest.fn()} - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.tsx index bbb635ac90c..a0985d8ef70 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.tsx @@ -19,7 +19,6 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { click } from '../../../../helpers/testUtils'; import IssueTransition from '../IssueTransition'; const issue: IssueTransition['props']['issue'] = { @@ -39,34 +38,6 @@ it('should render without the action when there is no transitions', () => { ).toMatchSnapshot(); }); -it('should render with the action', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should render with a resolution', () => { - expect( - shallowRender({ - issue: { - fromHotspot: false, - key: 'foo1234', - transitions: ['reopen'], - status: 'RESOLVED', - resolution: 'FIXED', - type: 'BUG' - } - }) - ).toMatchSnapshot(); -}); - -it('should open the popup when the button is clicked', () => { - const togglePopup = jest.fn(); - const element = shallowRender({ togglePopup }); - click(element.find('ButtonLink')); - expect(togglePopup.mock.calls).toMatchSnapshot(); - element.setProps({ isOpen: true }); - expect(element).toMatchSnapshot(); -}); - function shallowRender(props: Partial<IssueTransition['props']> = {}) { return shallow( <IssueTransition diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.tsx b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.tsx index 7242637a05a..ef627eeec5d 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.tsx @@ -19,7 +19,6 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { click } from '../../../../helpers/testUtils'; import { Issue } from '../../../../types/types'; import IssueType from '../IssueType'; @@ -29,19 +28,6 @@ it('should render without the action when the correct rights are missing', () => expect(shallowRender({ canSetType: false })).toMatchSnapshot(); }); -it('should render with the action', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should open the popup when the button is clicked', () => { - const togglePopup = jest.fn(); - const element = shallowRender({ togglePopup }); - click(element.find('ButtonLink')); - expect(togglePopup.mock.calls).toMatchSnapshot(); - element.setProps({ isOpen: true }); - expect(element).toMatchSnapshot(); -}); - function shallowRender(props: Partial<IssueType['props']> = {}) { return shallow( <IssueType 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 5bf3b691fb5..2efed8970bf 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 @@ -180,12 +180,11 @@ exports[`should render commentable correctly 1`] = ` /> </div> <IssueCommentAction + canComment={true} 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 deleted file mode 100644 index 602e0060008..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.tsx.snap +++ /dev/null @@ -1,81 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should open the popup when the button is clicked 1`] = ` -<div - className="issue-meta dropdown" -> - <Toggler - closeOnClickOutside={false} - onRequestClose={[Function]} - open={true} - overlay={ - <CommentPopup - deleteComment={[MockFunction]} - onComment={[Function]} - onEdit={[MockFunction]} - placeholder="" - toggleComment={ - [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - } - } - /> - } - > - <ButtonLink - aria-expanded={true} - aria-label="issue.comment.add_comment" - className="issue-action js-issue-comment" - onClick={[Function]} - > - <span - className="issue-meta-label" - > - issue.comment.formlink - </span> - </ButtonLink> - </Toggler> -</div> -`; - -exports[`should render correctly 1`] = ` -<div - className="issue-meta dropdown" -> - <Toggler - closeOnClickOutside={false} - onRequestClose={[Function]} - open={false} - overlay={ - <CommentPopup - deleteComment={[MockFunction]} - onComment={[Function]} - onEdit={[MockFunction]} - placeholder="" - toggleComment={[MockFunction]} - /> - } - > - <ButtonLink - aria-expanded={false} - aria-label="issue.comment.add_comment" - className="issue-action js-issue-comment" - onClick={[Function]} - > - <span - className="issue-meta-label" - > - issue.comment.formlink - </span> - </ButtonLink> - </Toggler> -</div> -`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.tsx.snap deleted file mode 100644 index 106169cb019..00000000000 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.tsx.snap +++ /dev/null @@ -1,98 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should open the popup when the button is clicked 1`] = ` -Array [ - Array [ - "edit-tags", - undefined, - ], -] -`; - -exports[`should open the popup when the button is clicked 2`] = ` -<div - className="dropdown" -> - <Toggler - onRequestClose={[Function]} - open={true} - overlay={ - <SetIssueTagsPopup - selectedTags={ - Array [ - "mytag", - "test", - ] - } - setTags={[Function]} - /> - } - > - <ButtonLink - aria-expanded={true} - className="issue-action issue-action-with-options js-issue-edit-tags" - onClick={[Function]} - > - <TagsList - allowUpdate={true} - tags={ - Array [ - "mytag", - "test", - ] - } - /> - </ButtonLink> - </Toggler> -</div> -`; - -exports[`should render with the action 1`] = ` -<div - className="dropdown" -> - <Toggler - onRequestClose={[Function]} - open={false} - overlay={ - <SetIssueTagsPopup - selectedTags={ - Array [ - "mytag", - "test", - ] - } - setTags={[Function]} - /> - } - > - <ButtonLink - aria-expanded={false} - className="issue-action issue-action-with-options js-issue-edit-tags" - onClick={[Function]} - > - <TagsList - allowUpdate={true} - tags={ - Array [ - "mytag", - "test", - ] - } - /> - </ButtonLink> - </Toggler> -</div> -`; - -exports[`should render without the action when the correct rights are missing 1`] = ` -<TagsList - allowUpdate={false} - className="note" - tags={ - Array [ - "issue.no_tag", - ] - } -/> -`; diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.tsx.snap index d3d54e58071..e4eb999534e 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.tsx.snap @@ -1,135 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should open the popup when the button is clicked 1`] = ` -Array [ - Array [ - "transition", - undefined, - ], -] -`; - -exports[`should open the popup when the button is clicked 2`] = ` -<div - className="dropdown" -> - <Toggler - onRequestClose={[Function]} - open={true} - overlay={ - <SetTransitionPopup - fromHotspot={false} - onSelect={[Function]} - transitions={ - Array [ - "confirm", - "resolve", - "falsepositive", - "wontfix", - ] - } - type="BUG" - /> - } - > - <ButtonLink - aria-expanded={true} - aria-label="issue.transition.status_x_click_to_change.issue.status.OPEN" - className="issue-action issue-action-with-options js-issue-transition" - onClick={[Function]} - > - <StatusHelper - className="issue-meta-label" - status="OPEN" - /> - <DropdownIcon - className="little-spacer-left" - /> - </ButtonLink> - </Toggler> -</div> -`; - -exports[`should render with a resolution 1`] = ` -<div - className="dropdown" -> - <Toggler - onRequestClose={[Function]} - open={false} - overlay={ - <SetTransitionPopup - fromHotspot={false} - onSelect={[Function]} - transitions={ - Array [ - "reopen", - ] - } - type="BUG" - /> - } - > - <ButtonLink - aria-expanded={false} - aria-label="issue.transition.status_x_click_to_change.issue.status.RESOLVED" - className="issue-action issue-action-with-options js-issue-transition" - onClick={[Function]} - > - <StatusHelper - className="issue-meta-label" - resolution="FIXED" - status="RESOLVED" - /> - <DropdownIcon - className="little-spacer-left" - /> - </ButtonLink> - </Toggler> -</div> -`; - -exports[`should render with the action 1`] = ` -<div - className="dropdown" -> - <Toggler - onRequestClose={[Function]} - open={false} - overlay={ - <SetTransitionPopup - fromHotspot={false} - onSelect={[Function]} - transitions={ - Array [ - "confirm", - "resolve", - "falsepositive", - "wontfix", - ] - } - type="BUG" - /> - } - > - <ButtonLink - aria-expanded={false} - aria-label="issue.transition.status_x_click_to_change.issue.status.OPEN" - className="issue-action issue-action-with-options js-issue-transition" - onClick={[Function]} - > - <StatusHelper - className="issue-meta-label" - status="OPEN" - /> - <DropdownIcon - className="little-spacer-left" - /> - </ButtonLink> - </Toggler> -</div> -`; - exports[`should render without the action when there is no transitions 1`] = ` <StatusHelper className="issue-meta-label" diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.tsx.snap index b07ade8d1fd..1078e100a75 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.tsx.snap @@ -1,90 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should open the popup when the button is clicked 1`] = ` -Array [ - Array [ - "set-type", - undefined, - ], -] -`; - -exports[`should open the popup when the button is clicked 2`] = ` -<div - className="dropdown" -> - <Toggler - onRequestClose={[Function]} - open={true} - overlay={ - <SetTypePopup - issue={ - Object { - "type": "BUG", - } - } - onSelect={[Function]} - /> - } - > - <ButtonLink - aria-expanded={true} - aria-label="issue.type.type_x_click_to_change.issue.type.BUG" - className="issue-action issue-action-with-options js-issue-set-type" - onClick={[Function]} - > - <IssueTypeIcon - className="little-spacer-right" - fill="#333" - query="BUG" - /> - issue.type.BUG - <DropdownIcon - className="little-spacer-left" - /> - </ButtonLink> - </Toggler> -</div> -`; - -exports[`should render with the action 1`] = ` -<div - className="dropdown" -> - <Toggler - onRequestClose={[Function]} - open={false} - overlay={ - <SetTypePopup - issue={ - Object { - "type": "BUG", - } - } - onSelect={[Function]} - /> - } - > - <ButtonLink - aria-expanded={false} - aria-label="issue.type.type_x_click_to_change.issue.type.BUG" - className="issue-action issue-action-with-options js-issue-set-type" - onClick={[Function]} - > - <IssueTypeIcon - className="little-spacer-right" - fill="#333" - query="BUG" - /> - issue.type.BUG - <DropdownIcon - className="little-spacer-left" - /> - </ButtonLink> - </Toggler> -</div> -`; - exports[`should render without the action when the correct rights are missing 1`] = ` <span> <IssueTypeIcon diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentForm.tsx b/server/sonar-web/src/main/js/components/issue/popups/CommentForm.tsx new file mode 100644 index 00000000000..4fd1f3fa6b1 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/CommentForm.tsx @@ -0,0 +1,88 @@ +/* + * 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 } from '../../../helpers/l10n'; +import FormattingTips from '../../common/FormattingTips'; +import { Button, ResetButtonLink } from '../../controls/buttons'; + +export interface CommentFormProps { + comment?: string; + onCancel: () => void; + onSaveComment: (comment: string) => void; + placeholder?: string; + showFormatHelp: boolean; + autoTriggered?: boolean; +} + +export default function CommentForm(props: CommentFormProps) { + const { comment, placeholder, showFormatHelp, autoTriggered } = props; + const [editComment, setEditComment] = React.useState(comment || ''); + + return ( + <> + <div className="issue-comment-form-text"> + <textarea + autoFocus={true} + placeholder={placeholder} + onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => + setEditComment(event.target.value) + } + onKeyDown={(event: React.KeyboardEvent) => { + if (event.nativeEvent.key === KeyboardKeys.Enter && (event.metaKey || event.ctrlKey)) { + props.onSaveComment(editComment); + setEditComment(''); + } + }} + rows={2} + value={editComment} + /> + </div> + <div className="issue-comment-form-footer"> + {showFormatHelp && ( + <div className="issue-comment-form-tips"> + <FormattingTips /> + </div> + )} + <div className="issue-comment-form-actions"> + <Button + className="js-issue-comment-submit little-spacer-right" + disabled={editComment.trim().length < 1} + onClick={() => { + props.onSaveComment(editComment); + setEditComment(''); + }}> + {comment ? translate('save') : translate('issue.comment.submit')} + </Button> + <ResetButtonLink + className="js-issue-comment-cancel" + aria-label={ + comment + ? translate('issue.comment.edit.cancel') + : translate('issue.comment.add_comment.cancel') + } + onClick={() => props.onCancel()}> + {autoTriggered ? translate('skip') : translate('cancel')} + </ResetButtonLink> + </div> + </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 e5fcc5f7411..9e122c0d6d7 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 @@ -18,111 +18,39 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Button, ResetButtonLink } from '../../../components/controls/buttons'; import { DropdownOverlay } from '../../../components/controls/Dropdown'; import { PopupPlacement } from '../../../components/ui/popups'; -import { KeyboardKeys } from '../../../helpers/keycodes'; -import { translate } from '../../../helpers/l10n'; import { IssueComment } from '../../../types/types'; -import FormattingTips from '../../common/FormattingTips'; -import CommentList from './CommentList'; +import CommentForm from './CommentForm'; 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 { - textComment: string; -} - -export default class CommentPopup extends React.PureComponent<CommentPopupProps, State> { - constructor(props: CommentPopupProps) { - super(props); - this.state = { - textComment: props.comment ? props.comment.markdown : '' - }; - } - - handleCommentChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => { - this.setState({ textComment: event.target.value }); - }; - - handleCommentClick = () => { - if (this.state.textComment.trim().length > 0) { - this.props.onComment(this.state.textComment); - this.setState({ textComment: '' }); - } - }; - +export default class CommentPopup extends React.PureComponent<CommentPopupProps> { handleCancelClick = () => { this.props.toggleComment(false); }; - handleKeyboard = (event: React.KeyboardEvent) => { - if (event.nativeEvent.key === KeyboardKeys.Enter && (event.metaKey || event.ctrlKey)) { - this.handleCommentClick(); - } else if ( - [ - KeyboardKeys.UpArrow, - KeyboardKeys.DownArrow, - KeyboardKeys.LeftArrow, - KeyboardKeys.RightArrow - ].includes(event.nativeEvent.key as KeyboardKeys) - ) { - // Arrow keys - event.stopPropagation(); - } - }; - render() { - const { comment, autoTriggered, comments, showCommentsInPopup } = this.props; + const { comment, autoTriggered } = 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} - onChange={this.handleCommentChange} - onKeyDown={this.handleKeyboard} - placeholder={this.props.placeholder} - rows={2} - value={this.state.textComment} - /> - </div> - <div className="issue-comment-form-footer"> - <div className="issue-comment-form-actions"> - <Button - className="js-issue-comment-submit little-spacer-right" - disabled={this.state.textComment.trim().length < 1} - onClick={this.handleCommentClick}> - {comment && translate('save')} - {!comment && translate('issue.comment.submit')} - </Button> - <ResetButtonLink className="js-issue-comment-cancel" onClick={this.handleCancelClick}> - {autoTriggered && !showCommentsInPopup ? translate('skip') : translate('cancel')} - </ResetButtonLink> - </div> - <div className="issue-comment-form-tips"> - <FormattingTips /> - </div> - </div> + <CommentForm + placeholder={this.props.placeholder} + onCancel={this.handleCancelClick} + onSaveComment={this.props.onComment} + showFormatHelp={true} + comment={comment?.markdown} + autoTriggered={autoTriggered} + /> </div> </DropdownOverlay> ); 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 index 10f8908dc33..c94c741b439 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/CommentTile.tsx +++ b/server/sonar-web/src/main/js/components/issue/popups/CommentTile.tsx @@ -18,13 +18,13 @@ * 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 { DeleteButton, EditButton } from '../../controls/buttons'; import DateTimeFormatter from '../../intl/DateTimeFormatter'; import Avatar from '../../ui/Avatar'; +import CommentForm from './CommentForm'; interface CommentTileProps { comment: IssueComment; @@ -34,46 +34,31 @@ interface CommentTileProps { interface CommentTileState { showEditArea: boolean; - editedComment: string; } export default class CommentTile extends React.PureComponent<CommentTileProps, CommentTileState> { state = { - showEditArea: false, - editedComment: '' + showEditArea: false }; handleEditClick = () => { - const { comment } = this.props; const { showEditArea } = this.state; - const editedComment = !showEditArea ? comment.markdown : ''; - this.setState({ showEditArea: !showEditArea, editedComment }); + this.setState({ showEditArea: !showEditArea }); }; - handleSaveClick = () => { + handleSaveClick = (editedComment: string) => { const { comment } = this.props; - const { editedComment } = this.state; this.props.onEdit(comment.key, editedComment); - this.setState({ showEditArea: false, editedComment: '' }); + this.setState({ showEditArea: false }); }; 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 { showEditArea } = this.state; const author = comment.authorName || comment.author; const displayName = comment.authorActive === false && author @@ -103,31 +88,13 @@ export default class CommentTile extends React.PureComponent<CommentTileProps, C /> )} {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 className="flex-1"> + <CommentForm + onCancel={this.handleCancelClick} + onSaveComment={this.handleSaveClick} + showFormatHelp={false} + comment={comment.markdown} + /> </div> )} {comment.updatable && ( diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentsListPopup.tsx b/server/sonar-web/src/main/js/components/issue/popups/CommentsListPopup.tsx new file mode 100644 index 00000000000..2691486180f --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/popups/CommentsListPopup.tsx @@ -0,0 +1,78 @@ +/* + * 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 { DropdownOverlay } from '../../../components/controls/Dropdown'; +import { PopupPlacement } from '../../../components/ui/popups'; +import { IssueComment } from '../../../types/types'; +import CommentForm from './CommentForm'; +import CommentList from './CommentList'; + +export interface Props { + onAddComment: (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[]; + canComment: boolean; +} + +export default class CommentListPopup extends React.PureComponent<Props, {}> { + handleCommentClick = (comment: string) => { + const { autoTriggered } = this.props; + + this.props.onAddComment(comment); + + if (autoTriggered) { + this.props.toggleComment(false); + } + }; + + handleCancelClick = () => { + this.props.toggleComment(false); + }; + + render() { + const { comments, placeholder, autoTriggered, canComment } = this.props; + + return ( + <DropdownOverlay placement={this.props.placement}> + <div className="issue-comment-bubble-popup"> + <CommentList + comments={comments} + deleteComment={this.props.deleteComment} + onEdit={this.props.onEdit} + /> + {canComment && ( + <CommentForm + autoTriggered={autoTriggered} + placeholder={placeholder} + onCancel={this.handleCancelClick} + onSaveComment={this.handleCommentClick} + showFormatHelp={true} + /> + )} + </div> + </DropdownOverlay> + ); + } +} 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 bffeda3387a..dd3e8007aa7 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 @@ -17,71 +17,33 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; +import { screen } from '@testing-library/dom'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { KeyboardKeys } from '../../../../helpers/keycodes'; -import { click, mockEvent } from '../../../../helpers/testUtils'; +import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import CommentPopup, { CommentPopupProps } from '../CommentPopup'; -it('should render the comment popup correctly without existing comment', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should render the comment popup correctly when changing a comment', () => { - expect(shallowRender({ comment: { markdown: '*test*' } })).toMatchSnapshot(); -}); - -it('should render not allow to send comment with only spaces', () => { +it('should trigger comment change', async () => { + const user = userEvent.setup(); const onComment = jest.fn(); - const wrapper = shallowRender({ onComment }); - click(wrapper.find('.js-issue-comment-submit')); - expect(onComment.mock.calls.length).toBe(0); - wrapper.setState({ textComment: 'mycomment' }); - click(wrapper.find('.js-issue-comment-submit')); - expect(onComment.mock.calls.length).toBe(1); -}); - -it('should render the alternative cancel button label', () => { - const wrapper = shallowRender({ autoTriggered: true }); - expect( - wrapper - .find('.js-issue-comment-cancel') - .childAt(0) - .text() - ).toBe('skip'); -}); - -it('should handle ctrl+enter', () => { - const onComment = jest.fn(); - const wrapper = shallowRender({ comment: { markdown: 'yes' }, onComment }); - - wrapper - .instance() - .handleKeyboard(mockEvent({ ctrlKey: true, nativeEvent: { key: KeyboardKeys.Enter } })); - - expect(onComment).toBeCalled(); -}); - -it('should stopPropagation for arrow keys events', () => { - const wrapper = shallowRender(); + const toggleComment = jest.fn(); + shallowRender({ onComment, toggleComment }); - const event = mockEvent({ - nativeEvent: { key: KeyboardKeys.UpArrow }, - stopPropagation: jest.fn() - }); - wrapper.instance().handleKeyboard(event); + expect(await screen.findByRole('textbox')).toHaveFocus(); + await user.keyboard('test'); + await user.keyboard('{Control>}{Enter}{/Control}'); + expect(onComment).toHaveBeenCalledWith('test'); - expect(event.stopPropagation).toBeCalled(); + await user.click(screen.getByRole('button', { name: 'issue.comment.add_comment.cancel' })); + expect(toggleComment).toHaveBeenCalledWith(false); }); function shallowRender(overrides: Partial<CommentPopupProps> = {}) { - return shallow<CommentPopup>( + return renderComponent( <CommentPopup onComment={jest.fn()} placeholder="placeholder test" toggleComment={jest.fn()} - deleteComment={jest.fn()} - onEdit={jest.fn()} {...overrides} /> ); diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx deleted file mode 100644 index 1f5c3572e05..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import SetIssueTagsPopup from '../SetIssueTagsPopup'; - -it('should render tags popup correctly', () => { - const element = shallow(<SetIssueTagsPopup selectedTags={['mytag']} setTags={jest.fn()} />); - element.setState({ searchResult: ['mytag', 'test', 'second'] }); - expect(element).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetSeverityPopup-test.tsx b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetSeverityPopup-test.tsx deleted file mode 100644 index 5b2040e23bb..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetSeverityPopup-test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import SetSeverityPopup from '../SetSeverityPopup'; - -it('should render tags popup correctly', () => { - const element = shallow(<SetSeverityPopup issue={{ severity: 'MAJOR' }} onSelect={jest.fn()} />); - expect(element).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTransitionPopup-test.tsx b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTransitionPopup-test.tsx deleted file mode 100644 index 0103c77e306..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTransitionPopup-test.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { hasMessage } from '../../../../helpers/l10n'; -import SetTransitionPopup, { Props } from '../SetTransitionPopup'; - -jest.mock('../../../../helpers/l10n', () => ({ - ...jest.requireActual('../../../../helpers/l10n'), - hasMessage: jest.fn().mockReturnValue(false) -})); - -it('should render transition popup correctly', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -it('should render transition popup correctly for vulnerability', () => { - (hasMessage as jest.Mock).mockReturnValueOnce('true'); - expect( - shallowRender({ - fromHotspot: true, - transitions: ['resolveasreviewed', 'confirm'] - }) - ).toMatchSnapshot(); -}); - -function shallowRender(props: Partial<Props> = {}) { - return shallow( - <SetTransitionPopup - fromHotspot={false} - onSelect={jest.fn()} - transitions={['confirm', 'resolve', 'falsepositive', 'wontfix']} - type="VULNERABILITY" - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTypePopup-test.tsx b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTypePopup-test.tsx deleted file mode 100644 index 2c6b4c49e29..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTypePopup-test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import SetTypePopup from '../SetTypePopup'; - -it('should render tags popup correctly', () => { - const element = shallow(<SetTypePopup issue={{ type: 'BUG' }} onSelect={jest.fn()} />); - expect(element).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.tsx.snap deleted file mode 100644 index 9fc6c4cc228..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.tsx.snap +++ /dev/null @@ -1,95 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render the comment popup correctly when changing a comment 1`] = ` -<DropdownOverlay> - <div - className="issue-comment-bubble-popup" - > - <div - className="issue-comment-form-text" - > - <textarea - autoFocus={true} - onChange={[Function]} - onKeyDown={[Function]} - placeholder="placeholder test" - rows={2} - value="*test*" - /> - </div> - <div - className="issue-comment-form-footer" - > - <div - className="issue-comment-form-actions" - > - <Button - className="js-issue-comment-submit little-spacer-right" - disabled={false} - onClick={[Function]} - > - save - </Button> - <ResetButtonLink - className="js-issue-comment-cancel" - onClick={[Function]} - > - cancel - </ResetButtonLink> - </div> - <div - className="issue-comment-form-tips" - > - <FormattingTips /> - </div> - </div> - </div> -</DropdownOverlay> -`; - -exports[`should render the comment popup correctly without existing comment 1`] = ` -<DropdownOverlay> - <div - className="issue-comment-bubble-popup" - > - <div - className="issue-comment-form-text" - > - <textarea - autoFocus={true} - onChange={[Function]} - onKeyDown={[Function]} - placeholder="placeholder test" - rows={2} - value="" - /> - </div> - <div - className="issue-comment-form-footer" - > - <div - className="issue-comment-form-actions" - > - <Button - className="js-issue-comment-submit little-spacer-right" - disabled={true} - onClick={[Function]} - > - issue.comment.submit - </Button> - <ResetButtonLink - className="js-issue-comment-cancel" - onClick={[Function]} - > - cancel - </ResetButtonLink> - </div> - <div - className="issue-comment-form-tips" - > - <FormattingTips /> - </div> - </div> - </div> -</DropdownOverlay> -`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap deleted file mode 100644 index a5eb2aafcac..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render tags popup correctly 1`] = ` -<DropdownOverlay - placement="bottom-right" -> - <TagsSelector - listSize={10} - onSearch={[Function]} - onSelect={[Function]} - onUnselect={[Function]} - selectedTags={ - Array [ - "mytag", - ] - } - tags={ - Array [ - "test", - "second", - ] - } - /> -</DropdownOverlay> -`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.tsx.snap deleted file mode 100644 index 405a967b10d..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.tsx.snap +++ /dev/null @@ -1,75 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render tags popup correctly 1`] = ` -<DropdownOverlay> - <SelectList - currentItem="MAJOR" - items={ - Array [ - "BLOCKER", - "CRITICAL", - "MAJOR", - "MINOR", - "INFO", - ] - } - onSelect={[MockFunction]} - > - <SelectListItem - className="display-flex-center" - item="BLOCKER" - key="BLOCKER" - > - <SeverityIcon - className="little-spacer-right" - severity="BLOCKER" - /> - severity.BLOCKER - </SelectListItem> - <SelectListItem - className="display-flex-center" - item="CRITICAL" - key="CRITICAL" - > - <SeverityIcon - className="little-spacer-right" - severity="CRITICAL" - /> - severity.CRITICAL - </SelectListItem> - <SelectListItem - className="display-flex-center" - item="MAJOR" - key="MAJOR" - > - <SeverityIcon - className="little-spacer-right" - severity="MAJOR" - /> - severity.MAJOR - </SelectListItem> - <SelectListItem - className="display-flex-center" - item="MINOR" - key="MINOR" - > - <SeverityIcon - className="little-spacer-right" - severity="MINOR" - /> - severity.MINOR - </SelectListItem> - <SelectListItem - className="display-flex-center" - item="INFO" - key="INFO" - > - <SeverityIcon - className="little-spacer-right" - severity="INFO" - /> - severity.INFO - </SelectListItem> - </SelectList> -</DropdownOverlay> -`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.tsx.snap deleted file mode 100644 index 70a86d8ea52..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.tsx.snap +++ /dev/null @@ -1,157 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render transition popup correctly 1`] = ` -<DropdownOverlay> - <SelectList - currentItem="confirm" - items={ - Array [ - "confirm", - "resolve", - "falsepositive", - "wontfix", - ] - } - onSelect={[MockFunction]} - > - <SelectListItem - item="confirm" - key="confirm" - title={ - <FormattedMessage - defaultMessage="issue.transition.confirm.description" - id="issue.transition.confirm.description" - values={ - Object { - "community_plug_link": <a - href="https://community.sonarsource.com/" - rel="noopener noreferrer" - target="_blank" - > - issue.transition.community_plug_link - </a>, - } - } - /> - } - > - issue.transition.confirm - </SelectListItem> - <SelectListItem - item="resolve" - key="resolve" - title={ - <FormattedMessage - defaultMessage="issue.transition.resolve.description" - id="issue.transition.resolve.description" - values={ - Object { - "community_plug_link": <a - href="https://community.sonarsource.com/" - rel="noopener noreferrer" - target="_blank" - > - issue.transition.community_plug_link - </a>, - } - } - /> - } - > - issue.transition.resolve - </SelectListItem> - <SelectListItem - item="falsepositive" - key="falsepositive" - title={ - <FormattedMessage - defaultMessage="issue.transition.falsepositive.description" - id="issue.transition.falsepositive.description" - values={ - Object { - "community_plug_link": <a - href="https://community.sonarsource.com/" - rel="noopener noreferrer" - target="_blank" - > - issue.transition.community_plug_link - </a>, - } - } - /> - } - > - issue.transition.falsepositive - </SelectListItem> - <SelectListItem - item="wontfix" - key="wontfix" - title={ - <FormattedMessage - defaultMessage="issue.transition.wontfix.description" - id="issue.transition.wontfix.description" - values={ - Object { - "community_plug_link": <a - href="https://community.sonarsource.com/" - rel="noopener noreferrer" - target="_blank" - > - issue.transition.community_plug_link - </a>, - } - } - /> - } - > - issue.transition.wontfix - </SelectListItem> - </SelectList> -</DropdownOverlay> -`; - -exports[`should render transition popup correctly for vulnerability 1`] = ` -<DropdownOverlay> - <SelectList - currentItem="resolveasreviewed" - items={ - Array [ - "resolveasreviewed", - "confirm", - ] - } - onSelect={[MockFunction]} - > - <SelectListItem - item="resolveasreviewed" - key="resolveasreviewed" - title="vulnerability.transition.resolveasreviewed.description" - > - vulnerability.transition.resolveasreviewed - </SelectListItem> - <SelectListItem - item="confirm" - key="confirm" - title={ - <FormattedMessage - defaultMessage="issue.transition.confirm.description" - id="issue.transition.confirm.description" - values={ - Object { - "community_plug_link": <a - href="https://community.sonarsource.com/" - rel="noopener noreferrer" - target="_blank" - > - issue.transition.community_plug_link - </a>, - } - } - /> - } - > - issue.transition.confirm - </SelectListItem> - </SelectList> -</DropdownOverlay> -`; diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.tsx.snap deleted file mode 100644 index 1073a5fd23c..00000000000 --- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.tsx.snap +++ /dev/null @@ -1,51 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render tags popup correctly 1`] = ` -<DropdownOverlay> - <SelectList - currentItem="BUG" - items={ - Array [ - "BUG", - "VULNERABILITY", - "CODE_SMELL", - ] - } - onSelect={[MockFunction]} - > - <SelectListItem - className="display-flex-center" - item="BUG" - key="BUG" - > - <IssueTypeIcon - className="little-spacer-right" - query="BUG" - /> - issue.type.BUG - </SelectListItem> - <SelectListItem - className="display-flex-center" - item="VULNERABILITY" - key="VULNERABILITY" - > - <IssueTypeIcon - className="little-spacer-right" - query="VULNERABILITY" - /> - issue.type.VULNERABILITY - </SelectListItem> - <SelectListItem - className="display-flex-center" - item="CODE_SMELL" - key="CODE_SMELL" - > - <IssueTypeIcon - className="little-spacer-right" - query="CODE_SMELL" - /> - issue.type.CODE_SMELL - </SelectListItem> - </SelectList> -</DropdownOverlay> -`; diff --git a/server/sonar-web/src/main/js/types/issues.ts b/server/sonar-web/src/main/js/types/issues.ts index 33e71519a35..2e12d798318 100644 --- a/server/sonar-web/src/main/js/types/issues.ts +++ b/server/sonar-web/src/main/js/types/issues.ts @@ -57,6 +57,7 @@ export interface RawIssue { line?: number; project: string; rule: string; + resolution?: string; message?: string; severity: string; status: string; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index ee93a308e00..d337af4e3ba 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -820,11 +820,13 @@ issue.assign.formlink=Assign issue.assign.to_me=to me issue.quick_fix_available_with_sonarlint=Quick fix available in {link} issue.comment.add_comment=Add Comment +issue.comment.add_comment.cancel=Cancel adding comment issue.comment.formlink=Comment issue.comment.submit=Comment issue.comment.explain_why=Consider explaining why issue.comment.posted_on=Comment posted on issue.comment.edit=Edit comment +issue.comment.edit.cancel=Cancel editing comment issue.comment.delete=Delete comment issue.comment.delete_confirm_message=Do you want to delete this comment? issue.manual_vulnerability=Manual |