From 8da77e625548aca7508dbe23b614f7aa25791e47 Mon Sep 17 00:00:00 2001
From: Wouter Admiraal
Date: Mon, 6 Sep 2021 15:18:53 +0200
Subject: [PATCH] SONAR-13086 Improve activity and comment layout for Security
Hotspots
---
.../security-hotspots/__tests__/utils-test.ts | 8 +-
.../components/HotspotReviewHistory.tsx | 211 ++---
.../HotspotReviewHistoryAndComments.tsx | 110 +--
.../components/HotspotViewer.tsx | 29 +-
.../components/HotspotViewerRenderer.tsx | 12 +-
.../__tests__/HotspotReviewHistory-test.tsx | 175 +++-
.../HotspotReviewHistoryAndComments-test.tsx | 40 +-
.../__tests__/HotspotViewer-test.tsx | 12 +-
.../__tests__/HotspotViewerRenderer-test.tsx | 4 +-
.../HotspotReviewHistory-test.tsx.snap | 784 +++++++++++++-----
...spotReviewHistoryAndComments-test.tsx.snap | 371 ++++-----
.../__snapshots__/HotspotViewer-test.tsx.snap | 8 +-
.../HotspotViewerRenderer-test.tsx.snap | 18 -
.../main/js/apps/security-hotspots/utils.ts | 2 +-
.../src/main/js/helpers/mocks/issues.ts | 17 +
.../js/helpers/mocks/security-hotspots.ts | 14 +
.../resources/org/sonar/l10n/core.properties | 3 +-
17 files changed, 1136 insertions(+), 682 deletions(-)
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
index f62e89a980e..295b6bc6714 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/__tests__/utils-test.ts
@@ -191,14 +191,14 @@ describe('getHotspotReviewHistory', () => {
const reviewHistory = getHotspotReviewHistory(hotspot);
expect(reviewHistory.length).toBe(4);
- expect(reviewHistory[0]).toEqual(
+ expect(reviewHistory[3]).toEqual(
expect.objectContaining({
type: ReviewHistoryType.Creation,
date: hotspot.creationDate,
user: hotspot.authorUser
})
);
- expect(reviewHistory[1]).toEqual(
+ expect(reviewHistory[2]).toEqual(
expect.objectContaining({
type: ReviewHistoryType.Comment,
date: commentElement.createdAt,
@@ -206,7 +206,7 @@ describe('getHotspotReviewHistory', () => {
html: commentElement.htmlText
})
);
- expect(reviewHistory[2]).toEqual(
+ expect(reviewHistory[1]).toEqual(
expect.objectContaining({
type: ReviewHistoryType.Comment,
date: commentElement1.createdAt,
@@ -214,7 +214,7 @@ describe('getHotspotReviewHistory', () => {
html: commentElement1.htmlText
})
);
- expect(reviewHistory[3]).toEqual(
+ expect(reviewHistory[0]).toEqual(
expect.objectContaining({
type: ReviewHistoryType.Diff,
date: changelogElement.creationDate,
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx
index fd320921e0f..e3cf11a8a23 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx
@@ -19,7 +19,7 @@
*/
import * as classNames from 'classnames';
import * as React from 'react';
-import { Button, DeleteButton, EditButton } from '../../../components/controls/buttons';
+import { Button, ButtonLink, DeleteButton, EditButton } from '../../../components/controls/buttons';
import Dropdown, { DropdownOverlay } from '../../../components/controls/Dropdown';
import Toggler from '../../../components/controls/Toggler';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
@@ -36,112 +36,131 @@ export interface HotspotReviewHistoryProps {
hotspot: Hotspot;
onDeleteComment: (key: string) => void;
onEditComment: (key: string, comment: string) => void;
+ onShowFullHistory: () => void;
+ showFullHistory: boolean;
}
+export const MAX_RECENT_ACTIVITY = 5;
+
export default function HotspotReviewHistory(props: HotspotReviewHistoryProps) {
- const { hotspot } = props;
- const reviewHistory = getHotspotReviewHistory(hotspot);
+ const { hotspot, showFullHistory } = props;
+ const fullReviewHistory = getHotspotReviewHistory(hotspot);
const [editedCommentKey, setEditedCommentKey] = React.useState('');
+ const reviewHistory = showFullHistory
+ ? fullReviewHistory
+ : fullReviewHistory.slice(0, MAX_RECENT_ACTIVITY);
+
return (
<>
- {reviewHistory.map((historyElt, historyIndex) => {
- const { user, type, diffs, date, html, key, updatable, markdown } = historyElt;
- return (
- 0 })}
- key={historyIndex}>
-
- {user.name && (
- <>
-
-
- {user.active ? user.name : translateWithParameters('user.x_deleted', user.name)}
-
- {type === ReviewHistoryType.Creation && (
-
- {translate('hotspots.review_history.created')}
-
- )}
- {type === ReviewHistoryType.Comment && (
-
- {translate('hotspots.review_history.comment_added')}
-
- )}
-
-
- >
- )}
-
-
-
- {type === ReviewHistoryType.Diff && diffs && (
-
- {diffs.map((diff, diffIndex) => (
-
- ))}
+
+ {reviewHistory.map((historyElt, historyIndex) => {
+ const { user, type, diffs, date, html, key, updatable, markdown } = historyElt;
+ return (
+ - 0
+ })}
+ key={historyIndex}>
+
+ {user.name && (
+ <>
+
+
+ {user.active
+ ? user.name
+ : translateWithParameters('user.x_deleted', user.name)}
+
+ {type === ReviewHistoryType.Creation && (
+
+ {translate('hotspots.review_history.created')}
+
+ )}
+ {type === ReviewHistoryType.Comment && (
+
+ {translate('hotspots.review_history.comment_added')}
+
+ )}
+
-
+ >
+ )}
+
- )}
- {type === ReviewHistoryType.Comment && key && html && markdown && (
-
-
- {updatable && (
-
-
-
{
- setEditedCommentKey('');
- }}
- open={key === editedCommentKey}
+ {type === ReviewHistoryType.Diff && diffs && (
+
+ {diffs.map((diff, diffIndex) => (
+
+ ))}
+
+ )}
+
+ {type === ReviewHistoryType.Comment && key && html && markdown && (
+
+
+ {updatable && (
+
+
+ {
+ setEditedCommentKey('');
+ }}
+ open={key === editedCommentKey}
+ overlay={
+
+ setEditedCommentKey('')}
+ onCommentEditSubmit={comment => {
+ setEditedCommentKey('');
+ props.onEditComment(key, comment);
+ }}
+ />
+
+ }>
+ setEditedCommentKey(key)}
+ />
+
+
+
setEditedCommentKey('')}
overlay={
-
- setEditedCommentKey('')}
- onCommentEditSubmit={comment => {
- setEditedCommentKey('');
- props.onEditComment(key, comment);
- }}
- />
-
- }>
- setEditedCommentKey(key)}
- />
-
+
+
{translate('issue.comment.delete_confirm_message')}
+
+
+ }
+ overlayPlacement={PopupPlacement.BottomRight}>
+
+
-
setEditedCommentKey('')}
- overlay={
-
-
{translate('issue.comment.delete_confirm_message')}
-
-
- }
- overlayPlacement={PopupPlacement.BottomRight}>
-
-
-
- )}
-
- )}
-
- );
- })}
+ )}
+
+ )}
+
+ );
+ })}
+
+ {!showFullHistory && fullReviewHistory.length > MAX_RECENT_ACTIVITY && (
+
+ {translate('show_all')}
+
+ )}
>
);
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistoryAndComments.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistoryAndComments.tsx
index 42d9a2fbc92..b358850b3ca 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistoryAndComments.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistoryAndComments.tsx
@@ -17,7 +17,6 @@
* 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 classNames from 'classnames';
import * as React from 'react';
import {
commentSecurityHotspot,
@@ -25,7 +24,7 @@ import {
editSecurityHotspotComment
} from '../../../api/security-hotspots';
import FormattingTips from '../../../components/common/FormattingTips';
-import { Button, ResetButtonLink } from '../../../components/controls/buttons';
+import { Button } from '../../../components/controls/buttons';
import { translate } from '../../../helpers/l10n';
import { isLoggedIn } from '../../../helpers/users';
import { Hotspot } from '../../../types/security-hotspots';
@@ -35,28 +34,28 @@ interface Props {
currentUser: T.CurrentUser;
hotspot: Hotspot;
commentTextRef: React.RefObject
;
- commentVisible: boolean;
onCommentUpdate: () => void;
- onOpenComment: () => void;
- onCloseComment: () => void;
}
interface State {
comment: string;
+ showFullHistory: boolean;
}
export default class HotspotReviewHistoryAndComments extends React.PureComponent {
constructor(props: Props) {
super(props);
this.state = {
- comment: ''
+ comment: '',
+ showFullHistory: false
};
}
componentDidUpdate(prevProps: Props) {
- if (prevProps.hotspot !== this.props.hotspot) {
+ if (prevProps.hotspot.key !== this.props.hotspot.key) {
this.setState({
- comment: ''
+ comment: '',
+ showFullHistory: false
});
}
}
@@ -65,15 +64,9 @@ export default class HotspotReviewHistoryAndComments extends React.PureComponent
this.setState({ comment: event.target.value });
};
- handleCloseComment = () => {
- this.setState({ comment: '' });
- this.props.onCloseComment();
- };
-
handleSubmitComment = () => {
return commentSecurityHotspot(this.props.hotspot.key, this.state.comment).then(() => {
this.setState({ comment: '' });
- this.props.onCloseComment();
this.props.onCommentUpdate();
});
};
@@ -90,62 +83,49 @@ export default class HotspotReviewHistoryAndComments extends React.PureComponent
});
};
+ handleShowFullHistory = () => {
+ this.setState({ showFullHistory: true });
+ };
+
render() {
- const { currentUser, hotspot, commentTextRef, commentVisible } = this.props;
- const { comment } = this.state;
+ const { currentUser, hotspot, commentTextRef } = this.props;
+ const { comment, showFullHistory } = this.state;
return (
- <>
- {translate('hotspot.section.activity')}
-
-
-
- {isLoggedIn(currentUser) && (
- <>
-
-
+
+ {isLoggedIn(currentUser) && (
+
+
{translate('hotspots.comment.field')}
+
+
+
+
-
-
-
{translate('hotspots.comment.field')}
-
-
-
- >
- )}
-
- >
+
+
+ )}
+
+
{translate('hotspot.section.activity')}
+
+
+
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
index 1b8fa64017b..300d2e53178 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx
@@ -43,7 +43,6 @@ interface State {
hotspot?: Hotspot;
lastStatusChangedTo?: HotspotStatusOption;
loading: boolean;
- commentVisible: boolean;
showStatusUpdateSuccessModal: boolean;
}
@@ -55,7 +54,7 @@ export default class HotspotViewer extends React.PureComponent
{
constructor(props: Props) {
super(props);
this.commentTextRef = React.createRef();
- this.state = { loading: false, commentVisible: false, showStatusUpdateSuccessModal: false };
+ this.state = { loading: false, showStatusUpdateSuccessModal: false };
}
componentDidMount() {
@@ -63,11 +62,11 @@ export default class HotspotViewer extends React.PureComponent {
this.fetchHotspot();
}
- componentDidUpdate(prevProps: Props, prevState: State) {
+ componentDidUpdate(prevProps: Props) {
if (prevProps.hotspotKey !== this.props.hotspotKey) {
this.fetchHotspot();
}
- if (this.commentTextRef.current && !prevState.commentVisible && this.state.commentVisible) {
+ if (this.commentTextRef.current) {
this.commentTextRef.current.focus({ preventScroll: true });
}
}
@@ -99,23 +98,15 @@ export default class HotspotViewer extends React.PureComponent {
}
};
- handleOpenComment = () => {
- this.setState({ commentVisible: true });
+ handleScrollToCommentForm = () => {
if (this.commentTextRef.current) {
- // Edge case when the comment is already open and unfocus.
this.commentTextRef.current.focus({ preventScroll: true });
- }
- if (this.commentTextRef.current) {
scrollToElement(this.commentTextRef.current, {
bottomOffset: 100
});
}
};
- handleCloseComment = () => {
- this.setState({ commentVisible: false });
- };
-
handleSwitchFilterToStatusOfUpdatedHotspot = () => {
const { lastStatusChangedTo } = this.state;
if (lastStatusChangedTo) {
@@ -129,28 +120,20 @@ export default class HotspotViewer extends React.PureComponent {
render() {
const { branchLike, component, hotspotsReviewedMeasure, securityCategories } = this.props;
- const {
- hotspot,
- lastStatusChangedTo,
- loading,
- commentVisible,
- showStatusUpdateSuccessModal
- } = this.state;
+ const { hotspot, lastStatusChangedTo, loading, showStatusUpdateSuccessModal } = this.state;
return (
;
- onOpenComment: () => void;
- onCloseComment: () => void;
onCloseStatusUpdateSuccessModal: () => void;
onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise;
+ onShowCommentForm: () => void;
onSwitchFilterToStatusOfUpdatedHotspot: () => void;
showStatusUpdateSuccessModal: boolean;
securityCategories: T.StandardSecurityCategories;
@@ -74,8 +72,7 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
lastStatusChangedTo,
showStatusUpdateSuccessModal,
securityCategories,
- commentTextRef,
- commentVisible
+ commentTextRef
} = props;
const permalink = getPathUrlAsString(
@@ -115,7 +112,7 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
{isLoggedIn(currentUser) && (
<>
-
@@ -175,12 +172,9 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) {
)}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotReviewHistory-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotReviewHistory-test.tsx
index 143dfb93abf..8eda5958c98 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotReviewHistory-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotReviewHistory-test.tsx
@@ -19,57 +19,152 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { mockHotspot } from '../../../../helpers/mocks/security-hotspots';
+import { Button, EditButton } from '../../../../components/controls/buttons';
+import Dropdown, { DropdownOverlay } from '../../../../components/controls/Dropdown';
+import Toggler from '../../../../components/controls/Toggler';
+import { mockIssueChangelog } from '../../../../helpers/mocks/issues';
+import { mockHotspot, mockHotspotComment } from '../../../../helpers/mocks/security-hotspots';
import { mockUser } from '../../../../helpers/testMocks';
+import HotspotCommentPopup from '../HotspotCommentPopup';
import HotspotReviewHistory, { HotspotReviewHistoryProps } from '../HotspotReviewHistory';
it('should render correctly', () => {
- const wrapper = shallowRender();
- expect(wrapper).toMatchSnapshot();
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender({ showFullHistory: true })).toMatchSnapshot('show full list');
+ expect(
+ shallowRender({ showFullHistory: true })
+ .find(Toggler)
+ .props().overlay
+ ).toMatchSnapshot('edit comment overlay');
+ expect(
+ shallowRender({ showFullHistory: true })
+ .find(Dropdown)
+ .props().overlay
+ ).toMatchSnapshot('delete comment overlay');
});
-function shallowRender(props?: Partial) {
- const changelogElement: T.IssueChangelog = {
- creationDate: '2018-10-01',
- isUserActive: true,
- user: 'me',
- userName: 'me-name',
- diffs: [
- {
- key: 'assign',
- newValue: 'me',
- oldValue: 'him'
- }
- ]
- };
- const commentElement = {
- key: 'comment-1',
- createdAt: '2018-09-10',
- htmlText: 'TEST',
- markdown: '*TEST*',
- updatable: true,
- login: 'dude-1',
- user: mockUser({ login: 'dude-1' })
- };
- const commentElement1 = {
- key: 'comment-2',
- createdAt: '2018-09-11',
- htmlText: 'TEST',
- markdown: '*TEST*',
- updatable: false,
- login: 'dude-2',
- user: mockUser({ login: 'dude-2' })
- };
- const hotspot = mockHotspot({
- creationDate: '2018-09-01',
- changelog: [changelogElement],
- comment: [commentElement, commentElement1]
+it('should correctly handle comment updating', () => {
+ return new Promise((resolve, reject) => {
+ const setEditedCommentKey = jest.fn();
+ jest.spyOn(React, 'useState').mockImplementationOnce(() => ['', setEditedCommentKey]);
+
+ const onEditComment = jest.fn();
+ const wrapper = shallowRender({ onEditComment, showFullHistory: true });
+
+ // Closing the Toggler sets the edited key back to an empty string.
+ wrapper
+ .find(Toggler)
+ .at(0)
+ .props()
+ .onRequestClose();
+ expect(setEditedCommentKey).toBeCalledWith('');
+
+ const editOnClick = wrapper
+ .find(EditButton)
+ .at(0)
+ .props().onClick;
+ if (!editOnClick) {
+ reject();
+ return;
+ }
+
+ // Clicking on the EditButton correctly flags the comment for editing.
+ editOnClick();
+ expect(setEditedCommentKey).toHaveBeenLastCalledWith('comment-1');
+
+ // Cancelling an edit sets the edited key back to an empty string
+ const dropdownOverlay = shallow(
+ wrapper
+ .find(Toggler)
+ .at(0)
+ .props().overlay as React.ReactElement
+ );
+ dropdownOverlay
+ .find(HotspotCommentPopup)
+ .props()
+ .onCancelEdit();
+ expect(setEditedCommentKey).toHaveBeenLastCalledWith('');
+
+ // Updating the comment sets the edited key back to an empty string, and calls the
+ // prop to update the comment value.
+ dropdownOverlay
+ .find(HotspotCommentPopup)
+ .props()
+ .onCommentEditSubmit('comment');
+ expect(onEditComment).toHaveBeenLastCalledWith('comment-1', 'comment');
+ expect(setEditedCommentKey).toHaveBeenLastCalledWith('');
+ expect(setEditedCommentKey).toHaveBeenCalledTimes(4);
+
+ resolve();
});
+});
+
+it('should correctly handle comment deleting', () => {
+ return new Promise((resolve, reject) => {
+ const setEditedCommentKey = jest.fn();
+ jest.spyOn(React, 'useState').mockImplementationOnce(() => ['', setEditedCommentKey]);
+
+ const onDeleteComment = jest.fn();
+ const wrapper = shallowRender({ onDeleteComment, showFullHistory: true });
+
+ // Opening the deletion Dropdown sets the edited key back to an empty string.
+ const dropdownOnOpen = wrapper
+ .find(Dropdown)
+ .at(0)
+ .props().onOpen;
+ if (!dropdownOnOpen) {
+ reject();
+ return;
+ }
+ dropdownOnOpen();
+ expect(setEditedCommentKey).toHaveBeenLastCalledWith('');
+
+ // Confirming deletion calls the prop to delete the comment.
+ const dropdownOverlay = shallow(
+ wrapper
+ .find(Dropdown)
+ .at(0)
+ .props().overlay as React.ReactElement
+ );
+ const deleteButtonOnClick = dropdownOverlay.find(Button).props().onClick;
+ if (!deleteButtonOnClick) {
+ reject();
+ return;
+ }
+
+ deleteButtonOnClick();
+ expect(onDeleteComment).toBeCalledWith('comment-1');
+
+ resolve();
+ });
+});
+
+function shallowRender(props?: Partial) {
return shallow(
);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotReviewHistoryAndComments-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotReviewHistoryAndComments-test.tsx
index 27f02fac853..b20d07a2e33 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotReviewHistoryAndComments-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotReviewHistoryAndComments-test.tsx
@@ -50,16 +50,10 @@ it('should render correctly without user', () => {
expect(wrapper).toMatchSnapshot();
});
-it('should open comment form', () => {
- const wrapper = shallowRender();
- wrapper.find('#hotspot-comment-box-display').simulate('click');
- expect(wrapper.instance().props.onOpenComment).toHaveBeenCalled();
-});
-
it('should submit comment', async () => {
const mockApi = commentSecurityHotspot as jest.Mock;
const hotspot = mockHotspot();
- const wrapper = shallowRender({ hotspot, commentVisible: true });
+ const wrapper = shallowRender({ hotspot });
mockApi.mockClear();
wrapper.instance().setState({ comment: 'Comment' });
@@ -68,26 +62,11 @@ it('should submit comment', async () => {
expect(mockApi).toHaveBeenCalledWith(hotspot.key, 'Comment');
expect(wrapper.state().comment).toBe('');
- expect(wrapper.instance().props.onCloseComment).toHaveBeenCalledTimes(1);
expect(wrapper.instance().props.onCommentUpdate).toHaveBeenCalledTimes(1);
});
-it('should cancel comment', () => {
- const mockApi = commentSecurityHotspot as jest.Mock;
- const hotspot = mockHotspot();
- const wrapper = shallowRender({ hotspot, commentVisible: true });
- wrapper.instance().setState({ comment: 'Comment' });
- mockApi.mockClear();
-
- wrapper.find('#hotspot-comment-box-cancel').simulate('click');
-
- expect(mockApi).not.toHaveBeenCalled();
- expect(wrapper.state().comment).toBe('');
- expect(wrapper.instance().props.onCloseComment).toHaveBeenCalledTimes(1);
-});
-
it('should change comment', () => {
- const wrapper = shallowRender({ commentVisible: true });
+ const wrapper = shallowRender();
wrapper.instance().setState({ comment: 'Comment' });
wrapper.find('textarea').simulate('change', { target: { value: 'Foo' } });
@@ -97,7 +76,7 @@ it('should change comment', () => {
it('should reset on change hotspot', () => {
const wrapper = shallowRender();
wrapper.setState({ comment: 'NOP' });
- wrapper.setProps({ hotspot: mockHotspot() });
+ wrapper.setProps({ hotspot: mockHotspot({ key: 'other-hotspot' }) });
expect(wrapper.state().comment).toBe('');
});
@@ -122,16 +101,23 @@ it('should edit comment', async () => {
expect(wrapper.instance().props.onCommentUpdate).toBeCalledTimes(1);
});
+it('should correctly toggle the show full history state', () => {
+ const wrapper = shallowRender();
+ expect(wrapper.state().showFullHistory).toBe(false);
+ wrapper
+ .find(HotspotReviewHistory)
+ .props()
+ .onShowFullHistory();
+ expect(wrapper.state().showFullHistory).toBe(true);
+});
+
function shallowRender(props?: Partial) {
return shallow(
);
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx
index 43410b47afe..cff7a9c5058 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewer-test.tsx
@@ -104,27 +104,19 @@ it('should NOT refresh hotspot list on assignee/comment updates', () => {
expect(onUpdateHotspot).not.toHaveBeenCalled();
});
-it('should open comment form when scroll to comment', () => {
+it('should scroll to comment form', () => {
const wrapper = shallowRender();
const mockTextRef = ({ current: { focus: jest.fn() } } as any) as React.RefObject<
HTMLTextAreaElement
>;
wrapper.instance().commentTextRef = mockTextRef;
- wrapper.find(HotspotViewerRenderer).simulate('openComment');
+ wrapper.find(HotspotViewerRenderer).simulate('showCommentForm');
- expect(wrapper.state().commentVisible).toBe(true);
expect(mockTextRef.current?.focus).toHaveBeenCalled();
expect(scrollToElement).toHaveBeenCalledWith(mockTextRef.current, expect.anything());
});
-it('should close comment', () => {
- const wrapper = shallowRender();
- wrapper.setState({ commentVisible: true });
- wrapper.find(HotspotViewerRenderer).simulate('closeComment');
- expect(wrapper.state().commentVisible).toBe(false);
-});
-
it('should reset loading even on fetch error', async () => {
const mockGetHostpot = getSecurityHotspotDetails as jest.Mock;
mockGetHostpot.mockRejectedValueOnce({});
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx
index 1a681d315fc..1b97fd6395e 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/HotspotViewerRenderer-test.tsx
@@ -69,16 +69,14 @@ function shallowRender(props?: Partial) {
branchLike={mockBranch()}
component={mockComponent()}
commentTextRef={React.createRef()}
- commentVisible={false}
currentUser={mockCurrentUser()}
hotspot={mockHotspot()}
hotspotsReviewedMeasure="75"
lastStatusChangedTo={HotspotStatusOption.FIXED}
loading={false}
- onCloseComment={jest.fn()}
onCloseStatusUpdateSuccessModal={jest.fn()}
- onOpenComment={jest.fn()}
onSwitchFilterToStatusOfUpdatedHotspot={jest.fn()}
+ onShowCommentForm={jest.fn()}
onUpdateHotspot={jest.fn()}
securityCategories={{ 'sql-injection': { title: 'SQL injection' } }}
showStatusUpdateSuccessModal={false}
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistory-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistory-test.tsx.snap
index c70652a47a0..2475a3723b9 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistory-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistory-test.tsx.snap
@@ -1,208 +1,620 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render correctly 1`] = `
+exports[`should render correctly: default 1`] = `
-
-
+
-
-
- John Doe
-
-
- hotspots.review_history.created
-
-
- -
-
-
-
-
-
-
+
+
+ Luke Skywalker
+
+
+ -
+
+
+
+
+
+
+
+
-
-
- John Doe
-
-
- hotspots.review_history.comment_added
-
-
- -
-
-
-
-
+
+
+ Luke Skywalker
+
+
+ -
+
+
+
+
+
+
+
+
TEST",
+ className="display-flex-center"
+ >
+
+
+ John Doe
+
+
+ hotspots.review_history.comment_added
+
+
+ -
+
+
+
+
+
TEST",
+ }
}
- }
- />
-
+ />
+
+
+
+
+
+
+ John Doe
+
+
+ hotspots.review_history.comment_added
+
+
+ -
+
+
+
+
TEST",
+ }
+ }
+ />
+
+
+
+
+
+
+ user.x_deleted.John Doe
+
+
-
-
-
+ hotspots.review_history.comment_added
+
+
+ -
+
+
+
+
+
TEST",
}
- >
-
-
-
-
-
- issue.comment.delete_confirm_message
-
-
- delete
-
-
}
- overlayPlacement="bottom-right"
- >
-
-
-
-
-
-
+
+
+
+
-
+
+`;
+
+exports[`should render correctly: delete comment overlay 1`] = `
+
+
+ issue.comment.delete_confirm_message
+
+
+ delete
+
+
+`;
+
+exports[`should render correctly: edit comment overlay 1`] = `
+
+
+
+`;
+
+exports[`should render correctly: show full list 1`] = `
+
+
+ -
-
-
- John Doe
-
-
- hotspots.review_history.comment_added
-
-
- -
-
-
-
-
+
+
+ Luke Skywalker
+
+
+ -
+
+
+
+
+
+
+
+
TEST",
+ className="display-flex-center"
+ >
+
+
+ Luke Skywalker
+
+
+ -
+
+
+
+
+
-
-
-
+
+
+
+
+
+ John Doe
+
+
+ hotspots.review_history.comment_added
+
+
+ -
+
+
+
+
+
TEST",
+ }
+ }
+ />
+
+
+
+
+
+
+ John Doe
+
+
+ hotspots.review_history.comment_added
+
+
+ -
+
+
+
+
+
TEST",
+ }
+ }
+ />
+
+
+
+
+
+
+ user.x_deleted.John Doe
+
+
+ hotspots.review_history.comment_added
+
+
+ -
+
+
+
+
+
TEST",
+ }
+ }
+ />
+
+
+
-
-
- me-name
-
-
- -
-
-
-
-
+
+
+ john.doe
+
+
+ hotspots.review_history.comment_added
+
+
+ -
+
+
+
+
+
TEST",
+ }
+ }
+ />
+
+
+
-
+
+
+ John Doe
+
+
+ hotspots.review_history.comment_added
+
+
+ -
+
+
+
+
+
TEST",
+ }
}
- }
- key="0"
- />
-
-
+ />
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+ issue.comment.delete_confirm_message
+
+
+ delete
+
+
+ }
+ overlayPlacement="bottom-right"
+ >
+
+
+
+
+
+
+
+
+
+ John Doe
+
+
+ hotspots.review_history.created
+
+
+ -
+
+
+
+
+
`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistoryAndComments-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistoryAndComments-test.tsx.snap
index e576a075c66..ee97c8ccccc 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistoryAndComments-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistoryAndComments-test.tsx.snap
@@ -1,225 +1,210 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render correctly 1`] = `
-
-
- hotspot.section.activity
-
+
-
+ hotspots.comment.field
+
+
+
+
+
+ hotspot.section.activity
+
+ This a strong message about fixing !
",
+ "key": "squid:S2077",
+ "name": "That rule",
+ "riskDescription": "This a strong message about risk !
",
+ "securityCategory": "sql-injection",
+ "vulnerabilityDescription": "This a strong message about vulnerability !
",
+ "vulnerabilityProbability": "HIGH",
+ },
+ "status": "REVIEWED",
+ "textRange": Object {
+ "endLine": 142,
+ "endOffset": 83,
+ "startLine": 142,
+ "startOffset": 26,
+ },
+ "updateDate": "2013-05-13T17:55:42+0200",
+ "users": Array [
+ Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
- "author": "author",
- "authorUser": Object {
+ Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
- "canChangeStatus": true,
- "changelog": Array [],
- "comment": Array [],
- "component": Object {
- "key": "hotspot-component",
- "longName": "Hotspot component long name",
- "name": "Hotspot Component",
- "path": "path/to/component",
- "qualifier": "FIL",
- },
- "creationDate": "2013-05-13T17:55:41+0200",
- "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
- "line": 142,
- "message": "'3' is a magic number.",
- "project": Object {
- "key": "hotspot-component",
- "longName": "Hotspot component long name",
- "name": "Hotspot Component",
- "path": "path/to/component",
- "qualifier": "TRK",
- },
- "resolution": "FIXED",
- "rule": Object {
- "fixRecommendations": "This a strong message about fixing !
",
- "key": "squid:S2077",
- "name": "That rule",
- "riskDescription": "This a strong message about risk !
",
- "securityCategory": "sql-injection",
- "vulnerabilityDescription": "This a strong message about vulnerability !
",
- "vulnerabilityProbability": "HIGH",
- },
- "status": "REVIEWED",
- "textRange": Object {
- "endLine": 142,
- "endOffset": 83,
- "startLine": 142,
- "startOffset": 26,
- },
- "updateDate": "2013-05-13T17:55:42+0200",
- "users": Array [
- Object {
- "active": true,
- "local": true,
- "login": "assignee",
- "name": "John Doe",
- },
- Object {
- "active": true,
- "local": true,
- "login": "author",
- "name": "John Doe",
- },
- ],
- }
+ ],
}
- onDeleteComment={[Function]}
- onEditComment={[Function]}
- />
-
-
-
-
-
- hotspots.comment.field
-
-
-
-
-
-
-
+ }
+ onDeleteComment={[Function]}
+ onEditComment={[Function]}
+ onShowFullHistory={[Function]}
+ showFullHistory={false}
+ />
+
`;
exports[`should render correctly without user 1`] = `
-
-
- hotspot.section.activity
-
-
+
-
+ This a strong message about fixing !",
+ "key": "squid:S2077",
+ "name": "That rule",
+ "riskDescription": "This a strong message about risk !
",
+ "securityCategory": "sql-injection",
+ "vulnerabilityDescription": "This a strong message about vulnerability !
",
+ "vulnerabilityProbability": "HIGH",
+ },
+ "status": "REVIEWED",
+ "textRange": Object {
+ "endLine": 142,
+ "endOffset": 83,
+ "startLine": 142,
+ "startOffset": 26,
+ },
+ "updateDate": "2013-05-13T17:55:42+0200",
+ "users": Array [
+ Object {
"active": true,
"local": true,
"login": "assignee",
"name": "John Doe",
},
- "author": "author",
- "authorUser": Object {
+ Object {
"active": true,
"local": true,
"login": "author",
"name": "John Doe",
},
- "canChangeStatus": true,
- "changelog": Array [],
- "comment": Array [],
- "component": Object {
- "key": "hotspot-component",
- "longName": "Hotspot component long name",
- "name": "Hotspot Component",
- "path": "path/to/component",
- "qualifier": "FIL",
- },
- "creationDate": "2013-05-13T17:55:41+0200",
- "key": "01fc972e-2a3c-433e-bcae-0bd7f88f5123",
- "line": 142,
- "message": "'3' is a magic number.",
- "project": Object {
- "key": "hotspot-component",
- "longName": "Hotspot component long name",
- "name": "Hotspot Component",
- "path": "path/to/component",
- "qualifier": "TRK",
- },
- "resolution": "FIXED",
- "rule": Object {
- "fixRecommendations": "This a strong message about fixing !
",
- "key": "squid:S2077",
- "name": "That rule",
- "riskDescription": "This a strong message about risk !
",
- "securityCategory": "sql-injection",
- "vulnerabilityDescription": "This a strong message about vulnerability !
",
- "vulnerabilityProbability": "HIGH",
- },
- "status": "REVIEWED",
- "textRange": Object {
- "endLine": 142,
- "endOffset": 83,
- "startLine": 142,
- "startOffset": 26,
- },
- "updateDate": "2013-05-13T17:55:42+0200",
- "users": Array [
- Object {
- "active": true,
- "local": true,
- "login": "assignee",
- "name": "John Doe",
- },
- Object {
- "active": true,
- "local": true,
- "login": "author",
- "name": "John Doe",
- },
- ],
- }
+ ],
}
- onDeleteComment={[Function]}
- onEditComment={[Function]}
- />
-
-
+ }
+ onDeleteComment={[Function]}
+ onEditComment={[Function]}
+ onShowFullHistory={[Function]}
+ showFullHistory={false}
+ />
+
`;
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
index 44e0fda6ace..e9d02356247 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewer-test.tsx.snap
@@ -7,7 +7,6 @@ exports[`should render correctly 1`] = `
"current": null,
}
}
- commentVisible={false}
component={
Object {
"breadcrumbs": Array [],
@@ -31,9 +30,8 @@ exports[`should render correctly 1`] = `
}
}
loading={true}
- onCloseComment={[Function]}
onCloseStatusUpdateSuccessModal={[Function]}
- onOpenComment={[Function]}
+ onShowCommentForm={[Function]}
onSwitchFilterToStatusOfUpdatedHotspot={[Function]}
onUpdateHotspot={[Function]}
securityCategories={
@@ -54,7 +52,6 @@ exports[`should render correctly 2`] = `
"current": null,
}
}
- commentVisible={false}
component={
Object {
"breadcrumbs": Array [],
@@ -83,9 +80,8 @@ exports[`should render correctly 2`] = `
}
}
loading={false}
- onCloseComment={[Function]}
onCloseStatusUpdateSuccessModal={[Function]}
- onOpenComment={[Function]}
+ onShowCommentForm={[Function]}
onSwitchFilterToStatusOfUpdatedHotspot={[Function]}
onUpdateHotspot={[Function]}
securityCategories={
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
index 1180deb78c9..ea3ef767b32 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerRenderer-test.tsx.snap
@@ -451,7 +451,6 @@ exports[`should render correctly: anonymous user 1`] = `
"current": null,
}
}
- commentVisible={false}
currentUser={
Object {
"isLoggedIn": false,
@@ -528,9 +527,7 @@ exports[`should render correctly: anonymous user 1`] = `
],
}
}
- onCloseComment={[MockFunction]}
onCommentUpdate={[MockFunction]}
- onOpenComment={[MockFunction]}
/>
@@ -987,7 +984,6 @@ exports[`should render correctly: assignee without name 1`] = `
"current": null,
}
}
- commentVisible={false}
currentUser={
Object {
"isLoggedIn": false,
@@ -1064,9 +1060,7 @@ exports[`should render correctly: assignee without name 1`] = `
],
}
}
- onCloseComment={[MockFunction]}
onCommentUpdate={[MockFunction]}
- onOpenComment={[MockFunction]}
/>
@@ -1523,7 +1517,6 @@ exports[`should render correctly: default 1`] = `
"current": null,
}
}
- commentVisible={false}
currentUser={
Object {
"isLoggedIn": false,
@@ -1600,9 +1593,7 @@ exports[`should render correctly: default 1`] = `
],
}
}
- onCloseComment={[MockFunction]}
onCommentUpdate={[MockFunction]}
- onOpenComment={[MockFunction]}
/>
@@ -2059,7 +2050,6 @@ exports[`should render correctly: deleted assignee 1`] = `
"current": null,
}
}
- commentVisible={false}
currentUser={
Object {
"isLoggedIn": false,
@@ -2136,9 +2126,7 @@ exports[`should render correctly: deleted assignee 1`] = `
],
}
}
- onCloseComment={[MockFunction]}
onCommentUpdate={[MockFunction]}
- onOpenComment={[MockFunction]}
/>
@@ -2608,7 +2596,6 @@ exports[`should render correctly: show success modal 1`] = `
"current": null,
}
}
- commentVisible={false}
currentUser={
Object {
"isLoggedIn": false,
@@ -2685,9 +2672,7 @@ exports[`should render correctly: show success modal 1`] = `
],
}
}
- onCloseComment={[MockFunction]}
onCommentUpdate={[MockFunction]}
- onOpenComment={[MockFunction]}
/>
@@ -3144,7 +3129,6 @@ exports[`should render correctly: unassigned 1`] = `
"current": null,
}
}
- commentVisible={false}
currentUser={
Object {
"isLoggedIn": false,
@@ -3221,9 +3205,7 @@ exports[`should render correctly: unassigned 1`] = `
],
}
}
- onCloseComment={[MockFunction]}
onCommentUpdate={[MockFunction]}
- onOpenComment={[MockFunction]}
/>
diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
index 7f7db63d9dd..ecde73f591e 100644
--- a/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
+++ b/server/sonar-web/src/main/js/apps/security-hotspots/utils.ts
@@ -148,7 +148,7 @@ export function getHotspotReviewHistory(hotspot: Hotspot): ReviewHistoryElement[
);
}
- return sortBy(history, elt => elt.date);
+ return sortBy(history, elt => elt.date).reverse();
}
const STATUS_AND_RESOLUTION_TO_STATUS_OPTION = {
diff --git a/server/sonar-web/src/main/js/helpers/mocks/issues.ts b/server/sonar-web/src/main/js/helpers/mocks/issues.ts
index 9c128954725..034eb0af139 100644
--- a/server/sonar-web/src/main/js/helpers/mocks/issues.ts
+++ b/server/sonar-web/src/main/js/helpers/mocks/issues.ts
@@ -26,3 +26,20 @@ export function mockReferencedRule(overrides: Partial = {}): Ref
...overrides
};
}
+
+export function mockIssueChangelog(overrides: Partial = {}): T.IssueChangelog {
+ return {
+ creationDate: '2018-10-01',
+ isUserActive: true,
+ user: 'luke.skywalker',
+ userName: 'Luke Skywalker',
+ diffs: [
+ {
+ key: 'assign',
+ newValue: 'darth.vader',
+ oldValue: 'luke.skywalker'
+ }
+ ],
+ ...overrides
+ };
+}
diff --git a/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts b/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts
index 74a2b80a647..60d2727fd03 100644
--- a/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts
+++ b/server/sonar-web/src/main/js/helpers/mocks/security-hotspots.ts
@@ -21,6 +21,7 @@ import { ComponentQualifier } from '../../types/component';
import { Standards } from '../../types/security';
import {
Hotspot,
+ HotspotComment,
HotspotComponent,
HotspotResolution,
HotspotRule,
@@ -94,6 +95,19 @@ export function mockHotspotComponent(overrides?: Partial): Hot
};
}
+export function mockHotspotComment(overrides?: Partial): HotspotComment {
+ return {
+ key: 'comment-1',
+ createdAt: '2018-09-10',
+ htmlText: 'TEST',
+ markdown: '*TEST*',
+ updatable: false,
+ login: 'dude-2',
+ user: mockUser({ login: 'dude-2' }),
+ ...overrides
+ };
+}
+
export function mockHotspotRule(overrides?: Partial): HotspotRule {
return {
key: 'squid:S2077',
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 ba0dde5de24..dd22228b224 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -184,6 +184,7 @@ set=Set
set_up=Set Up
severity=Severity
shared=Shared
+show_all=Show all
start_date=Start Date
x_show={0} shown
x_selected={0} selected
@@ -784,7 +785,7 @@ hotspot.filters.period.since_leak_period=New code
hotspot.filters.period.overall=Overall code
hotspot.filters.status.safe=Reviewed as safe
hotspot.filters.show_all=Show all hotspots
-hotspot.section.activity=Activity:
+hotspot.section.activity=Recent activity:
hotspots.reviewed.tooltip=Percentage of Security Hotspots reviewed (fixed or safe) among all non-closed Security Hotspots.
hotspots.review_hotspot=Review Hotspot
--
2.39.5