@@ -18,9 +18,11 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as classNames from 'classnames'; | |||
import { uniq } from 'lodash'; | |||
import * as React from 'react'; | |||
import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; | |||
import IssueIcon from 'sonar-ui-common/components/icons/IssueIcon'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import { sortByType } from '../../../helpers/issues'; | |||
export interface LineIssuesIndicatorProps { | |||
@@ -36,26 +38,46 @@ export function LineIssuesIndicator(props: LineIssuesIndicatorProps) { | |||
const className = classNames('source-meta', 'source-line-issues', { | |||
'source-line-with-issues': hasIssues | |||
}); | |||
const mostImportantIssue = hasIssues ? sortByType(issues)[0] : null; | |||
const handleClick = (e: React.MouseEvent<HTMLElement>) => { | |||
e.preventDefault(); | |||
e.currentTarget.blur(); | |||
props.onClick(); | |||
}; | |||
if (!hasIssues) { | |||
return <td className={className} data-line-number={line.line} />; | |||
} | |||
const mostImportantIssue = sortByType(issues)[0]; | |||
const issueTypes = uniq(issues.map(i => i.type)); | |||
let tooltipContent; | |||
if (issueTypes.length > 1) { | |||
tooltipContent = translate('source_viewer.issues_on_line.multiple_issues'); | |||
} else if (issues.length === 1) { | |||
tooltipContent = translateWithParameters( | |||
'source_viewer.issues_on_line.issue_of_type_X', | |||
translate('issue.type', mostImportantIssue.type) | |||
); | |||
} else { | |||
tooltipContent = translateWithParameters( | |||
'source_viewer.issues_on_line.X_issues_of_type_Y', | |||
issues.length, | |||
translate('issue.type', mostImportantIssue.type, 'plural') | |||
); | |||
} | |||
return ( | |||
<td className={className} data-line-number={line.line}> | |||
{hasIssues && ( | |||
<span | |||
aria-label={translate('source_viewer.issues_on_line', issuesOpen ? 'hide' : 'show')} | |||
onClick={handleClick} | |||
role="button" | |||
tabIndex={0}> | |||
{mostImportantIssue != null && <IssueIcon type={mostImportantIssue.type} />} | |||
{issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>} | |||
</span> | |||
)} | |||
<span | |||
aria-label={translate('source_viewer.issues_on_line', issuesOpen ? 'hide' : 'show')} | |||
onClick={(e: React.MouseEvent<HTMLElement>) => { | |||
e.preventDefault(); | |||
e.currentTarget.blur(); | |||
props.onClick(); | |||
}} | |||
role="button" | |||
tabIndex={0}> | |||
<Tooltip overlay={tooltipContent}> | |||
<IssueIcon type={mostImportantIssue.type} /> | |||
</Tooltip> | |||
{issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>} | |||
</span> | |||
</td> | |||
); | |||
} |
@@ -29,10 +29,13 @@ it('should render correctly', () => { | |||
shallowRender({ | |||
issues: [ | |||
mockIssue(false, { key: 'foo', type: 'VULNERABILITY' }), | |||
mockIssue(false, { key: 'bar', type: 'SECURITY_HOTSPOT' }) | |||
mockIssue(false, { key: 'bar', type: 'VULNERABILITY' }) | |||
] | |||
}) | |||
).toMatchSnapshot('diff issue types'); | |||
).toMatchSnapshot('multiple issues, same type'); | |||
expect( | |||
shallowRender({ issues: [mockIssue(false, { key: 'foo', type: 'VULNERABILITY' })] }) | |||
).toMatchSnapshot('single issue'); | |||
expect(shallowRender({ issues: [] })).toMatchSnapshot('no issues'); | |||
}); | |||
@@ -11,9 +11,13 @@ exports[`should render correctly: default 1`] = ` | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<IssueIcon | |||
type="BUG" | |||
/> | |||
<Tooltip | |||
overlay="source_viewer.issues_on_line.multiple_issues" | |||
> | |||
<IssueIcon | |||
type="BUG" | |||
/> | |||
</Tooltip> | |||
<span | |||
className="source-line-issues-counter" | |||
> | |||
@@ -23,7 +27,7 @@ exports[`should render correctly: default 1`] = ` | |||
</td> | |||
`; | |||
exports[`should render correctly: diff issue types 1`] = ` | |||
exports[`should render correctly: multiple issues, same type 1`] = ` | |||
<td | |||
className="source-meta source-line-issues source-line-with-issues" | |||
data-line-number={3} | |||
@@ -34,9 +38,13 @@ exports[`should render correctly: diff issue types 1`] = ` | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<IssueIcon | |||
type="VULNERABILITY" | |||
/> | |||
<Tooltip | |||
overlay="source_viewer.issues_on_line.X_issues_of_type_Y.2.issue.type.VULNERABILITY.plural" | |||
> | |||
<IssueIcon | |||
type="VULNERABILITY" | |||
/> | |||
</Tooltip> | |||
<span | |||
className="source-line-issues-counter" | |||
> | |||
@@ -52,3 +60,25 @@ exports[`should render correctly: no issues 1`] = ` | |||
data-line-number={3} | |||
/> | |||
`; | |||
exports[`should render correctly: single issue 1`] = ` | |||
<td | |||
className="source-meta source-line-issues source-line-with-issues" | |||
data-line-number={3} | |||
> | |||
<span | |||
aria-label="source_viewer.issues_on_line.show" | |||
onClick={[Function]} | |||
role="button" | |||
tabIndex={0} | |||
> | |||
<Tooltip | |||
overlay="source_viewer.issues_on_line.issue_of_type_X.issue.type.VULNERABILITY" | |||
> | |||
<IssueIcon | |||
type="VULNERABILITY" | |||
/> | |||
</Tooltip> | |||
</span> | |||
</td> | |||
`; |
@@ -2540,6 +2540,9 @@ source_viewer.tooltip.no_information_about_tests=There is no extra information a | |||
source_viewer.tooltip.scm.commited_on=Committed on | |||
source_viewer.tooltip.scm.revision=Revision | |||
source_viewer.issues_on_line.multiple_issues=There are multiple issues on this line. | |||
source_viewer.issues_on_line.issue_of_type_X=There is a {0} on this line | |||
source_viewer.issues_on_line.X_issues_of_type_Y=There are {0} {1} on this line | |||
source_viewer.issues_on_line.show=Click to show all issues on this line | |||
source_viewer.issues_on_line.hide=Click to hide all issues on this line | |||