SONAR-12719: * Prevent action button text from wrapping * Conditional submit button labeltags/8.2.0.32929
@@ -59,8 +59,15 @@ export function getSecurityHotspots( | |||
return getJSON('/api/hotspots/search', data).catch(throwGlobalError); | |||
} | |||
export function getSecurityHotspotList(hotspotKeys: string[]): Promise<HotspotSearchResponse> { | |||
return getJSON('/api/hotspots/search', { hotspots: hotspotKeys.join() }).catch(throwGlobalError); | |||
export function getSecurityHotspotList( | |||
hotspotKeys: string[], | |||
data: { | |||
projectKey: string; | |||
} & BranchParameters | |||
): Promise<HotspotSearchResponse> { | |||
return getJSON('/api/hotspots/search', { ...data, hotspots: hotspotKeys.join() }).catch( | |||
throwGlobalError | |||
); | |||
} | |||
export function getSecurityHotspotDetails(securityHotspotKey: string): Promise<Hotspot> { |
@@ -157,7 +157,10 @@ export class SecurityHotspotsApp extends React.PureComponent<Props, State> { | |||
this.setState({ hotspotKeys }); | |||
if (hotspotKeys && hotspotKeys.length > 0) { | |||
return getSecurityHotspotList(hotspotKeys); | |||
return getSecurityHotspotList(hotspotKeys, { | |||
projectKey: component.key, | |||
...getBranchLikeQuery(branchLike) | |||
}); | |||
} | |||
const status = |
@@ -113,7 +113,10 @@ it('should load data correctly when hotspot key list is forced', async () => { | |||
}); | |||
await waitAndUpdate(wrapper); | |||
expect(getSecurityHotspotList).toBeCalledWith(hotspotKeys); | |||
expect(getSecurityHotspotList).toBeCalledWith(hotspotKeys, { | |||
projectKey: 'my-project', | |||
branch: 'branch-6.7' | |||
}); | |||
expect(wrapper.state().hotspotKeys).toEqual(hotspotKeys); | |||
expect(wrapper.find(SecurityHotspotsAppRenderer).props().isStaticListOfHotspots).toBeTruthy(); | |||
@@ -53,7 +53,7 @@ export default function HotspotActions(props: HotspotActionsProps) { | |||
}); | |||
return ( | |||
<div className="dropdown"> | |||
<div className="dropdown big-spacer-left flex-0"> | |||
<Button onClick={() => setOpen(!open)}> | |||
{translate('hotspot.change_status', hotspot.status)} | |||
<DropdownIcon className="little-spacer-left" /> | |||
@@ -63,7 +63,7 @@ export default function HotspotActions(props: HotspotActionsProps) { | |||
<OutsideClickHandler onClickOutside={() => setOpen(false)}> | |||
<DropdownOverlay placement={PopupPlacement.BottomRight}> | |||
<HotspotActionsForm | |||
hotspotKey={hotspot.key} | |||
hotspot={hotspot} | |||
onSubmit={data => { | |||
setOpen(false); | |||
props.onSubmit(data); |
@@ -20,6 +20,7 @@ | |||
import * as React from 'react'; | |||
import { assignSecurityHotspot, setSecurityHotspotStatus } from '../../../api/security-hotspots'; | |||
import { | |||
Hotspot, | |||
HotspotResolution, | |||
HotspotSetStatusRequest, | |||
HotspotStatus, | |||
@@ -29,7 +30,7 @@ import { | |||
import HotspotActionsFormRenderer from './HotspotActionsFormRenderer'; | |||
interface Props { | |||
hotspotKey: string; | |||
hotspot: Hotspot; | |||
onSubmit: (data: HotspotUpdateFields) => void; | |||
} | |||
@@ -62,7 +63,7 @@ export default class HotspotActionsForm extends React.Component<Props, State> { | |||
handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
const { hotspotKey } = this.props; | |||
const { hotspot } = this.props; | |||
const { comment, selectedOption, selectedUser } = this.state; | |||
const status = | |||
@@ -82,7 +83,7 @@ export default class HotspotActionsForm extends React.Component<Props, State> { | |||
} | |||
this.setState({ submitting: true }); | |||
return setSecurityHotspotStatus(hotspotKey, data) | |||
return setSecurityHotspotStatus(hotspot.key, data) | |||
.then(() => { | |||
if (selectedOption === HotspotStatusOption.ADDITIONAL_REVIEW && selectedUser) { | |||
return this.assignHotspot(selectedUser, comment); | |||
@@ -98,22 +99,22 @@ export default class HotspotActionsForm extends React.Component<Props, State> { | |||
}; | |||
assignHotspot = (assignee: T.UserActive, comment: string) => { | |||
const { hotspotKey } = this.props; | |||
const { hotspot } = this.props; | |||
return assignSecurityHotspot(hotspotKey, { | |||
return assignSecurityHotspot(hotspot.key, { | |||
assignee: assignee.login, | |||
comment | |||
}); | |||
}; | |||
render() { | |||
const { hotspotKey } = this.props; | |||
const { hotspot } = this.props; | |||
const { comment, selectedOption, selectedUser, submitting } = this.state; | |||
return ( | |||
<HotspotActionsFormRenderer | |||
comment={comment} | |||
hotspotKey={hotspotKey} | |||
hotspotStatus={hotspot.status} | |||
onAssign={this.handleAssign} | |||
onChangeComment={this.handleCommentChange} | |||
onSelectOption={this.handleSelectOption} |
@@ -22,12 +22,12 @@ 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 { HotspotStatusOption } from '../../../types/security-hotspots'; | |||
import { HotspotStatus, HotspotStatusOption } from '../../../types/security-hotspots'; | |||
import HotspotAssigneeSelect from './HotspotAssigneeSelect'; | |||
export interface HotspotActionsFormRendererProps { | |||
comment: string; | |||
hotspotKey: string; | |||
hotspotStatus: HotspotStatus; | |||
onAssign: (user: T.UserActive) => void; | |||
onChangeComment: (comment: string) => void; | |||
onSelectOption: (option: HotspotStatusOption) => void; | |||
@@ -38,7 +38,7 @@ export interface HotspotActionsFormRendererProps { | |||
} | |||
export default function HotspotActionsFormRenderer(props: HotspotActionsFormRendererProps) { | |||
const { comment, selectedOption, submitting } = props; | |||
const { comment, hotspotStatus, selectedOption, submitting } = props; | |||
return ( | |||
<form className="abs-width-400 padded" onSubmit={props.onSubmit}> | |||
@@ -69,8 +69,8 @@ export default function HotspotActionsFormRenderer(props: HotspotActionsFormRend | |||
<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} | |||
className="form-field fixed-width spacer-bottom" | |||
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => | |||
props.onChangeComment(event.currentTarget.value) | |||
} | |||
@@ -86,7 +86,9 @@ export default function HotspotActionsFormRenderer(props: HotspotActionsFormRend | |||
</div> | |||
<div className="text-right"> | |||
{submitting && <i className="spinner spacer-right" />} | |||
<SubmitButton disabled={submitting}>{translate('hotspots.form.submit')}</SubmitButton> | |||
<SubmitButton disabled={submitting}> | |||
{translate('hotspots.form.submit', hotspotStatus)} | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
); |
@@ -21,6 +21,7 @@ import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { assignSecurityHotspot, setSecurityHotspotStatus } from '../../../../api/security-hotspots'; | |||
import { mockHotspot } from '../../../../helpers/mocks/security-hotspots'; | |||
import { mockLoggedInUser } from '../../../../helpers/testMocks'; | |||
import { | |||
HotspotResolution, | |||
@@ -128,6 +129,6 @@ it('should handle submit failure', async () => { | |||
function shallowRender(props: Partial<HotspotActionsForm['props']> = {}) { | |||
return shallow<HotspotActionsForm>( | |||
<HotspotActionsForm hotspotKey="key" onSubmit={jest.fn()} {...props} /> | |||
<HotspotActionsForm hotspot={mockHotspot({ key: 'key' })} onSubmit={jest.fn()} {...props} /> | |||
); | |||
} |
@@ -20,7 +20,7 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockLoggedInUser } from '../../../../helpers/testMocks'; | |||
import { HotspotStatusOption } from '../../../../types/security-hotspots'; | |||
import { HotspotStatus, HotspotStatusOption } from '../../../../types/security-hotspots'; | |||
import HotspotActionsForm from '../HotspotActionsForm'; | |||
import HotspotActionsFormRenderer, { | |||
HotspotActionsFormRendererProps | |||
@@ -44,7 +44,7 @@ function shallowRender(props: Partial<HotspotActionsFormRendererProps> = {}) { | |||
return shallow<HotspotActionsForm>( | |||
<HotspotActionsFormRenderer | |||
comment="written comment" | |||
hotspotKey="key" | |||
hotspotStatus={HotspotStatus.TO_REVIEW} | |||
onAssign={jest.fn()} | |||
onChangeComment={jest.fn()} | |||
onSelectOption={jest.fn()} |
@@ -2,7 +2,7 @@ | |||
exports[`should open when clicked 1`] = ` | |||
<div | |||
className="dropdown" | |||
className="dropdown big-spacer-left flex-0" | |||
> | |||
<Button | |||
onClick={[Function]} | |||
@@ -19,7 +19,104 @@ exports[`should open when clicked 1`] = ` | |||
placement="bottom-right" | |||
> | |||
<HotspotActionsForm | |||
hotspotKey="key" | |||
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", | |||
}, | |||
"changelog": Array [], | |||
"comment": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "FIL", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
}, | |||
"creationDate": "2013-05-13T17:55:41+0200", | |||
"key": "key", | |||
"line": 142, | |||
"message": "'3' is a magic number.", | |||
"project": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
}, | |||
"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": "TO_REVIEW", | |||
"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", | |||
}, | |||
], | |||
} | |||
} | |||
onSubmit={[Function]} | |||
/> | |||
</DropdownOverlay> | |||
@@ -29,7 +126,7 @@ exports[`should open when clicked 1`] = ` | |||
exports[`should register an eventlistener: Dropdown closed 1`] = ` | |||
<div | |||
className="dropdown" | |||
className="dropdown big-spacer-left flex-0" | |||
> | |||
<Button | |||
onClick={[Function]} | |||
@@ -44,7 +141,7 @@ exports[`should register an eventlistener: Dropdown closed 1`] = ` | |||
exports[`should register an eventlistener: Dropdown open 1`] = ` | |||
<div | |||
className="dropdown" | |||
className="dropdown big-spacer-left flex-0" | |||
> | |||
<Button | |||
onClick={[Function]} | |||
@@ -61,7 +158,104 @@ exports[`should register an eventlistener: Dropdown open 1`] = ` | |||
placement="bottom-right" | |||
> | |||
<HotspotActionsForm | |||
hotspotKey="key" | |||
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", | |||
}, | |||
"changelog": Array [], | |||
"comment": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "FIL", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
}, | |||
"creationDate": "2013-05-13T17:55:41+0200", | |||
"key": "key", | |||
"line": 142, | |||
"message": "'3' is a magic number.", | |||
"project": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
}, | |||
"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": "TO_REVIEW", | |||
"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", | |||
}, | |||
], | |||
} | |||
} | |||
onSubmit={[Function]} | |||
/> | |||
</DropdownOverlay> | |||
@@ -71,7 +265,7 @@ exports[`should register an eventlistener: Dropdown open 1`] = ` | |||
exports[`should register an eventlistener: Dropdown still open 1`] = ` | |||
<div | |||
className="dropdown" | |||
className="dropdown big-spacer-left flex-0" | |||
> | |||
<Button | |||
onClick={[Function]} | |||
@@ -88,7 +282,104 @@ exports[`should register an eventlistener: Dropdown still open 1`] = ` | |||
placement="bottom-right" | |||
> | |||
<HotspotActionsForm | |||
hotspotKey="key" | |||
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", | |||
}, | |||
"changelog": Array [], | |||
"comment": Array [], | |||
"component": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "FIL", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
}, | |||
"creationDate": "2013-05-13T17:55:41+0200", | |||
"key": "key", | |||
"line": 142, | |||
"message": "'3' is a magic number.", | |||
"project": Object { | |||
"breadcrumbs": Array [], | |||
"key": "my-project", | |||
"name": "MyProject", | |||
"organization": "foo", | |||
"qualifier": "TRK", | |||
"qualityGate": Object { | |||
"isDefault": true, | |||
"key": "30", | |||
"name": "Sonar way", | |||
}, | |||
"qualityProfiles": Array [ | |||
Object { | |||
"deleted": false, | |||
"key": "my-qp", | |||
"language": "ts", | |||
"name": "Sonar way", | |||
}, | |||
], | |||
"tags": Array [], | |||
}, | |||
"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": "TO_REVIEW", | |||
"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", | |||
}, | |||
], | |||
} | |||
} | |||
onSubmit={[Function]} | |||
/> | |||
</DropdownOverlay> | |||
@@ -98,7 +389,7 @@ exports[`should register an eventlistener: Dropdown still open 1`] = ` | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="dropdown" | |||
className="dropdown big-spacer-left flex-0" | |||
> | |||
<Button | |||
onClick={[Function]} |
@@ -3,7 +3,7 @@ | |||
exports[`should render correctly 1`] = ` | |||
<HotspotActionsFormRenderer | |||
comment="" | |||
hotspotKey="key" | |||
hotspotStatus="REVIEWED" | |||
onAssign={[Function]} | |||
onChangeComment={[Function]} | |||
onSelectOption={[Function]} |
@@ -90,7 +90,7 @@ exports[`should render correctly 1`] = ` | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
hotspots.form.submit | |||
hotspots.form.submit.TO_REVIEW | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
@@ -189,7 +189,7 @@ exports[`should render correctly: Submitting 1`] = ` | |||
<SubmitButton | |||
disabled={true} | |||
> | |||
hotspots.form.submit | |||
hotspots.form.submit.TO_REVIEW | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
@@ -285,7 +285,7 @@ exports[`should render correctly: safe option selected 1`] = ` | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
hotspots.form.submit | |||
hotspots.form.submit.TO_REVIEW | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
@@ -391,7 +391,7 @@ exports[`should render correctly: user selected 1`] = ` | |||
<SubmitButton | |||
disabled={false} | |||
> | |||
hotspots.form.submit | |||
hotspots.form.submit.TO_REVIEW | |||
</SubmitButton> | |||
</div> | |||
</form> |
@@ -84,7 +84,7 @@ export interface Hotspot { | |||
project: T.Component; | |||
resolution?: string; | |||
rule: HotspotRule; | |||
status: string; | |||
status: HotspotStatus; | |||
textRange: T.TextRange; | |||
updateDate: string; | |||
users: T.UserBase[]; |
@@ -688,7 +688,8 @@ hotspots.form.assign_to=Assign to: | |||
hotspots.form.select_user=Select a user... | |||
hotspots.form.comment=Comment: | |||
hotspots.form.comment.placeholder=This status requires justification | |||
hotspots.form.submit=Apply changes | |||
hotspots.form.submit.TO_REVIEW=Submit Review | |||
hotspots.form.submit.REVIEWED=Apply changes | |||
hotspots.status_option.FIXED=Fixed | |||
hotspots.status_option.FIXED.description=The code has been modified to follow recommended secure coding practices. |