diff options
Diffstat (limited to 'server/sonar-web/src/main/js/apps/security-hotspots')
14 files changed, 1103 insertions, 681 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 ( - <div - className={classNames('padded', { 'bordered-top': historyIndex > 0 })} - key={historyIndex}> - <div className="display-flex-center"> - {user.name && ( - <> - <Avatar - className="little-spacer-right" - hash={user.avatar} - name={user.name} - size={20} - /> - <strong> - {user.active ? user.name : translateWithParameters('user.x_deleted', user.name)} - </strong> - {type === ReviewHistoryType.Creation && ( - <span className="little-spacer-left"> - {translate('hotspots.review_history.created')} - </span> - )} - {type === ReviewHistoryType.Comment && ( - <span className="little-spacer-left"> - {translate('hotspots.review_history.comment_added')} - </span> - )} - <span className="little-spacer-left little-spacer-right">-</span> - </> - )} - <DateTimeFormatter date={date} /> - </div> - - {type === ReviewHistoryType.Diff && diffs && ( - <div className="spacer-top"> - {diffs.map((diff, diffIndex) => ( - <IssueChangelogDiff diff={diff} key={diffIndex} /> - ))} + <ul> + {reviewHistory.map((historyElt, historyIndex) => { + const { user, type, diffs, date, html, key, updatable, markdown } = historyElt; + return ( + <li + className={classNames('padded-top padded-bottom', { + 'bordered-top': historyIndex > 0 + })} + key={historyIndex}> + <div className="display-flex-center"> + {user.name && ( + <> + <Avatar + className="little-spacer-right" + hash={user.avatar} + name={user.name} + size={20} + /> + <strong> + {user.active + ? user.name + : translateWithParameters('user.x_deleted', user.name)} + </strong> + {type === ReviewHistoryType.Creation && ( + <span className="little-spacer-left"> + {translate('hotspots.review_history.created')} + </span> + )} + {type === ReviewHistoryType.Comment && ( + <span className="little-spacer-left"> + {translate('hotspots.review_history.comment_added')} + </span> + )} + <span className="little-spacer-left little-spacer-right">-</span> + </> + )} + <DateTimeFormatter date={date} /> </div> - )} - {type === ReviewHistoryType.Comment && key && html && markdown && ( - <div className="spacer-top display-flex-space-between"> - <div - className="markdown" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeString(html) }} - /> - {updatable && ( - <div> - <div className="dropdown"> - <Toggler - onRequestClose={() => { - setEditedCommentKey(''); - }} - open={key === editedCommentKey} + {type === ReviewHistoryType.Diff && diffs && ( + <div className="spacer-top"> + {diffs.map((diff, diffIndex) => ( + <IssueChangelogDiff diff={diff} key={diffIndex} /> + ))} + </div> + )} + + {type === ReviewHistoryType.Comment && key && html && markdown && ( + <div className="spacer-top display-flex-space-between"> + <div + className="markdown" + // eslint-disable-next-line react/no-danger + dangerouslySetInnerHTML={{ __html: sanitizeString(html) }} + /> + {updatable && ( + <div> + <div className="dropdown"> + <Toggler + onRequestClose={() => { + setEditedCommentKey(''); + }} + open={key === editedCommentKey} + overlay={ + <DropdownOverlay placement={PopupPlacement.BottomRight}> + <HotspotCommentPopup + markdownComment={markdown} + onCancelEdit={() => setEditedCommentKey('')} + onCommentEditSubmit={comment => { + setEditedCommentKey(''); + props.onEditComment(key, comment); + }} + /> + </DropdownOverlay> + }> + <EditButton + className="button-small" + onClick={() => setEditedCommentKey(key)} + /> + </Toggler> + </div> + <Dropdown + onOpen={() => setEditedCommentKey('')} overlay={ - <DropdownOverlay placement={PopupPlacement.BottomRight}> - <HotspotCommentPopup - markdownComment={markdown} - onCancelEdit={() => setEditedCommentKey('')} - onCommentEditSubmit={comment => { - setEditedCommentKey(''); - props.onEditComment(key, comment); - }} - /> - </DropdownOverlay> - }> - <EditButton - className="button-small" - onClick={() => setEditedCommentKey(key)} - /> - </Toggler> + <div className="padded abs-width-150"> + <p>{translate('issue.comment.delete_confirm_message')}</p> + <Button + className="button-red big-spacer-top pull-right" + onClick={() => props.onDeleteComment(key)}> + {translate('delete')} + </Button> + </div> + } + overlayPlacement={PopupPlacement.BottomRight}> + <DeleteButton className="button-small" /> + </Dropdown> </div> - <Dropdown - onOpen={() => setEditedCommentKey('')} - overlay={ - <div className="padded abs-width-150"> - <p>{translate('issue.comment.delete_confirm_message')}</p> - <Button - className="button-red big-spacer-top pull-right" - onClick={() => props.onDeleteComment(key)}> - {translate('delete')} - </Button> - </div> - } - overlayPlacement={PopupPlacement.BottomRight}> - <DeleteButton className="button-small" /> - </Dropdown> - </div> - )} - </div> - )} - </div> - ); - })} + )} + </div> + )} + </li> + ); + })} + </ul> + {!showFullHistory && fullReviewHistory.length > MAX_RECENT_ACTIVITY && ( + <ButtonLink className="spacer-top" onClick={props.onShowFullHistory}> + {translate('show_all')} + </ButtonLink> + )} </> ); } 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<HTMLTextAreaElement>; - commentVisible: boolean; onCommentUpdate: () => void; - onOpenComment: () => void; - onCloseComment: () => void; } interface State { comment: string; + showFullHistory: boolean; } export default class HotspotReviewHistoryAndComments extends React.PureComponent<Props, State> { 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 ( - <> - <h1>{translate('hotspot.section.activity')}</h1> - <div className="padded it__hs-review-history"> - <HotspotReviewHistory - hotspot={hotspot} - onDeleteComment={this.handleDeleteComment} - onEditComment={this.handleEditComment} - /> - - {isLoggedIn(currentUser) && ( - <> - <hr /> - <div className="big-spacer-top"> + <div className="padded it__hs-review-history"> + {isLoggedIn(currentUser) && ( + <div className="big-spacer-top"> + <div className="little-spacer-bottom">{translate('hotspots.comment.field')}</div> + <textarea + className="form-field fixed-width width-100 spacer-bottom" + onChange={this.handleCommentChange} + ref={commentTextRef} + rows={2} + value={comment} + /> + <div className="display-flex-space-between display-flex-center "> + <FormattingTips className="huge-spacer-bottom" /> + <div> <Button - className={classNames('it__hs-add-comment', { invisible: commentVisible })} - id="hotspot-comment-box-display" - onClick={this.props.onOpenComment}> - {translate('hotspots.comment.open')} + className="huge-spacer-bottom" + id="hotspot-comment-box-submit" + onClick={this.handleSubmitComment}> + {translate('hotspots.comment.submit')} </Button> - - <div className={classNames({ invisible: !commentVisible })}> - <div className="little-spacer-bottom">{translate('hotspots.comment.field')}</div> - <textarea - className="form-field fixed-width width-100 spacer-bottom" - onChange={this.handleCommentChange} - ref={commentTextRef} - rows={2} - value={comment} - /> - <div className="display-flex-space-between display-flex-center "> - <FormattingTips className="huge-spacer-bottom" /> - <div> - <Button - className="huge-spacer-bottom" - id="hotspot-comment-box-submit" - onClick={this.handleSubmitComment}> - {translate('hotspots.comment.submit')} - </Button> - <ResetButtonLink - className="spacer-left huge-spacer-bottom" - id="hotspot-comment-box-cancel" - onClick={this.handleCloseComment}> - {translate('cancel')} - </ResetButtonLink> - </div> - </div> - </div> </div> - </> - )} - </div> - </> + </div> + </div> + )} + + <h2 className="spacer-top big-spacer-bottom">{translate('hotspot.section.activity')}</h2> + + <HotspotReviewHistory + hotspot={hotspot} + onDeleteComment={this.handleDeleteComment} + onEditComment={this.handleEditComment} + onShowFullHistory={this.handleShowFullHistory} + showFullHistory={showFullHistory} + /> + </div> ); } } 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<Props, State> { constructor(props: Props) { super(props); this.commentTextRef = React.createRef<HTMLTextAreaElement>(); - 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<Props, State> { 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<Props, State> { } }; - 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<Props, State> { render() { const { branchLike, component, hotspotsReviewedMeasure, securityCategories } = this.props; - const { - hotspot, - lastStatusChangedTo, - loading, - commentVisible, - showStatusUpdateSuccessModal - } = this.state; + const { hotspot, lastStatusChangedTo, loading, showStatusUpdateSuccessModal } = this.state; return ( <HotspotViewerRenderer branchLike={branchLike} component={component} commentTextRef={this.commentTextRef} - commentVisible={commentVisible} hotspot={hotspot} hotspotsReviewedMeasure={hotspotsReviewedMeasure} lastStatusChangedTo={lastStatusChangedTo} loading={loading} - onCloseComment={this.handleCloseComment} onCloseStatusUpdateSuccessModal={this.handleCloseStatusUpdateSuccessModal} - onOpenComment={this.handleOpenComment} onSwitchFilterToStatusOfUpdatedHotspot={this.handleSwitchFilterToStatusOfUpdatedHotspot} + onShowCommentForm={this.handleScrollToCommentForm} onUpdateHotspot={this.handleHotspotUpdate} showStatusUpdateSuccessModal={showStatusUpdateSuccessModal} securityCategories={securityCategories} diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx index 19142ca7c0f..35231c6a120 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerRenderer.tsx @@ -52,12 +52,10 @@ export interface HotspotViewerRendererProps { hotspotsReviewedMeasure?: string; lastStatusChangedTo?: HotspotStatusOption; loading: boolean; - commentVisible: boolean; commentTextRef: React.RefObject<HTMLTextAreaElement>; - onOpenComment: () => void; - onCloseComment: () => void; onCloseStatusUpdateSuccessModal: () => void; onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise<void>; + 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) && ( <> <div className="dropdown spacer-right flex-1-0-auto"> - <Button className="it__hs-add-comment" onClick={props.onOpenComment}> + <Button className="it__hs-add-comment" onClick={props.onShowCommentForm}> {translate('hotspots.comment.open')} </Button> </div> @@ -175,12 +172,9 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { <HotspotViewerTabs hotspot={hotspot} /> <HotspotReviewHistoryAndComments commentTextRef={commentTextRef} - commentVisible={commentVisible} currentUser={currentUser} hotspot={hotspot} - onCloseComment={props.onCloseComment} onCommentUpdate={props.onUpdateHotspot} - onOpenComment={props.onOpenComment} /> </div> )} 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<HotspotReviewHistoryProps>) { - 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: '<strong>TEST</strong>', - markdown: '*TEST*', - updatable: true, - login: 'dude-1', - user: mockUser({ login: 'dude-1' }) - }; - const commentElement1 = { - key: 'comment-2', - createdAt: '2018-09-11', - htmlText: '<strong>TEST</strong>', - 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<void>((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> + ); + 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<void>((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<HTMLDivElement> + ); + const deleteButtonOnClick = dropdownOverlay.find(Button).props().onClick; + if (!deleteButtonOnClick) { + reject(); + return; + } + + deleteButtonOnClick(); + expect(onDeleteComment).toBeCalledWith('comment-1'); + + resolve(); + }); +}); + +function shallowRender(props?: Partial<HotspotReviewHistoryProps>) { return shallow( <HotspotReviewHistory - hotspot={hotspot} + hotspot={mockHotspot({ + creationDate: '2018-09-01', + changelog: [ + mockIssueChangelog(), + mockIssueChangelog({ + creationDate: '2018-10-12' + }) + ], + comment: [ + mockHotspotComment({ + key: 'comment-1', + updatable: true + }), + mockHotspotComment({ key: 'comment-2', user: mockUser({ name: undefined }) }), + mockHotspotComment({ key: 'comment-3', user: mockUser({ active: false }) }), + mockHotspotComment({ key: 'comment-4' }), + mockHotspotComment({ key: 'comment-5' }) + ] + })} onDeleteComment={jest.fn()} onEditComment={jest.fn()} + onShowFullHistory={jest.fn()} + showFullHistory={false} {...props} /> ); 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<HotspotReviewHistoryAndComments['props']>) { return shallow<HotspotReviewHistoryAndComments>( <HotspotReviewHistoryAndComments commentTextRef={React.createRef()} - commentVisible={false} currentUser={mockCurrentUser()} hotspot={mockHotspot()} - onCloseComment={jest.fn()} onCommentUpdate={jest.fn()} - onOpenComment={jest.fn()} {...props} /> ); 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<HotspotViewerRendererProps>) { 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`] = ` <Fragment> - <div - className="padded" - key="0" - > - <div - className="display-flex-center" + <ul> + <li + className="padded-top padded-bottom" + key="0" > - <Connect(Avatar) - className="little-spacer-right" - name="John Doe" - size={20} - /> - <strong> - John Doe - </strong> - <span - className="little-spacer-left" - > - hotspots.review_history.created - </span> - <span - className="little-spacer-left little-spacer-right" - > - - - </span> - <DateTimeFormatter - date="2018-09-01" - /> - </div> - </div> - <div - className="padded bordered-top" - key="1" - > - <div - className="display-flex-center" + <div + className="display-flex-center" + > + <Connect(Avatar) + className="little-spacer-right" + name="Luke Skywalker" + size={20} + /> + <strong> + Luke Skywalker + </strong> + <span + className="little-spacer-left little-spacer-right" + > + - + </span> + <DateTimeFormatter + date="2018-10-12" + /> + </div> + <div + className="spacer-top" + > + <IssueChangelogDiff + diff={ + Object { + "key": "assign", + "newValue": "darth.vader", + "oldValue": "luke.skywalker", + } + } + key="0" + /> + </div> + </li> + <li + className="padded-top padded-bottom bordered-top" + key="1" > - <Connect(Avatar) - className="little-spacer-right" - name="John Doe" - size={20} - /> - <strong> - John Doe - </strong> - <span - className="little-spacer-left" - > - hotspots.review_history.comment_added - </span> - <span - className="little-spacer-left little-spacer-right" - > - - - </span> - <DateTimeFormatter - date="2018-09-10" - /> - </div> - <div - className="spacer-top display-flex-space-between" + <div + className="display-flex-center" + > + <Connect(Avatar) + className="little-spacer-right" + name="Luke Skywalker" + size={20} + /> + <strong> + Luke Skywalker + </strong> + <span + className="little-spacer-left little-spacer-right" + > + - + </span> + <DateTimeFormatter + date="2018-10-01" + /> + </div> + <div + className="spacer-top" + > + <IssueChangelogDiff + diff={ + Object { + "key": "assign", + "newValue": "darth.vader", + "oldValue": "luke.skywalker", + } + } + key="0" + /> + </div> + </li> + <li + className="padded-top padded-bottom bordered-top" + key="2" > <div - className="markdown" - dangerouslySetInnerHTML={ - Object { - "__html": "<strong>TEST</strong>", + className="display-flex-center" + > + <Connect(Avatar) + className="little-spacer-right" + name="John Doe" + size={20} + /> + <strong> + John Doe + </strong> + <span + className="little-spacer-left" + > + hotspots.review_history.comment_added + </span> + <span + className="little-spacer-left little-spacer-right" + > + - + </span> + <DateTimeFormatter + date="2018-09-10" + /> + </div> + <div + className="spacer-top display-flex-space-between" + > + <div + className="markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "<strong>TEST</strong>", + } } - } - /> - <div> + /> + </div> + </li> + <li + className="padded-top padded-bottom bordered-top" + key="3" + > + <div + className="display-flex-center" + > + <Connect(Avatar) + className="little-spacer-right" + name="John Doe" + size={20} + /> + <strong> + John Doe + </strong> + <span + className="little-spacer-left" + > + hotspots.review_history.comment_added + </span> + <span + className="little-spacer-left little-spacer-right" + > + - + </span> + <DateTimeFormatter + date="2018-09-10" + /> + </div> + <div + className="spacer-top display-flex-space-between" + > <div - className="dropdown" + className="markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "<strong>TEST</strong>", + } + } + /> + </div> + </li> + <li + className="padded-top padded-bottom bordered-top" + key="4" + > + <div + className="display-flex-center" + > + <Connect(Avatar) + className="little-spacer-right" + name="John Doe" + size={20} + /> + <strong> + user.x_deleted.John Doe + </strong> + <span + className="little-spacer-left" > - <Toggler - onRequestClose={[Function]} - open={false} - overlay={ - <DropdownOverlay - placement="bottom-right" - > - <HotspotCommentPopup - markdownComment="*TEST*" - onCancelEdit={[Function]} - onCommentEditSubmit={[Function]} - /> - </DropdownOverlay> + hotspots.review_history.comment_added + </span> + <span + className="little-spacer-left little-spacer-right" + > + - + </span> + <DateTimeFormatter + date="2018-09-10" + /> + </div> + <div + className="spacer-top display-flex-space-between" + > + <div + className="markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "<strong>TEST</strong>", } - > - <EditButton - className="button-small" - onClick={[Function]} - /> - </Toggler> - </div> - <Dropdown - onOpen={[Function]} - overlay={ - <div - className="padded abs-width-150" - > - <p> - issue.comment.delete_confirm_message - </p> - <Button - className="button-red big-spacer-top pull-right" - onClick={[Function]} - > - delete - </Button> - </div> } - overlayPlacement="bottom-right" - > - <DeleteButton - className="button-small" - /> - </Dropdown> - </div> - </div> - </div> - <div - className="padded bordered-top" - key="2" + /> + </div> + </li> + </ul> + <ButtonLink + className="spacer-top" + onClick={[MockFunction]} > - <div - className="display-flex-center" + show_all + </ButtonLink> +</Fragment> +`; + +exports[`should render correctly: delete comment overlay 1`] = ` +<div + className="padded abs-width-150" +> + <p> + issue.comment.delete_confirm_message + </p> + <Button + className="button-red big-spacer-top pull-right" + onClick={[Function]} + > + delete + </Button> +</div> +`; + +exports[`should render correctly: edit comment overlay 1`] = ` +<DropdownOverlay + placement="bottom-right" +> + <HotspotCommentPopup + markdownComment="*TEST*" + onCancelEdit={[Function]} + onCommentEditSubmit={[Function]} + /> +</DropdownOverlay> +`; + +exports[`should render correctly: show full list 1`] = ` +<Fragment> + <ul> + <li + className="padded-top padded-bottom" + key="0" > - <Connect(Avatar) - className="little-spacer-right" - name="John Doe" - size={20} - /> - <strong> - John Doe - </strong> - <span - className="little-spacer-left" - > - hotspots.review_history.comment_added - </span> - <span - className="little-spacer-left little-spacer-right" - > - - - </span> - <DateTimeFormatter - date="2018-09-11" - /> - </div> - <div - className="spacer-top display-flex-space-between" + <div + className="display-flex-center" + > + <Connect(Avatar) + className="little-spacer-right" + name="Luke Skywalker" + size={20} + /> + <strong> + Luke Skywalker + </strong> + <span + className="little-spacer-left little-spacer-right" + > + - + </span> + <DateTimeFormatter + date="2018-10-12" + /> + </div> + <div + className="spacer-top" + > + <IssueChangelogDiff + diff={ + Object { + "key": "assign", + "newValue": "darth.vader", + "oldValue": "luke.skywalker", + } + } + key="0" + /> + </div> + </li> + <li + className="padded-top padded-bottom bordered-top" + key="1" > <div - className="markdown" - dangerouslySetInnerHTML={ - Object { - "__html": "<strong>TEST</strong>", + className="display-flex-center" + > + <Connect(Avatar) + className="little-spacer-right" + name="Luke Skywalker" + size={20} + /> + <strong> + Luke Skywalker + </strong> + <span + className="little-spacer-left little-spacer-right" + > + - + </span> + <DateTimeFormatter + date="2018-10-01" + /> + </div> + <div + className="spacer-top" + > + <IssueChangelogDiff + diff={ + Object { + "key": "assign", + "newValue": "darth.vader", + "oldValue": "luke.skywalker", + } } - } - /> - </div> - </div> - <div - className="padded bordered-top" - key="3" - > - <div - className="display-flex-center" + key="0" + /> + </div> + </li> + <li + className="padded-top padded-bottom bordered-top" + key="2" + > + <div + className="display-flex-center" + > + <Connect(Avatar) + className="little-spacer-right" + name="John Doe" + size={20} + /> + <strong> + John Doe + </strong> + <span + className="little-spacer-left" + > + hotspots.review_history.comment_added + </span> + <span + className="little-spacer-left little-spacer-right" + > + - + </span> + <DateTimeFormatter + date="2018-09-10" + /> + </div> + <div + className="spacer-top display-flex-space-between" + > + <div + className="markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "<strong>TEST</strong>", + } + } + /> + </div> + </li> + <li + className="padded-top padded-bottom bordered-top" + key="3" + > + <div + className="display-flex-center" + > + <Connect(Avatar) + className="little-spacer-right" + name="John Doe" + size={20} + /> + <strong> + John Doe + </strong> + <span + className="little-spacer-left" + > + hotspots.review_history.comment_added + </span> + <span + className="little-spacer-left little-spacer-right" + > + - + </span> + <DateTimeFormatter + date="2018-09-10" + /> + </div> + <div + className="spacer-top display-flex-space-between" + > + <div + className="markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "<strong>TEST</strong>", + } + } + /> + </div> + </li> + <li + className="padded-top padded-bottom bordered-top" + key="4" + > + <div + className="display-flex-center" + > + <Connect(Avatar) + className="little-spacer-right" + name="John Doe" + size={20} + /> + <strong> + user.x_deleted.John Doe + </strong> + <span + className="little-spacer-left" + > + hotspots.review_history.comment_added + </span> + <span + className="little-spacer-left little-spacer-right" + > + - + </span> + <DateTimeFormatter + date="2018-09-10" + /> + </div> + <div + className="spacer-top display-flex-space-between" + > + <div + className="markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "<strong>TEST</strong>", + } + } + /> + </div> + </li> + <li + className="padded-top padded-bottom bordered-top" + key="5" > - <Connect(Avatar) - className="little-spacer-right" - name="me-name" - size={20} - /> - <strong> - me-name - </strong> - <span - className="little-spacer-left little-spacer-right" - > - - - </span> - <DateTimeFormatter - date="2018-10-01" - /> - </div> - <div - className="spacer-top" + <div + className="display-flex-center" + > + <Connect(Avatar) + className="little-spacer-right" + name="john.doe" + size={20} + /> + <strong> + john.doe + </strong> + <span + className="little-spacer-left" + > + hotspots.review_history.comment_added + </span> + <span + className="little-spacer-left little-spacer-right" + > + - + </span> + <DateTimeFormatter + date="2018-09-10" + /> + </div> + <div + className="spacer-top display-flex-space-between" + > + <div + className="markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "<strong>TEST</strong>", + } + } + /> + </div> + </li> + <li + className="padded-top padded-bottom bordered-top" + key="6" > - <IssueChangelogDiff - diff={ - Object { - "key": "assign", - "newValue": "me", - "oldValue": "him", + <div + className="display-flex-center" + > + <Connect(Avatar) + className="little-spacer-right" + name="John Doe" + size={20} + /> + <strong> + John Doe + </strong> + <span + className="little-spacer-left" + > + hotspots.review_history.comment_added + </span> + <span + className="little-spacer-left little-spacer-right" + > + - + </span> + <DateTimeFormatter + date="2018-09-10" + /> + </div> + <div + className="spacer-top display-flex-space-between" + > + <div + className="markdown" + dangerouslySetInnerHTML={ + Object { + "__html": "<strong>TEST</strong>", + } } - } - key="0" - /> - </div> - </div> + /> + <div> + <div + className="dropdown" + > + <Toggler + onRequestClose={[Function]} + open={false} + overlay={ + <DropdownOverlay + placement="bottom-right" + > + <HotspotCommentPopup + markdownComment="*TEST*" + onCancelEdit={[Function]} + onCommentEditSubmit={[Function]} + /> + </DropdownOverlay> + } + > + <EditButton + className="button-small" + onClick={[Function]} + /> + </Toggler> + </div> + <Dropdown + onOpen={[Function]} + overlay={ + <div + className="padded abs-width-150" + > + <p> + issue.comment.delete_confirm_message + </p> + <Button + className="button-red big-spacer-top pull-right" + onClick={[Function]} + > + delete + </Button> + </div> + } + overlayPlacement="bottom-right" + > + <DeleteButton + className="button-small" + /> + </Dropdown> + </div> + </div> + </li> + <li + className="padded-top padded-bottom bordered-top" + key="7" + > + <div + className="display-flex-center" + > + <Connect(Avatar) + className="little-spacer-right" + name="John Doe" + size={20} + /> + <strong> + John Doe + </strong> + <span + className="little-spacer-left" + > + hotspots.review_history.created + </span> + <span + className="little-spacer-left little-spacer-right" + > + - + </span> + <DateTimeFormatter + date="2018-09-01" + /> + </div> + </li> + </ul> </Fragment> `; 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`] = ` -<Fragment> - <h1> - hotspot.section.activity - </h1> +<div + className="padded it__hs-review-history" +> <div - className="padded it__hs-review-history" + className="big-spacer-top" > - <HotspotReviewHistory - hotspot={ - Object { - "assignee": "assignee", - "assigneeUser": Object { + <div + className="little-spacer-bottom" + > + hotspots.comment.field + </div> + <textarea + className="form-field fixed-width width-100 spacer-bottom" + onChange={[Function]} + rows={2} + value="" + /> + <div + className="display-flex-space-between display-flex-center " + > + <FormattingTips + className="huge-spacer-bottom" + /> + <div> + <Button + className="huge-spacer-bottom" + id="hotspot-comment-box-submit" + onClick={[Function]} + > + hotspots.comment.submit + </Button> + </div> + </div> + </div> + <h2 + className="spacer-top big-spacer-bottom" + > + hotspot.section.activity + </h2> + <HotspotReviewHistory + hotspot={ + Object { + "assignee": "assignee", + "assigneeUser": Object { + "active": true, + "local": true, + "login": "assignee", + "name": "John Doe", + }, + "author": "author", + "authorUser": 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": "<p>This a <strong>strong</strong> message about fixing !</p>", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>", + "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": "<p>This a <strong>strong</strong> message about fixing !</p>", - "key": "squid:S2077", - "name": "That rule", - "riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>", - "securityCategory": "sql-injection", - "vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>", - "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]} - /> - <hr /> - <div - className="big-spacer-top" - > - <Button - className="it__hs-add-comment" - id="hotspot-comment-box-display" - onClick={[MockFunction]} - > - hotspots.comment.open - </Button> - <div - className="invisible" - > - <div - className="little-spacer-bottom" - > - hotspots.comment.field - </div> - <textarea - className="form-field fixed-width width-100 spacer-bottom" - onChange={[Function]} - rows={2} - value="" - /> - <div - className="display-flex-space-between display-flex-center " - > - <FormattingTips - className="huge-spacer-bottom" - /> - <div> - <Button - className="huge-spacer-bottom" - id="hotspot-comment-box-submit" - onClick={[Function]} - > - hotspots.comment.submit - </Button> - <ResetButtonLink - className="spacer-left huge-spacer-bottom" - id="hotspot-comment-box-cancel" - onClick={[Function]} - > - cancel - </ResetButtonLink> - </div> - </div> - </div> - </div> - </div> -</Fragment> + } + onDeleteComment={[Function]} + onEditComment={[Function]} + onShowFullHistory={[Function]} + showFullHistory={false} + /> +</div> `; exports[`should render correctly without user 1`] = ` -<Fragment> - <h1> - hotspot.section.activity - </h1> - <div - className="padded it__hs-review-history" +<div + className="padded it__hs-review-history" +> + <h2 + className="spacer-top big-spacer-bottom" > - <HotspotReviewHistory - hotspot={ - Object { - "assignee": "assignee", - "assigneeUser": Object { + hotspot.section.activity + </h2> + <HotspotReviewHistory + hotspot={ + Object { + "assignee": "assignee", + "assigneeUser": Object { + "active": true, + "local": true, + "login": "assignee", + "name": "John Doe", + }, + "author": "author", + "authorUser": 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": "<p>This a <strong>strong</strong> message about fixing !</p>", + "key": "squid:S2077", + "name": "That rule", + "riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>", + "securityCategory": "sql-injection", + "vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>", + "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": "<p>This a <strong>strong</strong> message about fixing !</p>", - "key": "squid:S2077", - "name": "That rule", - "riskDescription": "<p>This a <strong>strong</strong> message about risk !</p>", - "securityCategory": "sql-injection", - "vulnerabilityDescription": "<p>This a <strong>strong</strong> message about vulnerability !</p>", - "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]} - /> - </div> -</Fragment> + } + onDeleteComment={[Function]} + onEditComment={[Function]} + onShowFullHistory={[Function]} + showFullHistory={false} + /> +</div> `; 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]} /> </div> </DeferredSpinner> @@ -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]} /> </div> </DeferredSpinner> @@ -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]} /> </div> </DeferredSpinner> @@ -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]} /> </div> </DeferredSpinner> @@ -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]} /> </div> </DeferredSpinner> @@ -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]} /> </div> </DeferredSpinner> 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 = { |