@@ -138,6 +138,10 @@ textarea.width-100 { | |||
max-width: 100%; | |||
} | |||
textarea.fixed-width { | |||
resize: vertical; | |||
} | |||
select { | |||
height: var(--controlHeight); | |||
line-height: var(--controlHeight); |
@@ -34,6 +34,7 @@ interface Props { | |||
} | |||
interface State { | |||
comment: string; | |||
selectedUser?: T.UserActive; | |||
selectedOption: HotspotStatusOptions; | |||
submitting: boolean; | |||
@@ -41,6 +42,7 @@ interface State { | |||
export default class HotspotActionsForm extends React.Component<Props, State> { | |||
state: State = { | |||
comment: '', | |||
selectedOption: HotspotStatusOptions.FIXED, | |||
submitting: false | |||
}; | |||
@@ -53,17 +55,27 @@ export default class HotspotActionsForm extends React.Component<Props, State> { | |||
this.setState({ selectedUser }); | |||
}; | |||
handleCommentChange = (comment: string) => { | |||
this.setState({ comment }); | |||
}; | |||
handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
const { hotspotKey } = this.props; | |||
const { selectedOption } = this.state; | |||
const { comment, selectedOption, selectedUser } = this.state; | |||
const status = | |||
selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW | |||
? HotspotStatus.TO_REVIEW | |||
: HotspotStatus.REVIEWED; | |||
const data: HotspotSetStatusRequest = { status }; | |||
// If reassigning, ignore comment for status update. It will be sent with the reassignment below | |||
if (comment && !(selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW && selectedUser)) { | |||
data.comment = comment; | |||
} | |||
if (selectedOption !== HotspotStatusOptions.ADDITIONAL_REVIEW) { | |||
data.resolution = HotspotResolution[selectedOption]; | |||
} | |||
@@ -71,9 +83,8 @@ export default class HotspotActionsForm extends React.Component<Props, State> { | |||
this.setState({ submitting: true }); | |||
return setSecurityHotspotStatus(hotspotKey, data) | |||
.then(() => { | |||
const { selectedUser } = this.state; | |||
if (selectedOption === HotspotStatusOptions.ADDITIONAL_REVIEW && selectedUser) { | |||
return this.assignHotspot(selectedUser); | |||
return this.assignHotspot(selectedUser, comment); | |||
} | |||
return null; | |||
}) | |||
@@ -85,22 +96,25 @@ export default class HotspotActionsForm extends React.Component<Props, State> { | |||
}); | |||
}; | |||
assignHotspot = (assignee: T.UserActive) => { | |||
assignHotspot = (assignee: T.UserActive, comment: string) => { | |||
const { hotspotKey } = this.props; | |||
return assignSecurityHotspot(hotspotKey, { | |||
assignee: assignee.login | |||
assignee: assignee.login, | |||
comment | |||
}); | |||
}; | |||
render() { | |||
const { hotspotKey } = this.props; | |||
const { selectedOption, selectedUser, submitting } = this.state; | |||
const { comment, selectedOption, selectedUser, submitting } = this.state; | |||
return ( | |||
<HotspotActionsFormRenderer | |||
comment={comment} | |||
hotspotKey={hotspotKey} | |||
onAssign={this.handleAssign} | |||
onChangeComment={this.handleCommentChange} | |||
onSelectOption={this.handleSelectOption} | |||
onSubmit={this.handleSubmit} | |||
selectedOption={selectedOption} |
@@ -21,12 +21,15 @@ import * as React from 'react'; | |||
import { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import Radio from 'sonar-ui-common/components/controls/Radio'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import MarkdownTips from '../../../components/common/MarkdownTips'; | |||
import { HotspotStatusOptions } from '../../../types/security-hotspots'; | |||
import HotspotAssigneeSelect from './HotspotAssigneeSelect'; | |||
export interface HotspotActionsFormRendererProps { | |||
comment: string; | |||
hotspotKey: string; | |||
onAssign: (user: T.UserActive) => void; | |||
onChangeComment: (comment: string) => void; | |||
onSelectOption: (option: HotspotStatusOptions) => void; | |||
onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void; | |||
selectedOption: HotspotStatusOptions; | |||
@@ -35,10 +38,10 @@ export interface HotspotActionsFormRendererProps { | |||
} | |||
export default function HotspotActionsFormRenderer(props: HotspotActionsFormRendererProps) { | |||
const { selectedOption, submitting } = props; | |||
const { comment, selectedOption, submitting } = props; | |||
return ( | |||
<form className="abs-width-400" onSubmit={props.onSubmit}> | |||
<form className="abs-width-400 padded" onSubmit={props.onSubmit}> | |||
<h2>{translate('hotspots.form.title')}</h2> | |||
<div className="display-flex-column big-spacer-bottom"> | |||
{renderOption({ | |||
@@ -63,6 +66,24 @@ export default function HotspotActionsFormRenderer(props: HotspotActionsFormRend | |||
<HotspotAssigneeSelect onSelect={props.onAssign} /> | |||
</div> | |||
)} | |||
<div className="display-flex-column big-spacer-bottom"> | |||
<label className="little-spacer-bottom">{translate('hotspots.form.comment')}</label> | |||
<textarea | |||
className="form-field fixed-width spacer-bottom" | |||
autoFocus={true} | |||
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => | |||
props.onChangeComment(event.currentTarget.value) | |||
} | |||
placeholder={ | |||
selectedOption === HotspotStatusOptions.SAFE | |||
? translate('hotspots.form.comment.placeholder') | |||
: '' | |||
} | |||
rows={6} | |||
value={comment} | |||
/> | |||
<MarkdownTips /> | |||
</div> | |||
<div className="text-right"> | |||
{submitting && <i className="spinner spacer-right" />} | |||
<SubmitButton disabled={submitting}>{translate('hotspots.form.submit')}</SubmitButton> |
@@ -45,6 +45,12 @@ it('should handle option selection', () => { | |||
expect(wrapper.state().selectedOption).toBe(HotspotStatusOptions.SAFE); | |||
}); | |||
it('should handle comment change', () => { | |||
const wrapper = shallowRender(); | |||
wrapper.instance().handleCommentChange('new comment'); | |||
expect(wrapper.state().comment).toBe('new comment'); | |||
}); | |||
it('should handle submit', async () => { | |||
const onSubmit = jest.fn(); | |||
const wrapper = shallowRender({ onSubmit }); | |||
@@ -63,19 +69,21 @@ it('should handle submit', async () => { | |||
expect(onSubmit).toBeCalled(); | |||
// SAFE | |||
wrapper.setState({ selectedOption: HotspotStatusOptions.SAFE }); | |||
wrapper.setState({ comment: 'commentsafe', selectedOption: HotspotStatusOptions.SAFE }); | |||
await waitAndUpdate(wrapper); | |||
await wrapper.instance().handleSubmit({ preventDefault } as any); | |||
expect(setSecurityHotspotStatus).toBeCalledWith('key', { | |||
comment: 'commentsafe', | |||
status: HotspotStatus.REVIEWED, | |||
resolution: HotspotResolution.SAFE | |||
}); | |||
// FIXED | |||
wrapper.setState({ selectedOption: HotspotStatusOptions.FIXED }); | |||
wrapper.setState({ comment: 'commentFixed', selectedOption: HotspotStatusOptions.FIXED }); | |||
await waitAndUpdate(wrapper); | |||
await wrapper.instance().handleSubmit({ preventDefault } as any); | |||
expect(setSecurityHotspotStatus).toBeCalledWith('key', { | |||
comment: 'commentFixed', | |||
status: HotspotStatus.REVIEWED, | |||
resolution: HotspotResolution.FIXED | |||
}); | |||
@@ -84,7 +92,10 @@ it('should handle submit', async () => { | |||
it('should handle assignment', async () => { | |||
const onSubmit = jest.fn(); | |||
const wrapper = shallowRender({ onSubmit }); | |||
wrapper.setState({ selectedOption: HotspotStatusOptions.ADDITIONAL_REVIEW }); | |||
wrapper.setState({ | |||
comment: 'assignment comment', | |||
selectedOption: HotspotStatusOptions.ADDITIONAL_REVIEW | |||
}); | |||
wrapper.instance().handleAssign(mockLoggedInUser({ login: 'userLogin' })); | |||
await waitAndUpdate(wrapper); | |||
@@ -97,7 +108,8 @@ it('should handle assignment', async () => { | |||
status: HotspotStatus.TO_REVIEW | |||
}); | |||
expect(assignSecurityHotspot).toBeCalledWith('key', { | |||
assignee: 'userLogin' | |||
assignee: 'userLogin', | |||
comment: 'assignment comment' | |||
}); | |||
expect(onSubmit).toBeCalled(); | |||
}); |
@@ -43,8 +43,10 @@ it('should render correctly', () => { | |||
function shallowRender(props: Partial<HotspotActionsFormRendererProps> = {}) { | |||
return shallow<HotspotActionsForm>( | |||
<HotspotActionsFormRenderer | |||
comment="written comment" | |||
hotspotKey="key" | |||
onAssign={jest.fn()} | |||
onChangeComment={jest.fn()} | |||
onSelectOption={jest.fn()} | |||
onSubmit={jest.fn()} | |||
selectedOption={HotspotStatusOptions.FIXED} |
@@ -2,8 +2,10 @@ | |||
exports[`should render correctly 1`] = ` | |||
<HotspotActionsFormRenderer | |||
comment="" | |||
hotspotKey="key" | |||
onAssign={[Function]} | |||
onChangeComment={[Function]} | |||
onSelectOption={[Function]} | |||
onSubmit={[Function]} | |||
selectedOption="FIXED" |
@@ -2,7 +2,7 @@ | |||
exports[`should render correctly 1`] = ` | |||
<form | |||
className="abs-width-400" | |||
className="abs-width-400 padded" | |||
onSubmit={[MockFunction]} | |||
> | |||
<h2> | |||
@@ -66,6 +66,24 @@ exports[`should render correctly 1`] = ` | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<label | |||
className="little-spacer-bottom" | |||
> | |||
hotspots.form.comment | |||
</label> | |||
<textarea | |||
autoFocus={true} | |||
className="form-field fixed-width spacer-bottom" | |||
onChange={[Function]} | |||
placeholder="" | |||
rows={6} | |||
value="written comment" | |||
/> | |||
<MarkdownTips /> | |||
</div> | |||
<div | |||
className="text-right" | |||
> | |||
@@ -80,7 +98,7 @@ exports[`should render correctly 1`] = ` | |||
exports[`should render correctly: Submitting 1`] = ` | |||
<form | |||
className="abs-width-400" | |||
className="abs-width-400 padded" | |||
onSubmit={[MockFunction]} | |||
> | |||
<h2> | |||
@@ -144,6 +162,24 @@ exports[`should render correctly: Submitting 1`] = ` | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<label | |||
className="little-spacer-bottom" | |||
> | |||
hotspots.form.comment | |||
</label> | |||
<textarea | |||
autoFocus={true} | |||
className="form-field fixed-width spacer-bottom" | |||
onChange={[Function]} | |||
placeholder="" | |||
rows={6} | |||
value="written comment" | |||
/> | |||
<MarkdownTips /> | |||
</div> | |||
<div | |||
className="text-right" | |||
> | |||
@@ -161,7 +197,7 @@ exports[`should render correctly: Submitting 1`] = ` | |||
exports[`should render correctly: safe option selected 1`] = ` | |||
<form | |||
className="abs-width-400" | |||
className="abs-width-400 padded" | |||
onSubmit={[MockFunction]} | |||
> | |||
<h2> | |||
@@ -225,6 +261,24 @@ exports[`should render correctly: safe option selected 1`] = ` | |||
</div> | |||
</div> | |||
</div> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<label | |||
className="little-spacer-bottom" | |||
> | |||
hotspots.form.comment | |||
</label> | |||
<textarea | |||
autoFocus={true} | |||
className="form-field fixed-width spacer-bottom" | |||
onChange={[Function]} | |||
placeholder="hotspots.form.comment.placeholder" | |||
rows={6} | |||
value="written comment" | |||
/> | |||
<MarkdownTips /> | |||
</div> | |||
<div | |||
className="text-right" | |||
> | |||
@@ -239,7 +293,7 @@ exports[`should render correctly: safe option selected 1`] = ` | |||
exports[`should render correctly: user selected 1`] = ` | |||
<form | |||
className="abs-width-400" | |||
className="abs-width-400 padded" | |||
onSubmit={[MockFunction]} | |||
> | |||
<h2> | |||
@@ -313,6 +367,24 @@ exports[`should render correctly: user selected 1`] = ` | |||
onSelect={[MockFunction]} | |||
/> | |||
</div> | |||
<div | |||
className="display-flex-column big-spacer-bottom" | |||
> | |||
<label | |||
className="little-spacer-bottom" | |||
> | |||
hotspots.form.comment | |||
</label> | |||
<textarea | |||
autoFocus={true} | |||
className="form-field fixed-width spacer-bottom" | |||
onChange={[Function]} | |||
placeholder="" | |||
rows={6} | |||
value="written comment" | |||
/> | |||
<MarkdownTips /> | |||
</div> | |||
<div | |||
className="text-right" | |||
> |
@@ -107,6 +107,7 @@ export interface HotspotSearchResponse { | |||
export interface HotspotSetStatusRequest { | |||
status: HotspotStatus; | |||
resolution?: HotspotResolution; | |||
comment?: string; | |||
} | |||
export interface HotspotAssignRequest { |
@@ -675,7 +675,8 @@ hotspots.form.title=Mark Security Hotspot as: | |||
hotspots.form.assign_to=Assign to: | |||
hotspots.form.select_user=Select a user... | |||
hotspots.form.comment=Comment | |||
hotspots.form.comment=Comment: | |||
hotspots.form.comment.placeholder=This status requires justification | |||
hotspots.form.submit=Apply changes | |||
hotspots.status_option.FIXED=Fixed |