max-width: 100%;
}
+textarea.fixed-width {
+ resize: vertical;
+}
+
select {
height: var(--controlHeight);
line-height: var(--controlHeight);
}
interface State {
+ comment: string;
selectedUser?: T.UserActive;
selectedOption: HotspotStatusOptions;
submitting: boolean;
export default class HotspotActionsForm extends React.Component<Props, State> {
state: State = {
+ comment: '',
selectedOption: HotspotStatusOptions.FIXED,
submitting: false
};
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];
}
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;
})
});
};
- 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}
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;
}
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({
<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>
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 });
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
});
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);
status: HotspotStatus.TO_REVIEW
});
expect(assignSecurityHotspot).toBeCalledWith('key', {
- assignee: 'userLogin'
+ assignee: 'userLogin',
+ comment: 'assignment comment'
});
expect(onSubmit).toBeCalled();
});
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}
exports[`should render correctly 1`] = `
<HotspotActionsFormRenderer
+ comment=""
hotspotKey="key"
onAssign={[Function]}
+ onChangeComment={[Function]}
onSelectOption={[Function]}
onSubmit={[Function]}
selectedOption="FIXED"
exports[`should render correctly 1`] = `
<form
- className="abs-width-400"
+ className="abs-width-400 padded"
onSubmit={[MockFunction]}
>
<h2>
</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"
>
exports[`should render correctly: Submitting 1`] = `
<form
- className="abs-width-400"
+ className="abs-width-400 padded"
onSubmit={[MockFunction]}
>
<h2>
</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"
>
exports[`should render correctly: safe option selected 1`] = `
<form
- className="abs-width-400"
+ className="abs-width-400 padded"
onSubmit={[MockFunction]}
>
<h2>
</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"
>
exports[`should render correctly: user selected 1`] = `
<form
- className="abs-width-400"
+ className="abs-width-400 padded"
onSubmit={[MockFunction]}
>
<h2>
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"
>
export interface HotspotSetStatusRequest {
status: HotspotStatus;
resolution?: HotspotResolution;
+ comment?: string;
}
export interface HotspotAssignRequest {
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