]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12720 comment status changes
authorJeremy <jeremy.davis@sonarsource.com>
Mon, 23 Dec 2019 08:12:09 +0000 (09:12 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 13 Jan 2020 19:46:32 +0000 (20:46 +0100)
server/sonar-web/src/main/js/app/styles/init/forms.css
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsForm.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/HotspotActionsFormRenderer.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsForm-test.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/HotspotActionsFormRenderer-test.tsx
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsForm-test.tsx.snap
server/sonar-web/src/main/js/apps/securityHotspots/components/__tests__/__snapshots__/HotspotActionsFormRenderer-test.tsx.snap
server/sonar-web/src/main/js/types/security-hotspots.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index cf79a4db03c23af8c62e55979557b2228860e230..f06098111ccf67ede4753fc1b51bcb63bfa9cbc4 100644 (file)
@@ -138,6 +138,10 @@ textarea.width-100 {
   max-width: 100%;
 }
 
+textarea.fixed-width {
+  resize: vertical;
+}
+
 select {
   height: var(--controlHeight);
   line-height: var(--controlHeight);
index b1c998847b59c6e9a5dd548bb3f29efa426f8880..e0c920dd561ccc1f096a081f41d88717788e0233 100644 (file)
@@ -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}
index 8fb0824385d2eb14a545d9d6035abfcb13c9cbf3..920c14dbc903c34b897a86bad0b8fbb185e6b011 100644 (file)
@@ -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>
index a371d5efa12363cc4ba271b8ae59722446f300c2..0fbf7bcf6d1a8c5aef196a439f919d6fcb8709db 100644 (file)
@@ -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();
 });
index ff65d450a0fcc4988fef21747931a1eec260c00d..bcdde2e91b650bcf2f93aa3a3afea9da49b071b8 100644 (file)
@@ -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}
index 68ed8d32c27249f8ce2ffe400417c04268068d27..68074754ad72ce5f2c995c62a203f7db20bf38e5 100644 (file)
@@ -2,8 +2,10 @@
 
 exports[`should render correctly 1`] = `
 <HotspotActionsFormRenderer
+  comment=""
   hotspotKey="key"
   onAssign={[Function]}
+  onChangeComment={[Function]}
   onSelectOption={[Function]}
   onSubmit={[Function]}
   selectedOption="FIXED"
index 0d539f72b10f00c22a1c92f4f04407d3d4062dc3..4ecad39a845259f3922834fa453411b025913435 100644 (file)
@@ -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"
   >
index 3d2fb32c7e0bb8d863395e05321b47924dd46c2e..0e0c07345a4d10cc71a0d0e3d82825f0e790b583 100644 (file)
@@ -107,6 +107,7 @@ export interface HotspotSearchResponse {
 export interface HotspotSetStatusRequest {
   status: HotspotStatus;
   resolution?: HotspotResolution;
+  comment?: string;
 }
 
 export interface HotspotAssignRequest {
index e9a6699355cf4e1ea302f7f0c0b877270cb459b1..a2fd92c2acac12891171125a8ea8d48170c8965d 100644 (file)
@@ -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