@@ -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, |
@@ -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> | |||
)} | |||
</> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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} |
@@ -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> | |||
)} |
@@ -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} | |||
/> | |||
); |
@@ -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} | |||
/> | |||
); |
@@ -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({}); |
@@ -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} |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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={ |
@@ -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> |
@@ -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 = { |
@@ -26,3 +26,20 @@ export function mockReferencedRule(overrides: Partial<ReferencedRule> = {}): Ref | |||
...overrides | |||
}; | |||
} | |||
export function mockIssueChangelog(overrides: Partial<T.IssueChangelog> = {}): 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 | |||
}; | |||
} |
@@ -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<HotspotComponent>): Hot | |||
}; | |||
} | |||
export function mockHotspotComment(overrides?: Partial<HotspotComment>): HotspotComment { | |||
return { | |||
key: 'comment-1', | |||
createdAt: '2018-09-10', | |||
htmlText: '<strong>TEST</strong>', | |||
markdown: '*TEST*', | |||
updatable: false, | |||
login: 'dude-2', | |||
user: mockUser({ login: 'dude-2' }), | |||
...overrides | |||
}; | |||
} | |||
export function mockHotspotRule(overrides?: Partial<HotspotRule>): HotspotRule { | |||
return { | |||
key: 'squid:S2077', |
@@ -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 |