Browse Source

SONAR-9063 Rework issue box

tags/6.4-RC1
Grégoire Aubert 7 years ago
parent
commit
d665528c87
100 changed files with 5485 additions and 195 deletions
  1. 66
    1
      server/sonar-web/src/main/js/api/issues.js
  2. 94
    0
      server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js
  3. 1
    5
      server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js
  4. 0
    5
      server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap
  5. 0
    2
      server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js
  6. 0
    1
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.js.snap
  7. 1
    1
      server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js
  8. 1
    1
      server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js
  9. 1
    1
      server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js
  10. 1
    1
      server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js
  11. 1
    1
      server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js
  12. 2
    2
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap
  13. 2
    2
      server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap
  14. 109
    0
      server/sonar-web/src/main/js/components/common/BubblePopupHelper.js
  15. 44
    0
      server/sonar-web/src/main/js/components/common/MarkdownTips.js
  16. 6
    0
      server/sonar-web/src/main/js/components/common/MultiSelect.js
  17. 133
    0
      server/sonar-web/src/main/js/components/common/SelectList.js
  18. 76
    0
      server/sonar-web/src/main/js/components/common/SelectListItem.js
  19. 139
    0
      server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js
  20. 4
    8
      server/sonar-web/src/main/js/components/common/__tests__/MarkdownTips-test.js
  21. 75
    0
      server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js
  22. 44
    0
      server/sonar-web/src/main/js/components/common/__tests__/SelectListItem-test.js
  23. 148
    0
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap
  24. 29
    0
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.js.snap
  25. 83
    0
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap
  26. 59
    0
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectListItem-test.js.snap
  27. 3
    2
      server/sonar-web/src/main/js/components/controls/Checkbox.js
  28. 7
    0
      server/sonar-web/src/main/js/components/controls/__tests__/Checkbox-test.js
  29. 153
    0
      server/sonar-web/src/main/js/components/issue/BaseIssue.js
  30. 9
    2
      server/sonar-web/src/main/js/components/issue/ConnectedIssue.js
  31. 7
    127
      server/sonar-web/src/main/js/components/issue/Issue.js
  32. 121
    0
      server/sonar-web/src/main/js/components/issue/IssueView.js
  33. 52
    0
      server/sonar-web/src/main/js/components/issue/actions.js
  34. 164
    0
      server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js
  35. 85
    0
      server/sonar-web/src/main/js/components/issue/components/IssueAssign.js
  36. 65
    0
      server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js
  37. 71
    0
      server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.js
  38. 72
    0
      server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js
  39. 122
    0
      server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js
  40. 53
    0
      server/sonar-web/src/main/js/components/issue/components/IssueMessage.js
  41. 70
    0
      server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js
  42. 90
    0
      server/sonar-web/src/main/js/components/issue/components/IssueTags.js
  43. 91
    0
      server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js
  44. 80
    0
      server/sonar-web/src/main/js/components/issue/components/IssueTransition.js
  45. 73
    0
      server/sonar-web/src/main/js/components/issue/components/IssueType.js
  46. 75
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js
  47. 62
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js
  48. 53
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js
  49. 64
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js
  50. 33
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.js
  51. 70
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js
  52. 77
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js
  53. 51
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js
  54. 91
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js
  55. 70
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js
  56. 111
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap
  57. 68
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap
  58. 49
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap
  59. 205
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap
  60. 10
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.js.snap
  61. 75
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap
  62. 89
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap
  63. 124
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap
  64. 108
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap
  65. 80
    0
      server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap
  66. 116
    0
      server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js
  67. 39
    0
      server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js
  68. 113
    0
      server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js
  69. 167
    0
      server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js
  70. 86
    0
      server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js
  71. 59
    0
      server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js
  72. 57
    0
      server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js
  73. 59
    0
      server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js
  74. 46
    0
      server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js
  75. 31
    0
      server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js
  76. 66
    0
      server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js
  77. 8
    15
      server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.js
  78. 27
    0
      server/sonar-web/src/main/js/components/issue/popups/__tests__/SetSeverityPopup-test.js
  79. 32
    0
      server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTransitionPopup-test.js
  80. 27
    0
      server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTypePopup-test.js
  81. 50
    0
      server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap
  82. 17
    0
      server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap
  83. 75
    0
      server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap
  84. 15
    0
      server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.js.snap
  85. 53
    0
      server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap
  86. 37
    0
      server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap
  87. 37
    0
      server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap
  88. 32
    1
      server/sonar-web/src/main/js/components/issue/types.js
  89. 36
    0
      server/sonar-web/src/main/js/components/shared/SeverityHelper.js
  90. 30
    0
      server/sonar-web/src/main/js/components/shared/SeverityIcon.js
  91. 37
    0
      server/sonar-web/src/main/js/components/shared/StatusHelper.js
  92. 27
    0
      server/sonar-web/src/main/js/components/shared/StatusIcon.js
  93. 1
    6
      server/sonar-web/src/main/js/components/tags/TagsList.css
  94. 3
    8
      server/sonar-web/src/main/js/components/tags/TagsList.js
  95. 2
    2
      server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.js
  96. 1
    1
      server/sonar-web/src/main/js/components/ui/IssueTypeIcon.js
  97. 2
    0
      server/sonar-web/src/main/js/helpers/testUtils.js
  98. 13
    0
      server/sonar-web/src/main/js/helpers/urls.js
  99. 12
    0
      server/sonar-web/src/main/less/components/bubble-popup.less
  100. 0
    0
      server/sonar-web/src/main/less/components/issues.less

+ 66
- 1
server/sonar-web/src/main/js/api/issues.js View File

@@ -18,7 +18,14 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import { getJSON, post } from '../helpers/request';
import { getJSON, post, postJSON } from '../helpers/request';

export type IssueResponse = {
components?: Array<*>,
issue: {},
rules?: Array<*>,
users?: Array<*>
};

type IssuesResponse = {
components?: Array<*>,
@@ -34,6 +41,15 @@ type IssuesResponse = {
users?: Array<*>
};

export type Transition =
| 'confirm'
| 'unconfirm'
| 'reopen'
| 'resolve'
| 'falsepositive'
| 'wontfix'
| 'close';

export const searchIssues = (query: {}): Promise<IssuesResponse> =>
getJSON('/api/issues/search', query);

@@ -83,11 +99,60 @@ export function getIssuesCount(query: {}): Promise<*> {

export const searchIssueTags = (ps: number = 500) => getJSON('/api/issues/tags', { ps });

export function getIssueChangelog(issue: string): Promise<*> {
const url = '/api/issues/changelog';
return getJSON(url, { issue }).then(r => r.changelog);
}

export function getIssueFilters() {
const url = '/api/issue_filters/search';
return getJSON(url).then(r => r.issueFilters);
}

export function addIssueComment(data: { issue: string, text: string }): Promise<IssueResponse> {
const url = '/api/issues/add_comment';
return postJSON(url, data);
}

export function deleteIssueComment(data: { comment: string }): Promise<IssueResponse> {
const url = '/api/issues/delete_comment';
return postJSON(url, data);
}

export function editIssueComment(data: { comment: string, text: string }): Promise<IssueResponse> {
const url = '/api/issues/edit_comment';
return postJSON(url, data);
}

export function setIssueAssignee(
data: { issue: string, assignee?: string }
): Promise<IssueResponse> {
const url = '/api/issues/assign';
return postJSON(url, data);
}

export function setIssueSeverity(data: { issue: string, severity: string }): Promise<*> {
const url = '/api/issues/set_severity';
return postJSON(url, data);
}

export function setIssueTags(data: { issue: string, tags: string }): Promise<IssueResponse> {
const url = '/api/issues/set_tags';
return postJSON(url, data);
}

export function setIssueTransition(
data: { issue: string, transition: Transition }
): Promise<IssueResponse> {
const url = '/api/issues/do_transition';
return postJSON(url, data);
}

export function setIssueType(data: { issue: string, type: string }): Promise<IssueResponse> {
const url = '/api/issues/set_type';
return postJSON(url, data);
}

export const bulkChangeIssues = (issueKeys: Array<string>, query: {}) =>
post('/api/issues/bulk_change', {
issues: issueKeys.join(),

+ 94
- 0
server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js View File

@@ -0,0 +1,94 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { orderBy, uniq, without } from 'lodash';
import FacetBox from './components/FacetBox';
import FacetHeader from './components/FacetHeader';
import FacetItem from './components/FacetItem';
import FacetItemsList from './components/FacetItemsList';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import { translate } from '../../../helpers/l10n';

type Props = {|
onChange: (changes: { [string]: Array<string> }) => void,
onToggle: (property: string) => void,
open: boolean,
severities: Array<string>,
stats?: { [string]: number }
|};

export default class SeverityFacet extends React.PureComponent {
props: Props;

static defaultProps = {
open: true
};

property = 'severities';

handleItemClick = (itemValue: string) => {
const { severities } = this.props;
const newValue = orderBy(
severities.includes(itemValue)
? without(severities, itemValue)
: uniq([...severities, itemValue])
);
this.props.onChange({ [this.property]: newValue });
};

handleHeaderClick = () => {
this.props.onToggle(this.property);
};

getStat(severity: string): ?number {
const { stats } = this.props;
return stats ? stats[severity] : null;
}

render() {
const severities = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR'];

return (
<FacetBox property={this.property}>
<FacetHeader
hasValue={this.props.severities.length > 0}
name={translate('issues.facet', this.property)}
onClick={this.handleHeaderClick}
open={this.props.open}
/>

<FacetItemsList open={this.props.open}>
{severities.map(severity => (
<FacetItem
active={this.props.severities.includes(severity)}
halfWidth={true}
key={severity}
name={<SeverityHelper severity={severity} />}
onClick={this.handleItemClick}
stat={this.getStat(severity)}
value={severity}
/>
))}
</FacetItemsList>
</FacetBox>
);
}
}

+ 1
- 5
server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js View File

@@ -123,11 +123,7 @@ export default class MetaTags extends React.PureComponent {
} else {
return (
<div className="overview-meta-card overview-meta-tags">
<TagsList
tags={tags.length ? tags : [translate('no_tags')]}
allowUpdate={false}
allowMultiLine={true}
/>
<TagsList tags={tags.length ? tags : [translate('no_tags')]} allowUpdate={false} />
</div>
);
}

+ 0
- 5
server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap View File

@@ -5,7 +5,6 @@ exports[`test should open the tag selector on click 1`] = `
className="button-link"
onClick={[Function]}>
<TagsList
allowMultiLine={false}
allowUpdate={true}
tags={
Array [
@@ -24,7 +23,6 @@ exports[`test should open the tag selector on click 2`] = `
className="button-link"
onClick={[Function]}>
<TagsList
allowMultiLine={false}
allowUpdate={true}
tags={
Array [
@@ -59,7 +57,6 @@ exports[`test should open the tag selector on click 3`] = `
className="button-link"
onClick={[Function]}>
<TagsList
allowMultiLine={false}
allowUpdate={true}
tags={
Array [
@@ -78,7 +75,6 @@ exports[`test should render with tags and admin rights 1`] = `
className="button-link"
onClick={[Function]}>
<TagsList
allowMultiLine={false}
allowUpdate={true}
tags={
Array [
@@ -94,7 +90,6 @@ exports[`test should render without tags and admin rights 1`] = `
<div
className="overview-meta-card overview-meta-tags">
<TagsList
allowMultiLine={true}
allowUpdate={false}
tags={
Array [

+ 0
- 2
server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js View File

@@ -26,7 +26,6 @@ import { searchProjectTags } from '../../../api/components';
import { setProjectTags } from '../store/actions';

type Props = {
open: boolean,
position: {},
project: string,
selectedTags: Array<string>,
@@ -75,7 +74,6 @@ class ProjectTagsSelectorContainer extends React.PureComponent {
render() {
return (
<TagsSelector
open={this.props.open}
position={this.props.position}
tags={this.state.searchResult}
selectedTags={this.props.selectedTags}

+ 0
- 1
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.js.snap View File

@@ -60,7 +60,6 @@ exports[`test should display tags 1`] = `
</Link>
</h2>
<TagsList
allowMultiLine={false}
allowUpdate={false}
customClass="spacer-left"
tags={

+ 1
- 1
server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js View File

@@ -19,7 +19,7 @@
*/
// @flow
import React from 'react';
import SeverityHelper from '../../../components/shared/severity-helper';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import { translate } from '../../../helpers/l10n';

type Props = {

+ 1
- 1
server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js View File

@@ -20,7 +20,7 @@
import { shallow } from 'enzyme';
import React from 'react';
import SeverityChange from '../SeverityChange';
import SeverityHelper from '../../../../components/shared/severity-helper';
import SeverityHelper from '../../../../components/shared/SeverityHelper';

it('should render SeverityHelper', () => {
const output = shallow(<SeverityChange severity="BLOCKER" />).find(SeverityHelper);

+ 1
- 1
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js View File

@@ -21,7 +21,7 @@
import React from 'react';
import { Link } from 'react-router';
import ComparisonEmpty from './ComparisonEmpty';
import SeverityIcon from '../../../components/shared/severity-icon';
import SeverityIcon from '../../../components/shared/SeverityIcon';
import { translateWithParameters } from '../../../helpers/l10n';
import { getRulesUrl } from '../../../helpers/urls';


+ 1
- 1
server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js View File

@@ -22,7 +22,7 @@ import React from 'react';
import { Link } from 'react-router';
import ComparisonResults from '../ComparisonResults';
import ComparisonEmpty from '../ComparisonEmpty';
import SeverityIcon from '../../../../components/shared/severity-icon';
import SeverityIcon from '../../../../components/shared/SeverityIcon';

it('should render ComparisonEmpty', () => {
const output = shallow(

+ 1
- 1
server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js View File

@@ -20,7 +20,7 @@
// @flow
import React from 'react';
import classNames from 'classnames';
import SeverityIcon from '../../shared/severity-icon';
import SeverityIcon from '../../shared/SeverityIcon';
import { sortBySeverity } from '../../../helpers/issues';
import type { SourceLine } from '../types';


+ 2
- 2
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap View File

@@ -11,7 +11,7 @@ exports[`test render highest severity 1`] = `
onClick={[Function]}
role="button"
tabIndex="0">
<severity-icon
<SeverityIcon
severity="CRITICAL" />
<span
className="source-line-issues-counter">
@@ -27,7 +27,7 @@ exports[`test render highest severity 2`] = `
onClick={[Function]}
role="button"
tabIndex="0">
<severity-icon
<SeverityIcon
severity="MINOR" />
<span
className="source-line-issues-counter">

+ 2
- 2
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap View File

@@ -1,11 +1,11 @@
exports[`test render issues list 1`] = `
<div
className="issue-list">
<Connect(Connect(Issue))
<Connect(BaseIssue)
issueKey="foo"
onClick={[Function]}
selected={true} />
<Connect(Connect(Issue))
<Connect(BaseIssue)
issueKey="bar"
onClick={[Function]}
selected={false} />

+ 109
- 0
server/sonar-web/src/main/js/components/common/BubblePopupHelper.js View File

@@ -0,0 +1,109 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import classNames from 'classnames';

type Props = {
className?: string,
children: React.Component<*>,
isOpen: boolean,
offset?: {
vertical: number,
horizontal: number
},
popup: React.Component<*>,
position: 'bottomleft' | 'bottomright',
togglePopup: (?boolean) => void
};

type State = {
position: { top: number, right: number }
};

export default class BubblePopupHelper extends React.PureComponent {
props: Props;
state: State = {
position: {
top: 0,
right: 0
}
};

componentDidMount() {
this.setState({ position: this.getPosition(this.props) });
}

componentWillReceiveProps(nextProps: Props) {
if (!this.props.isOpen && nextProps.isOpen) {
window.addEventListener('keydown', this.handleKey, false);
window.addEventListener('click', this.handleOutsideClick, false);
} else if (this.props.isOpen && !nextProps.isOpen) {
window.removeEventListener('keydown', this.handleKey);
window.removeEventListener('click', this.handleOutsideClick);
}
}

handleKey = (evt: KeyboardEvent) => {
// Escape key
if (evt.keyCode === 27) {
this.props.togglePopup(false);
}
};

handleOutsideClick = (evt: SyntheticInputEvent) => {
if (!this.popupContainer || !this.popupContainer.contains(evt.target)) {
this.props.togglePopup(false);
}
};

handleClick(evt: SyntheticInputEvent) {
evt.stopPropagation();
}

getPosition(props: Props) {
const containerPos = this.container.getBoundingClientRect();
const { position } = props;
const offset = props.offset || { vertical: 0, horizontal: 0 };
if (position === 'bottomleft') {
return { top: containerPos.height + offset.vertical, left: offset.horizontal };
} else if (position === 'bottomright') {
return { top: containerPos.height + offset.vertical, right: offset.horizontal };
}
}

render() {
return (
<div
className={classNames(this.props.className, 'bubble-popup-helper')}
ref={container => this.container = container}
onClick={this.handleClick}
tabIndex={0}
role="tooltip">
{this.props.children}
{this.props.isOpen &&
<div ref={popupContainer => this.popupContainer = popupContainer}>
{React.cloneElement(this.props.popup, {
popupPosition: this.state.position
})}
</div>}
</div>
);
}
}

+ 44
- 0
server/sonar-web/src/main/js/components/common/MarkdownTips.js View File

@@ -0,0 +1,44 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { getMarkdownHelpUrl } from '../../helpers/urls';
import { translate } from '../../helpers/l10n';

export default class MarkdownTips extends React.PureComponent {
handleClick(evt: MouseEvent) {
evt.preventDefault();
window.open(getMarkdownHelpUrl(), 'height=300,width=600,scrollbars=1,resizable=1');
}

render() {
return (
<div className="markdown-tips">
<a className="little-spacer-right" href="#" onClick={this.handleClick}>
{translate('markdown.helplink')}
</a>
{':'}
<span className="spacer-left">*{translate('bold')}*</span>
<span className="spacer-left">``{translate('code')}``</span>
<span className="spacer-left">* {translate('bulleted_point')}</span>
</div>
);
}
}

+ 6
- 0
server/sonar-web/src/main/js/components/common/MultiSelect.js View File

@@ -112,12 +112,18 @@ export default class MultiSelect extends React.PureComponent {
switch (evt.keyCode) {
case 40: // down
this.setState(this.selectNextElement);
evt.stopPropagation();
evt.preventDefault();
break;
case 38: // up
this.setState(this.selectPreviousElement);
evt.stopPropagation();
evt.preventDefault();
break;
case 37: // left
case 39: // right
evt.stopPropagation();
break;
case 13: // return
if (this.state.activeIdx >= 0) {
this.toggleSelect(this.getAllElements(this.props, this.state)[this.state.activeIdx]);

+ 133
- 0
server/sonar-web/src/main/js/components/common/SelectList.js View File

@@ -0,0 +1,133 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import SelectListItem from './SelectListItem';

type Props = {
children?: SelectListItem,
items: Array<string>,
currentItem: string,
onSelect: (string) => void
};

type State = {
active: string
};

export default class SelectList extends React.PureComponent {
list: HTMLElement;
props: Props;
state: State;

constructor(props: Props) {
super(props);
this.state = {
active: props.currentItem
};
}

componentDidMount() {
this.list.focus();
}

componentWillReceiveProps(nextProps: Props) {
if (
nextProps.currentItem !== this.props.currentItem &&
!nextProps.items.includes(this.state.active)
) {
this.setState({ active: nextProps.currentItem });
}
}

handleKeyboard = (evt: KeyboardEvent) => {
switch (evt.keyCode) {
case 40: // down
this.setState(this.selectNextElement);
break;
case 38: // up
this.setState(this.selectPreviousElement);
break;
case 13: // return
if (this.state.active) {
this.handleSelect(this.state.active);
}
break;
default:
return;
}
evt.preventDefault();
evt.stopPropagation();
};

handleSelect = (item: string) => {
this.props.onSelect(item);
};

handleHover = (item: string) => {
this.setState({ active: item });
};

selectNextElement = (state: State, props: Props) => {
const idx = props.items.indexOf(state.active);
if (idx < 0) {
return { active: props.items[0] };
}
return { active: props.items[(idx + 1) % props.items.length] };
};

selectPreviousElement = (state: State, props: Props) => {
const idx = props.items.indexOf(state.active);
if (idx <= 0) {
return { active: props.items[props.items.length - 1] };
}
return { active: props.items[idx - 1] };
};

render() {
const { children } = this.props;
const hasChildren = React.Children.count(children) > 0;
return (
<ul
className="menu"
onKeyDown={this.handleKeyboard}
ref={list => this.list = list}
tabIndex={0}>
{hasChildren &&
React.Children.map(children, child =>
React.cloneElement(child, {
active: this.state.active,
onHover: this.handleHover,
onSelect: this.handleSelect
}))}
{!hasChildren &&
this.props.items.map(item => (
<SelectListItem
active={this.state.active}
item={item}
key={item}
onHover={this.handleHover}
onSelect={this.handleSelect}
/>
))}
</ul>
);
}
}

+ 76
- 0
server/sonar-web/src/main/js/components/common/SelectListItem.js View File

@@ -0,0 +1,76 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import classNames from 'classnames';
import Tooltip from '../controls/Tooltip';

type Props = {
active?: string,
children?: React.Component<*>,
item: string,
onSelect?: (string) => void,
onHover?: (string) => void,
title?: string
};

export default class SelectListItem extends React.PureComponent {
props: Props;

handleSelect = (evt: SyntheticInputEvent) => {
evt.preventDefault();
this.props.onSelect && this.props.onSelect(this.props.item);
};

handleHover = () => {
this.props.onHover && this.props.onHover(this.props.item);
};

renderLink() {
let children = this.props.item;
if (this.props.hasOwnProperty('children')) {
children = this.props.children;
}
return (
<li>
<a
href="#"
className={classNames({ active: this.props.active === this.props.item })}
onClick={this.handleSelect}
onMouseOver={this.handleHover}
onFocus={this.handleHover}>
{children}
</a>
</li>
);
}

render() {
if (this.props.title) {
return (
<Tooltip placement="right" overlay={this.props.title}>
{this.renderLink()}
</Tooltip>
);
} else {
return this.renderLink();
}
}
}

+ 139
- 0
server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js View File

@@ -0,0 +1,139 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow, mount } from 'enzyme';
import React from 'react';
import BubblePopupHelper from '../BubblePopupHelper';
import BubblePopup from '../BubblePopup';
import { click } from '../../../helpers/testUtils';

it('should render an open popup on the right', () => {
const toggle = jest.fn();
const popup = shallow(
<BubblePopupHelper
isOpen={true}
position="bottomright"
togglePopup={toggle}
popup={
<BubblePopup>
<span>test</span>
</BubblePopup>
}>
<button onClick={toggle}>open</button>
</BubblePopupHelper>
);
expect(popup).toMatchSnapshot();
});

it('should render the popup helper with a closed popup', () => {
const toggle = jest.fn();
const popup = shallow(
<BubblePopupHelper
isOpen={false}
position="bottomright"
togglePopup={toggle}
popup={
<BubblePopup>
<span>test</span>
</BubblePopup>
}>
<button onClick={toggle}>open</button>
</BubblePopupHelper>
);
expect(popup).toMatchSnapshot();
});

it('should render with custom classes', () => {
const toggle = jest.fn();
const popup = shallow(
<BubblePopupHelper
customClass="myhelperclass"
isOpen={true}
position="bottomright"
togglePopup={toggle}
popup={
<BubblePopup customClass="mypopupclass">
<span>test</span>
</BubblePopup>
}>
<button onClick={toggle}>open</button>
</BubblePopupHelper>
);
expect(popup).toMatchSnapshot();
});

it('should render the popup with offset', () => {
const toggle = jest.fn();
const popup = mount(
<BubblePopupHelper
isOpen={true}
offset={{ vertical: 5, horizontal: 2 }}
position="bottomright"
togglePopup={toggle}
popup={
<BubblePopup>
<span>test</span>
</BubblePopup>
}>
<button onClick={toggle}>open</button>
</BubblePopupHelper>
);
expect(popup.find('BubblePopup')).toMatchSnapshot();
});

it('should render an open popup on the left', () => {
const toggle = jest.fn();
const popup = mount(
<BubblePopupHelper
isOpen={true}
offset={{ vertical: 0, horizontal: 2 }}
position="bottomleft"
togglePopup={toggle}
popup={
<BubblePopup>
<span>test</span>
</BubblePopup>
}>
<button onClick={toggle}>open</button>
</BubblePopupHelper>
);
expect(popup.find('BubblePopup')).toMatchSnapshot();
});

it('should correctly handle clicks on the button', () => {
const toggle = jest.fn(() => popup.setProps({ isOpen: !popup.props().isOpen }));
const popup = shallow(
<BubblePopupHelper
isOpen={false}
offset={{ vertical: 0, horizontal: 2 }}
position="bottomleft"
togglePopup={toggle}
popup={
<BubblePopup>
<span>test</span>
</BubblePopup>
}>
<button onClick={toggle}>open</button>
</BubblePopupHelper>
);
expect(popup).toMatchSnapshot();
click(popup.find('button'));
expect(toggle.mock.calls.length).toBe(1);
expect(popup).toMatchSnapshot();
});

server/sonar-web/src/main/js/components/shared/severity-icon.js → server/sonar-web/src/main/js/components/common/__tests__/MarkdownTips-test.js View File

@@ -17,14 +17,10 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import MarkdownTips from '../MarkdownTips';

export default React.createClass({
render() {
if (!this.props.severity) {
return null;
}
const className = 'icon-severity-' + this.props.severity.toLowerCase();
return <i className={className} />;
}
it('should render the tips', () => {
expect(shallow(<MarkdownTips />)).toMatchSnapshot();
});

+ 75
- 0
server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js View File

@@ -0,0 +1,75 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow, mount } from 'enzyme';
import React from 'react';
import SelectList from '../SelectList';
import SelectListItem from '../SelectListItem';
import { click, keydown } from '../../../helpers/testUtils';

it('should render correctly without children', () => {
const onSelect = jest.fn();
expect(
shallow(
<SelectList
items={['item', 'seconditem', 'third']}
currentItem="seconditem"
onSelect={onSelect}
/>
)
).toMatchSnapshot();
});

it('should render correctly with children', () => {
const onSelect = jest.fn();
const items = ['item', 'seconditem', 'third'];
expect(
shallow(
<SelectList items={items} currentItem="seconditem" onSelect={onSelect}>
{items.map(item => (
<SelectListItem key={item} item={item}>
<i className="myicon" />item
</SelectListItem>
))}
</SelectList>
)
).toMatchSnapshot();
});

it('should correclty handle user actions', () => {
const onSelect = jest.fn();
const items = ['item', 'seconditem', 'third'];
const list = mount(
<SelectList items={items} currentItem="seconditem" onSelect={onSelect}>
{items.map(item => (
<SelectListItem key={item} item={item}>
<i className="myicon" />item
</SelectListItem>
))}
</SelectList>
);
keydown(list.find('ul'), 40);
expect(list.state()).toMatchSnapshot();
keydown(list.find('ul'), 40);
expect(list.state()).toMatchSnapshot();
keydown(list.find('ul'), 38);
expect(list.state()).toMatchSnapshot();
click(list.childAt(2).find('a'));
expect(onSelect.mock.calls).toMatchSnapshot(); // eslint-disable-linelist
});

+ 44
- 0
server/sonar-web/src/main/js/components/common/__tests__/SelectListItem-test.js View File

@@ -0,0 +1,44 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import SelectListItem from '../SelectListItem';

it('should render correctly without children', () => {
expect(shallow(<SelectListItem item="myitem" />)).toMatchSnapshot();
});

it('should render correctly with children', () => {
expect(
shallow(
<SelectListItem active="myitem" item="seconditem">
<i className="custom-icon" /><p>seconditem</p>
</SelectListItem>
)
).toMatchSnapshot();
});

it('should render correctly with a tooltip', () => {
expect(shallow(<SelectListItem item="myitem" title="my custom tooltip" />)).toMatchSnapshot();
});

it('should render with the active class', () => {
expect(shallow(<SelectListItem active="myitem" item="myitem" />)).toMatchSnapshot();
});

+ 148
- 0
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap View File

@@ -0,0 +1,148 @@
exports[`test should correctly handle clicks on the button 1`] = `
<div
className="bubble-popup-helper"
onClick={[Function]}
role="tooltip"
tabIndex={0}>
<button
onClick={[Function]}>
open
</button>
</div>
`;

exports[`test should correctly handle clicks on the button 2`] = `
<div
className="bubble-popup-helper"
onClick={[Function]}
role="tooltip"
tabIndex={0}>
<button
onClick={[Function]}>
open
</button>
<div>
<BubblePopup
customClass=""
popupPosition={
Object {
"right": 0,
"top": 0,
}
}>
<span>
test
</span>
</BubblePopup>
</div>
</div>
`;

exports[`test should render an open popup on the left 1`] = `
<BubblePopup
customClass=""
popupPosition={
Object {
"left": 2,
"top": 0,
}
}>
<div
className="bubble-popup"
style={Object {}}>
<span>
test
</span>
<div
className="bubble-popup-arrow" />
</div>
</BubblePopup>
`;

exports[`test should render an open popup on the right 1`] = `
<div
className="bubble-popup-helper"
onClick={[Function]}
role="tooltip"
tabIndex={0}>
<button
onClick={[Function]}>
open
</button>
<div>
<BubblePopup
customClass=""
popupPosition={
Object {
"right": 0,
"top": 0,
}
}>
<span>
test
</span>
</BubblePopup>
</div>
</div>
`;

exports[`test should render the popup helper with a closed popup 1`] = `
<div
className="bubble-popup-helper"
onClick={[Function]}
role="tooltip"
tabIndex={0}>
<button
onClick={[Function]}>
open
</button>
</div>
`;

exports[`test should render the popup with offset 1`] = `
<BubblePopup
customClass=""
popupPosition={
Object {
"right": 2,
"top": 5,
}
}>
<div
className="bubble-popup"
style={Object {}}>
<span>
test
</span>
<div
className="bubble-popup-arrow" />
</div>
</BubblePopup>
`;

exports[`test should render with custom classes 1`] = `
<div
className="bubble-popup-helper"
onClick={[Function]}
role="tooltip"
tabIndex={0}>
<button
onClick={[Function]}>
open
</button>
<div>
<BubblePopup
customClass="mypopupclass"
popupPosition={
Object {
"right": 0,
"top": 0,
}
}>
<span>
test
</span>
</BubblePopup>
</div>
</div>
`;

+ 29
- 0
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.js.snap View File

@@ -0,0 +1,29 @@
exports[`test should render the tips 1`] = `
<div
className="markdown-tips">
<a
className="little-spacer-right"
href="#"
onClick={[Function]}>
markdown.helplink
</a>
:
<span
className="spacer-left">
*
bold
*
</span>
<span
className="spacer-left">
\`\`
code
\`\`
</span>
<span
className="spacer-left">
*
bulleted_point
</span>
</div>
`;

+ 83
- 0
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap View File

@@ -0,0 +1,83 @@
exports[`test should correclty handle user actions 1`] = `
Object {
"active": "third",
}
`;

exports[`test should correclty handle user actions 2`] = `
Object {
"active": "item",
}
`;

exports[`test should correclty handle user actions 3`] = `
Object {
"active": "third",
}
`;

exports[`test should correclty handle user actions 4`] = `
Array [
Array [
"third",
],
]
`;

exports[`test should render correctly with children 1`] = `
<ul
className="menu"
onKeyDown={[Function]}
tabIndex={0}>
<SelectListItem
active="seconditem"
item="item"
onHover={[Function]}
onSelect={[Function]}>
<i
className="myicon" />
item
</SelectListItem>
<SelectListItem
active="seconditem"
item="seconditem"
onHover={[Function]}
onSelect={[Function]}>
<i
className="myicon" />
item
</SelectListItem>
<SelectListItem
active="seconditem"
item="third"
onHover={[Function]}
onSelect={[Function]}>
<i
className="myicon" />
item
</SelectListItem>
</ul>
`;

exports[`test should render correctly without children 1`] = `
<ul
className="menu"
onKeyDown={[Function]}
tabIndex={0}>
<SelectListItem
active="seconditem"
item="item"
onHover={[Function]}
onSelect={[Function]} />
<SelectListItem
active="seconditem"
item="seconditem"
onHover={[Function]}
onSelect={[Function]} />
<SelectListItem
active="seconditem"
item="third"
onHover={[Function]}
onSelect={[Function]} />
</ul>
`;

+ 59
- 0
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectListItem-test.js.snap View File

@@ -0,0 +1,59 @@
exports[`test should render correctly with a tooltip 1`] = `
<Tooltip
overlay="my custom tooltip"
placement="right">
<li>
<a
className=""
href="#"
onClick={[Function]}
onFocus={[Function]}
onMouseOver={[Function]}>
myitem
</a>
</li>
</Tooltip>
`;

exports[`test should render correctly with children 1`] = `
<li>
<a
className=""
href="#"
onClick={[Function]}
onFocus={[Function]}
onMouseOver={[Function]}>
<i
className="custom-icon" />
<p>
seconditem
</p>
</a>
</li>
`;

exports[`test should render correctly without children 1`] = `
<li>
<a
className=""
href="#"
onClick={[Function]}
onFocus={[Function]}
onMouseOver={[Function]}>
myitem
</a>
</li>
`;

exports[`test should render with the active class 1`] = `
<li>
<a
className="active"
href="#"
onClick={[Function]}
onFocus={[Function]}
onMouseOver={[Function]}>
myitem
</a>
</li>
`;

+ 3
- 2
server/sonar-web/src/main/js/components/controls/Checkbox.js View File

@@ -25,7 +25,8 @@ export default class Checkbox extends React.Component {
id: React.PropTypes.string,
onCheck: React.PropTypes.func.isRequired,
checked: React.PropTypes.bool.isRequired,
thirdState: React.PropTypes.bool
thirdState: React.PropTypes.bool,
className: React.PropTypes.string
};

static defaultProps = {
@@ -43,7 +44,7 @@ export default class Checkbox extends React.Component {
}

render() {
const className = classNames('icon-checkbox', {
const className = classNames(this.props.className, 'icon-checkbox', {
'icon-checkbox-checked': this.props.checked,
'icon-checkbox-single': this.props.thirdState
});

+ 7
- 0
server/sonar-web/src/main/js/components/controls/__tests__/Checkbox-test.js View File

@@ -57,3 +57,10 @@ it('should call onCheck with id as second parameter', () => {
click(checkbox);
expect(onCheck).toBeCalledWith(true, 'foo');
});

it('should apply custom class', () => {
const checkbox = shallow(
<Checkbox className="customclass" checked={true} onCheck={() => true} />
);
expect(checkbox.is('.customclass')).toBe(true);
});

+ 153
- 0
server/sonar-web/src/main/js/components/issue/BaseIssue.js View File

@@ -0,0 +1,153 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import IssueView from './IssueView';
import { setIssueAssignee } from '../../api/issues';
import type { Issue } from './types';

type Props = {
checked?: boolean,
issue: Issue,
onCheck?: () => void,
onClick: (string) => void,
onFail: (Error) => void,
onFilterClick?: () => void,
onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void,
selected: boolean
};

type State = {
currentPopup: string
};

export default class BaseIssue extends React.PureComponent {
mounted: boolean;
props: Props;
state: State;

static defaultProps = {
selected: false
};

constructor(props: Props) {
super(props);
this.state = {
currentPopup: ''
};
}

componentDidMount() {
this.mounted = true;
if (this.props.selected) {
this.bindShortcuts();
}
}

componentWillUpdate(nextProps: Props) {
if (!nextProps.selected && this.props.selected) {
this.unbindShortcuts();
}
}

componentDidUpdate(prevProps: Props) {
if (!prevProps.selected && this.props.selected) {
this.bindShortcuts();
}
}

componentWillUnmount() {
this.mounted = false;
if (this.props.selected) {
this.unbindShortcuts();
}
}

bindShortcuts() {
document.addEventListener('keypress', this.handleKeyPress);
}

unbindShortcuts() {
document.removeEventListener('keypress', this.handleKeyPress);
}

togglePopup = (popupName: string, open?: boolean) => {
if (this.mounted) {
this.setState((prevState: State) => {
if (prevState.currentPopup !== popupName && open !== false) {
return { currentPopup: popupName };
} else if (prevState.currentPopup === popupName && open !== true) {
return { currentPopup: '' };
}
return prevState;
});
}
};

handleAssignement = (login: string) => {
const { issue } = this.props;
if (issue.assignee !== login) {
this.props.onIssueChange(setIssueAssignee({ issue: issue.key, assignee: login }));
}
this.togglePopup('assign', false);
};

handleKeyPress = (e: Object) => {
const tagName = e.target.tagName.toUpperCase();
const shouldHandle = tagName !== 'INPUT' && tagName !== 'TEXTAREA' && tagName !== 'BUTTON';

if (shouldHandle) {
switch (e.key) {
case 'f':
return this.togglePopup('transition');
case 'a':
return this.togglePopup('assign');
case 'm':
return this.props.issue.actions.includes('assign_to_me') && this.handleAssignement('_me');
case 'p':
return this.togglePopup('plan');
case 'i':
return this.togglePopup('set-severity');
case 'c':
return this.togglePopup('comment');
case 't':
return this.togglePopup('edit-tags');
}
}
};

render() {
return (
<IssueView
issue={this.props.issue}
checked={this.props.checked}
onAssign={this.handleAssignement}
onCheck={this.props.onCheck}
onClick={this.props.onClick}
onFail={this.props.onFail}
onFilterClick={this.props.onFilterClick}
onIssueChange={this.props.onIssueChange}
togglePopup={this.togglePopup}
currentPopup={this.state.currentPopup}
selected={this.props.selected}
/>
);
}
}

+ 9
- 2
server/sonar-web/src/main/js/components/issue/ConnectedIssue.js View File

@@ -19,11 +19,18 @@
*/
// @flow
import { connect } from 'react-redux';
import Issue from './Issue';
import BaseIssue from './BaseIssue';
import { getIssueByKey } from '../../store/rootReducer';
import { onFail } from '../../store/rootActions';
import { updateIssue } from './actions';

const mapStateToProps = (state, ownProps) => ({
issue: getIssueByKey(state, ownProps.issueKey)
});

export default connect(mapStateToProps)(Issue);
const mapDispatchToProps = {
onIssueChange: updateIssue,
onFail: error => dispatch => onFail(dispatch)(error)
};

export default connect(mapStateToProps, mapDispatchToProps)(BaseIssue);

+ 7
- 127
server/sonar-web/src/main/js/components/issue/Issue.js View File

@@ -18,134 +18,14 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { connect } from 'react-redux';
import IssueView from './issue-view';
import IssueModel from './models/issue';
import { receiveIssues } from '../../store/issues/duck';
import type { Issue as IssueType } from './types';
import BaseIssue from './BaseIssue';
import { onFail } from '../../store/rootActions';
import { updateIssue } from './actions';

type Model = { toJSON: () => {} };

type Props = {
checked?: boolean,
issue: IssueType | Model,
onCheck?: () => void,
onClick: () => void,
onFilterClick?: () => void,
onIssueChange: ({}) => void,
selected: boolean
const mapDispatchToProps = {
onIssueChange: updateIssue,
onFail: error => dispatch => onFail(dispatch)(error)
};

class Issue extends React.PureComponent {
issueView: Object;
node: HTMLElement;
props: Props;

static defaultProps = {
selected: false
};

componentDidMount() {
this.renderIssueView();
if (this.props.selected) {
this.bindShortcuts();
}
}

componentWillUpdate(nextProps: Props) {
if (!nextProps.selected && this.props.selected) {
this.unbindShortcuts();
}
this.destroyIssueView();
}

componentDidUpdate(prevProps: Props) {
this.renderIssueView();
if (!prevProps.selected && this.props.selected) {
this.bindShortcuts();
}

// $FlowFixMe resolution doesn't exist in type `Model`
const { resolution } = this.props.issue;
if (!prevProps.issue.resolution && ['FALSE-POSITIVE', 'WONTFIX'].includes(resolution)) {
this.issueView.comment({ fromTransition: true });
}
}

componentWillUnmount() {
if (this.props.selected) {
this.unbindShortcuts();
}
this.destroyIssueView();
}

bindShortcuts() {
document.addEventListener('keypress', this.handleKeyPress);
}

unbindShortcuts() {
document.removeEventListener('keypress', this.handleKeyPress);
}

doIssueAction(action: string) {
this.issueView.$('.js-issue-' + action).click();
}

handleKeyPress = (e: Object) => {
const tagName = e.target.tagName.toUpperCase();
const shouldHandle = tagName !== 'INPUT' && tagName !== 'TEXTAREA' && tagName !== 'BUTTON';

if (shouldHandle) {
switch (e.key) {
case 'f':
return this.doIssueAction('transition');
case 'a':
return this.doIssueAction('assign');
case 'm':
return this.doIssueAction('assign-to-me');
case 'p':
return this.doIssueAction('plan');
case 'i':
return this.doIssueAction('set-severity');
case 'c':
return this.doIssueAction('comment');
case 't':
return this.doIssueAction('edit-tags');
}
}
};

destroyIssueView() {
this.issueView.destroy();
}

renderIssueView() {
const model = this.props.issue.toJSON ? this.props.issue : new IssueModel(this.props.issue);
this.issueView = new IssueView({
model,
checked: this.props.checked,
onCheck: this.props.onCheck,
onClick: this.props.onClick,
onFilterClick: this.props.onFilterClick,
onIssueChange: this.props.onIssueChange
});
this.issueView.render().$el.appendTo(this.node);
if (this.props.selected) {
this.issueView.select();
}
}

render() {
return <div className="issue-container" ref={node => this.node = node} />;
}
}

const onIssueChange = issue =>
dispatch => {
dispatch(receiveIssues([issue]));
};

const mapDispatchToProps = { onIssueChange };

export default connect(null, mapDispatchToProps)(Issue);
export default connect(null, mapDispatchToProps)(BaseIssue);

+ 121
- 0
server/sonar-web/src/main/js/components/issue/IssueView.js View File

@@ -0,0 +1,121 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import classNames from 'classnames';
import Checkbox from '../../components/controls/Checkbox';
import IssueTitleBar from './components/IssueTitleBar';
import IssueActionsBar from './components/IssueActionsBar';
import IssueCommentLine from './components/IssueCommentLine';
import { deleteIssueComment, editIssueComment } from '../../api/issues';
import type { Issue } from './types';

type Props = {
checked?: boolean,
currentPopup: string,
issue: Issue,
onAssign: (string) => void,
onCheck?: () => void,
onClick: (string) => void,
onFail: (Error) => void,
onFilterClick?: () => void,
onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void,
selected: boolean,
togglePopup: (string) => void
};

export default class IssueView extends React.PureComponent {
props: Props;

handleClick = (evt: MouseEvent) => {
evt.preventDefault();
if (this.props.onClick) {
this.props.onClick(this.props.issue.key);
}
};

editComment = (comment: string, text: string) => {
this.props.onIssueChange(editIssueComment({ comment, text }));
};

deleteComment = (comment: string) => {
this.props.onIssueChange(deleteIssueComment({ comment }));
};

render() {
const { issue } = this.props;

const hasCheckbox = this.props.onCheck != null;

const issueClass = classNames('issue', {
'issue-with-checkbox': hasCheckbox,
selected: this.props.selected
});

return (
<div
className={issueClass}
data-issue={issue.key}
onClick={this.handleClick}
tabIndex={0}
role="listitem">
<IssueTitleBar
issue={issue}
currentPopup={this.props.currentPopup}
onFail={this.props.onFail}
onFilterClick={this.props.onFilterClick}
togglePopup={this.props.togglePopup}
/>
<IssueActionsBar
issue={issue}
currentPopup={this.props.currentPopup}
onAssign={this.props.onAssign}
onFail={this.props.onFail}
togglePopup={this.props.togglePopup}
onIssueChange={this.props.onIssueChange}
/>
{issue.comments &&
issue.comments.length > 0 &&
<div className="issue-comments">
{issue.comments.map(comment => (
<IssueCommentLine
comment={comment}
key={comment.key}
onEdit={this.editComment}
onDelete={this.deleteComment}
/>
))}
</div>}
<a className="issue-navigate js-issue-navigate">
<i className="issue-navigate-to-left icon-chevron-left" />
<i className="issue-navigate-to-right icon-chevron-right" />
</a>
{hasCheckbox &&
<div className="js-toggle issue-checkbox-container">
<Checkbox
className="issue-checkbox"
onCheck={this.props.onCheck}
checked={this.props.checked}
/>
</div>}
</div>
);
}
}

+ 52
- 0
server/sonar-web/src/main/js/components/issue/actions.js View File

@@ -0,0 +1,52 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import type { Dispatch } from 'redux';
import type { Issue } from './types';
import { onFail } from '../../store/rootActions';
import { receiveIssues } from '../../store/issues/duck';
import { parseIssueFromResponse } from '../../helpers/issues';

export const updateIssue = (resultPromise: Promise<*>, oldIssue?: Issue, newIssue?: Issue) =>
(dispatch: Dispatch<*>) => {
if (oldIssue && newIssue) {
dispatch(receiveIssues([newIssue]));
}
resultPromise.then(
response => {
dispatch(
receiveIssues([
parseIssueFromResponse(
response.issue,
response.components,
response.users,
response.rules
)
])
);
},
error => {
onFail(dispatch)(error);
if (oldIssue && newIssue) {
dispatch(receiveIssues([oldIssue]));
}
}
);
};

+ 164
- 0
server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js View File

@@ -0,0 +1,164 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import IssueAssign from './IssueAssign';
import IssueCommentAction from './IssueCommentAction';
import IssueSeverity from './IssueSeverity';
import IssueTags from './IssueTags';
import IssueTransition from './IssueTransition';
import IssueType from './IssueType';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import type { Issue } from '../types';

type Props = {
issue: Issue,
currentPopup: string,
onAssign: (string) => void,
onFail: (Error) => void,
onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void,
togglePopup: (string) => void
};

type State = {
commentPlaceholder: string
};

export default class IssueActionsBar extends React.PureComponent {
props: Props;
state: State = {
commentPlaceholder: ''
};

componentDidUpdate(prevProps: Props) {
const { resolution } = this.props.issue;
if (!prevProps.issue.resolution && ['FALSE-POSITIVE', 'WONTFIX'].includes(resolution)) {
this.toggleComment(true, translate('issue.comment.tell_why'));
}
}

setIssueProperty = (
property: string,
popup: string,
apiCall: (Object) => Promise<*>,
value: string
) => {
const { issue } = this.props;
if (issue[property] !== value) {
const newIssue = { ...issue, [property]: value };
this.props.onIssueChange(apiCall({ issue: issue.key, [property]: value }), issue, newIssue);
}
this.props.togglePopup(popup, false);
};

toggleComment = (open?: boolean, placeholder?: string) => {
this.setState({
commentPlaceholder: placeholder || ''
});
this.props.togglePopup('comment', open);
};

render() {
const { issue } = this.props;
const canAssign = issue.actions.includes('assign');
const canComment = issue.actions.includes('comment');
const canSetSeverity = issue.actions.includes('set_severity');
const canSetTags = issue.actions.includes('set_tags');
const hasTransitions = issue.transitions && issue.transitions.length > 0;

return (
<table className="issue-table">
<tbody>
<tr>
<td>
<ul className="list-inline issue-meta-list">
<li className="issue-meta">
<IssueType
isOpen={this.props.currentPopup === 'set-type' && canSetSeverity}
issue={issue}
canSetSeverity={canSetSeverity}
togglePopup={this.props.togglePopup}
setIssueProperty={this.setIssueProperty}
/>
</li>
<li className="issue-meta">
<IssueSeverity
isOpen={this.props.currentPopup === 'set-severity' && canSetSeverity}
issue={issue}
canSetSeverity={canSetSeverity}
togglePopup={this.props.togglePopup}
setIssueProperty={this.setIssueProperty}
/>
</li>
<li className="issue-meta">
<IssueTransition
isOpen={this.props.currentPopup === 'transition' && hasTransitions}
issue={issue}
hasTransitions={hasTransitions}
togglePopup={this.props.togglePopup}
setIssueProperty={this.setIssueProperty}
/>
</li>
<li className="issue-meta">
<IssueAssign
isOpen={this.props.currentPopup === 'assign' && canAssign}
issue={issue}
canAssign={canAssign}
onAssign={this.props.onAssign}
onFail={this.props.onFail}
togglePopup={this.props.togglePopup}
/>
</li>
{issue.effort &&
<li className="issue-meta">
<span className="issue-meta-label">
{translateWithParameters('issue.x_effort', issue.effort)}
</span>
</li>}
{canComment &&
<IssueCommentAction
issueKey={issue.key}
commentPlaceholder={this.state.commentPlaceholder}
currentPopup={this.props.currentPopup}
onIssueChange={this.props.onIssueChange}
toggleComment={this.toggleComment}
/>}
</ul>
</td>
<td className="issue-table-meta-cell">
<ul className="list-inline">
<li className="issue-meta js-issue-tags">
<IssueTags
isOpen={this.props.currentPopup === 'edit-tags' && canSetTags}
canSetTags={canSetTags}
issue={issue}
onFail={this.props.onFail}
onIssueChange={this.props.onIssueChange}
togglePopup={this.props.togglePopup}
/>
</li>
</ul>
</td>
</tr>
</tbody>
</table>
);
}
}

+ 85
- 0
server/sonar-web/src/main/js/components/issue/components/IssueAssign.js View File

@@ -0,0 +1,85 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import Avatar from '../../../components/ui/Avatar';
import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import SetAssigneePopup from '../popups/SetAssigneePopup';
import { translate } from '../../../helpers/l10n';
import type { Issue } from '../types';

type Props = {
isOpen: boolean,
issue: Issue,
canAssign: boolean,
onAssign: (string) => void,
onFail: (Error) => void,
togglePopup: (string) => void
};

export default class IssueAssign extends React.PureComponent {
props: Props;

toggleAssign = (open?: boolean) => {
this.props.togglePopup('assign', open);
};

renderAssignee() {
const { issue } = this.props;
return (
<span>
{issue.assignee &&
<span className="text-top">
<Avatar className="little-spacer-right" hash={issue.assigneeAvatar} size={16} />
</span>}
<span className="issue-meta-label">
{issue.assignee ? issue.assigneeName : translate('unassigned')}
</span>
</span>
);
}

render() {
if (this.props.canAssign) {
return (
<BubblePopupHelper
isOpen={this.props.isOpen && this.props.canAssign}
position="bottomleft"
togglePopup={this.toggleAssign}
popup={
<SetAssigneePopup
issue={this.props.issue}
onFail={this.props.onFail}
onSelect={this.props.onAssign}
/>
}>
<button
className="button-link issue-action issue-action-with-options js-issue-assign"
onClick={this.toggleAssign}>
{this.renderAssignee()}
<i className="little-spacer-left icon-dropdown" />
</button>
</BubblePopupHelper>
);
} else {
return this.renderAssignee();
}
}
}

+ 65
- 0
server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js View File

@@ -0,0 +1,65 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import moment from 'moment';
import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import ChangelogPopup from '../popups/ChangelogPopup';
import type { Issue } from '../types';

type Props = {
isOpen: boolean,
issue: Issue,
creationDate: string,
togglePopup: (string) => void,
onFail: (Error) => void
};

export default class IssueChangelog extends React.PureComponent {
props: Props;

handleClick = (evt: SyntheticInputEvent) => {
evt.preventDefault();
this.toggleChangelog();
};

toggleChangelog = (open?: boolean) => {
this.props.togglePopup('changelog', open);
};

render() {
const momentCreationDate = moment(this.props.creationDate);
return (
<BubblePopupHelper
isOpen={this.props.isOpen}
position="bottomright"
togglePopup={this.toggleChangelog}
popup={<ChangelogPopup issue={this.props.issue} onFail={this.props.onFail} />}>
<button
className="button-link issue-action issue-action-with-options js-issue-show-changelog"
title={momentCreationDate.format('LLL')}
onClick={this.handleClick}>
<span className="issue-meta-label">{momentCreationDate.fromNow()}</span>
<i className="icon-dropdown little-spacer-left" />
</button>
</BubblePopupHelper>
);
}
}

+ 71
- 0
server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.js View File

@@ -0,0 +1,71 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { formatMeasure } from '../../../helpers/measures';
import { translate, translateWithParameters } from '../../../helpers/l10n';

export type ChangelogDiff = {
key: string,
oldValue?: string,
newValue?: string
};

export default function IssueChangelogDiff(props: { diff: ChangelogDiff }) {
const { diff } = props;
if (diff.key === 'file') {
return (
<p>
{translateWithParameters(
'issue.change.file_move',
diff.oldValue || '',
diff.newValue || ''
)}
</p>
);
}

let message: string;
if (diff.newValue != null) {
let newValue: string = diff.newValue;
if (diff.key === 'effort') {
newValue = formatMeasure(diff.newValue, 'WORK_DUR');
}
message = translateWithParameters(
'issue.changelog.changed_to',
translate('issue.changelog.field', diff.key),
newValue
);
} else {
message = translateWithParameters(
'issue.changelog.removed',
translate('issue.changelog.field', diff.key)
);
}

if (diff.oldValue != null) {
let oldValue: string = diff.oldValue;
if (diff.key === 'effort') {
oldValue = formatMeasure(diff.oldValue, 'WORK_DUR');
}
message += ` (${translateWithParameters('issue.changelog.was', oldValue)})`;
}
return <p>{message}</p>;
}

+ 72
- 0
server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js View File

@@ -0,0 +1,72 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import CommentPopup from '../popups/CommentPopup';
import { addIssueComment } from '../../../api/issues';
import { translate } from '../../../helpers/l10n';
import type { Issue } from '../types';

type Props = {
issueKey: string,
commentPlaceholder: string,
currentPopup: string,
onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void,
toggleComment: (open?: boolean, placeholder?: string) => void
};

export default class IssueCommentAction extends React.PureComponent {
props: Props;

addComment = (text: string) => {
this.props.onIssueChange(addIssueComment({ issue: this.props.issueKey, text }));
this.props.toggleComment(false);
};

handleCommentClick = () => this.props.toggleComment();

render() {
return (
<li className="issue-meta">
<BubblePopupHelper
isOpen={this.props.currentPopup === 'comment'}
position="bottomleft"
togglePopup={this.props.toggleComment}
popup={
<CommentPopup
customClass="issue-comment-bubble-popup"
placeholder={this.props.commentPlaceholder}
onComment={this.addComment}
toggleComment={this.props.toggleComment}
/>
}>
<button
className="button-link issue-action js-issue-comment"
onClick={this.handleCommentClick}>
<span className="issue-meta-label">
{translate('issue.comment.formlink')}
</span>
</button>
</BubblePopupHelper>
</li>
);
}
}

+ 122
- 0
server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js View File

@@ -0,0 +1,122 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import moment from 'moment';
import Avatar from '../../../components/ui/Avatar';
import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import CommentDeletePopup from '../popups/CommentDeletePopup';
import CommentPopup from '../popups/CommentPopup';
import type { IssueComment } from '../types';

type Props = {
comment: IssueComment,
onDelete: (string) => void,
onEdit: (string, string) => void
};

type State = {
openPopup: string
};

export default class IssueCommentLine extends React.PureComponent {
props: Props;
state: State = {
openPopup: ''
};

handleEdit = (text: string) => {
this.props.onEdit(this.props.comment.key, text);
this.toggleEditPopup(false);
};

handleDelete = () => {
this.props.onDelete(this.props.comment.key);
this.toggleDeletePopup(false);
};

togglePopup = (popupName: string, force?: boolean) => {
this.setState((prevState: State) => {
if (prevState.openPopup !== popupName && force !== false) {
return { openPopup: popupName };
} else if (prevState.openPopup === popupName && force !== true) {
return { openPopup: '' };
}
return prevState;
});
};

toggleDeletePopup = (force?: boolean) => this.togglePopup('delete', force);

toggleEditPopup = (force?: boolean) => this.togglePopup('edit', force);

render() {
const { comment } = this.props;
return (
<div className="issue-comment">
<div className="issue-comment-author" title={comment.authorName}>
<Avatar className="little-spacer-right" hash={comment.authorAvatar} size={16} />
{comment.authorName}
</div>
<div
className="issue-comment-text markdown"
dangerouslySetInnerHTML={{ __html: comment.htmlText }}
/>
<div className="issue-comment-age">({moment(comment.createdAt).fromNow()})</div>
<div className="issue-comment-actions">
{comment.updatable &&
<BubblePopupHelper
className="bubble-popup-helper-inline"
isOpen={this.state.openPopup === 'edit'}
offset={{ vertical: 0, horizontal: -6 }}
position="bottomright"
togglePopup={this.toggleDeletePopup}
popup={
<CommentPopup
comment={comment}
customClass="issue-edit-comment-bubble-popup"
onComment={this.handleEdit}
placeholder=""
toggleComment={this.toggleEditPopup}
/>
}>
<button
className="js-issue-comment-edit button-link icon-edit icon-half-transparent"
onClick={this.toggleEditPopup}
/>
</BubblePopupHelper>}
{comment.updatable &&
<BubblePopupHelper
className="bubble-popup-helper-inline"
isOpen={this.state.openPopup === 'delete'}
offset={{ vertical: 0, horizontal: -10 }}
position="bottomright"
togglePopup={this.toggleDeletePopup}
popup={<CommentDeletePopup onDelete={this.handleDelete} />}>
<button
className="js-issue-comment-delete button-link icon-delete icon-half-transparent"
onClick={this.toggleDeletePopup}
/>
</BubblePopupHelper>}
</div>
</div>
);
}
}

+ 53
- 0
server/sonar-web/src/main/js/components/issue/components/IssueMessage.js View File

@@ -0,0 +1,53 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { translate } from '../../../helpers/l10n';

export default class IssueMessage extends React.PureComponent {
props: {
message: string,
rule: string,
organization: string
};

handleClick = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const Workspace = require('../../workspace/main').default;
Workspace.openRule({
key: this.props.rule,
organization: this.props.organization
});
};

render() {
return (
<div className="issue-message">
{this.props.message}
<button
className="button-link issue-rule icon-ellipsis-h little-spacer-left"
aria-label={translate('issue.rule_details')}
onClick={this.handleClick}
/>
</div>
);
}
}

+ 70
- 0
server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js View File

@@ -0,0 +1,70 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import SetSeverityPopup from '../popups/SetSeverityPopup';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import { setIssueSeverity } from '../../../api/issues';
import type { Issue } from '../types';

type Props = {
canSetSeverity: boolean,
isOpen: boolean,
issue: Issue,
setIssueProperty: (string, string, apiCall: (Object) => Promise<*>, string) => void,
togglePopup: (string) => void
};

export default class IssueSeverity extends React.PureComponent {
props: Props;

toggleSetSeverity = (open?: boolean) => {
this.props.togglePopup('set-severity', open);
};

setSeverity = (severity: string) =>
this.props.setIssueProperty('severity', 'set-severity', setIssueSeverity, severity);

render() {
const { issue } = this.props;
if (this.props.canSetSeverity) {
return (
<BubblePopupHelper
isOpen={this.props.isOpen && this.props.canSetSeverity}
position="bottomleft"
togglePopup={this.toggleSetSeverity}
popup={<SetSeverityPopup issue={issue} onSelect={this.setSeverity} />}>
<button
className="button-link issue-action issue-action-with-options js-issue-set-severity"
onClick={this.toggleSetSeverity}>
<SeverityHelper
className="issue-meta-label little-spacer-right"
severity={issue.severity}
/>
<i className="little-spacer-left icon-dropdown" />
</button>
</BubblePopupHelper>
);
} else {
return <SeverityHelper className="issue-meta-label" severity={issue.severity} />;
}
}
}

+ 90
- 0
server/sonar-web/src/main/js/components/issue/components/IssueTags.js View File

@@ -0,0 +1,90 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import SetIssueTagsPopup from '../popups/SetIssueTagsPopup';
import TagsList from '../../../components/tags/TagsList';
import { setIssueTags } from '../../../api/issues';
import { translate } from '../../../helpers/l10n';
import type { Issue } from '../types';

type Props = {
canSetTags: boolean,
isOpen: boolean,
issue: Issue,
onFail: (Error) => void,
onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void,
togglePopup: (string) => void
};

export default class IssueTags extends React.PureComponent {
props: Props;

toggleSetTags = (open?: boolean) => {
this.props.togglePopup('edit-tags', open);
};

setTags = (tags: Array<string>) => {
const { issue } = this.props;
const newIssue = { ...issue, tags };
this.props.onIssueChange(
setIssueTags({ issue: issue.key, tags: tags.join(',') }),
issue,
newIssue
);
};

render() {
const { issue } = this.props;

if (this.props.canSetTags) {
return (
<BubblePopupHelper
isOpen={this.props.isOpen}
position="bottomright"
togglePopup={this.toggleSetTags}
popup={
<SetIssueTagsPopup
onFail={this.props.onFail}
selectedTags={issue.tags}
setTags={this.setTags}
/>
}>
<button
className={'js-issue-edit-tags button-link issue-action issue-action-with-options'}
onClick={this.toggleSetTags}>
<TagsList
tags={issue.tags && issue.tags.length > 0 ? issue.tags : [translate('issue.no_tag')]}
allowUpdate={this.props.canSetTags}
/>
</button>
</BubblePopupHelper>
);
} else {
return (
<TagsList
tags={issue.tags && issue.tags.length > 0 ? issue.tags : [translate('issue.no_tag')]}
allowUpdate={this.props.canSetTags}
/>
);
}
}
}

+ 91
- 0
server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js View File

@@ -0,0 +1,91 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import IssueChangelog from './IssueChangelog';
import IssueMessage from './IssueMessage';
import { getSingleIssueUrl } from '../../../helpers/urls';
import { translate } from '../../../helpers/l10n';
import type { Issue } from '../types';

type Props = {
issue: Issue,
currentPopup: string,
onFail: (Error) => void,
onFilterClick?: () => void,
togglePopup: (string) => void
};

export default function IssueTitleBar(props: Props) {
const { issue } = props;
const hasSimilarIssuesFilter = props.onFilterClick != null;

return (
<table className="issue-table">
<tbody>
<tr>
<td>
<IssueMessage
message={issue.message}
rule={issue.rule}
organization={issue.organization}
/>
</td>
<td className="issue-table-meta-cell issue-table-meta-cell-first">
<ul className="list-inline issue-meta-list">
<li className="issue-meta">
<IssueChangelog
creationDate={issue.creationDate}
isOpen={props.currentPopup === 'changelog'}
issue={issue}
togglePopup={props.togglePopup}
onFail={props.onFail}
/>
</li>
{issue.line != null &&
<li className="issue-meta">
<span className="issue-meta-label" title={translate('line_number')}>
L{issue.line}
</span>
</li>}
<li className="issue-meta">
<a
className="js-issue-permalink icon-link"
href={getSingleIssueUrl(issue.key)}
target="_blank"
/>
</li>
{hasSimilarIssuesFilter &&
<li className="issue-meta">
<button
className="js-issue-filter button-link issue-action issue-action-with-options"
aria-label={translate('issue.filter_similar_issues')}
onClick={props.onFilterClick}>
<i className="icon-filter icon-half-transparent" />{' '}
<i className="icon-dropdown" />
</button>
</li>}
</ul>
</td>
</tr>
</tbody>
</table>
);
}

+ 80
- 0
server/sonar-web/src/main/js/components/issue/components/IssueTransition.js View File

@@ -0,0 +1,80 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import SetTransitionPopup from '../popups/SetTransitionPopup';
import StatusHelper from '../../../components/shared/StatusHelper';
import { setIssueTransition } from '../../../api/issues';
import type { Issue } from '../types';

type Props = {
hasTransitions: boolean,
isOpen: boolean,
issue: Issue,
setIssueProperty: (string, string, apiCall: (Object) => Promise<*>, string) => void,
togglePopup: (string) => void
};

export default class IssueTransition extends React.PureComponent {
props: Props;

setTransition = (transition: string) =>
this.props.setIssueProperty('transition', 'transition', setIssueTransition, transition);

toggleSetTransition = (open?: boolean) => {
this.props.togglePopup('transition', open);
};

render() {
const { issue } = this.props;

if (this.props.hasTransitions) {
return (
<BubblePopupHelper
isOpen={this.props.isOpen && this.props.hasTransitions}
position="bottomleft"
togglePopup={this.toggleSetTransition}
popup={
<SetTransitionPopup transitions={issue.transitions} onSelect={this.setTransition} />
}>
<button
className="button-link issue-action issue-action-with-options js-issue-transition"
onClick={this.toggleSetTransition}>
<StatusHelper
className="issue-meta-label little-spacer-right"
status={issue.status}
resolution={issue.resolution}
/>
<i className="little-spacer-left icon-dropdown" />
</button>
</BubblePopupHelper>
);
} else {
return (
<StatusHelper
className="issue-meta-label"
status={issue.status}
resolution={issue.resolution}
/>
);
}
}
}

+ 73
- 0
server/sonar-web/src/main/js/components/issue/components/IssueType.js View File

@@ -0,0 +1,73 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import SetTypePopup from '../popups/SetTypePopup';
import { setIssueType } from '../../../api/issues';
import { translate } from '../../../helpers/l10n';
import type { Issue } from '../types';

type Props = {
canSetSeverity: boolean,
isOpen: boolean,
issue: Issue,
setIssueProperty: (string, string, apiCall: (Object) => Promise<*>, string) => void,
togglePopup: (string) => void
};

export default class IssueType extends React.PureComponent {
props: Props;

toggleSetType = (open?: boolean) => {
this.props.togglePopup('set-type', open);
};

setType = (type: string) => this.props.setIssueProperty('type', 'set-type', setIssueType, type);

render() {
const { issue } = this.props;
if (this.props.canSetSeverity) {
return (
<BubblePopupHelper
isOpen={this.props.isOpen && this.props.canSetSeverity}
position="bottomleft"
togglePopup={this.toggleSetType}
popup={<SetTypePopup issue={issue} onSelect={this.setType} />}>
<button
className="button-link issue-action issue-action-with-options js-issue-set-type"
onClick={this.toggleSetType}>
<IssueTypeIcon className="little-spacer-right" query={issue.type} />
{translate('issue.type', issue.type)}
<i className="little-spacer-left icon-dropdown" />
</button>
</BubblePopupHelper>
);
} else {
return (
<span>
<IssueTypeIcon className="little-spacer-right" query={issue.type} />
{translate('issue.type', issue.type)}
</span>
);
}
}
}

+ 75
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js View File

@@ -0,0 +1,75 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import IssueAssign from '../IssueAssign';
import { click } from '../../../../helpers/testUtils';

const issue = {
assignee: 'john',
assigneeAvatar: 'gravatarhash',
assigneeName: 'John Doe'
};

it('should render without the action when the correct rights are missing', () => {
const element = shallow(
<IssueAssign
canAssign={false}
isOpen={false}
issue={issue}
onFail={jest.fn()}
onAssign={jest.fn()}
togglePopup={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
});

it('should render with the action', () => {
const element = shallow(
<IssueAssign
canAssign={true}
isOpen={false}
issue={issue}
onFail={jest.fn()}
onAssign={jest.fn()}
togglePopup={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
});

it('should open the popup when the button is clicked', () => {
const toggle = jest.fn();
const element = shallow(
<IssueAssign
canAssign={true}
isOpen={false}
issue={issue}
onFail={jest.fn()}
onAssign={jest.fn()}
togglePopup={toggle}
/>
);
click(element.find('button'));
expect(toggle.mock.calls).toMatchSnapshot();
element.setProps({ isOpen: true });
expect(element).toMatchSnapshot();
});

+ 62
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js View File

@@ -0,0 +1,62 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import moment from 'moment';
import IssueChangelog from '../IssueChangelog';
import { click } from '../../../../helpers/testUtils';

const issue = {
key: 'issuekey',
author: 'john.david.dalton@gmail.com',
creationDate: '2017-03-01T09:36:01+0100'
};

moment.fn.fromNow = jest.fn(() => 'a month ago');

it('should render correctly', () => {
const element = shallow(
<IssueChangelog
creationDate="2017-03-01T09:36:01+0100"
isOpen={false}
issue={issue}
onFail={jest.fn()}
togglePopup={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
});

it('should open the popup when the button is clicked', () => {
const toggle = jest.fn();
const element = shallow(
<IssueChangelog
creationDate="2017-03-01T09:36:01+0100"
isOpen={false}
issue={issue}
onFail={jest.fn()}
togglePopup={toggle}
/>
);
click(element.find('button'));
expect(toggle.mock.calls).toMatchSnapshot();
element.setProps({ isOpen: true });
expect(element).toMatchSnapshot();
});

+ 53
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js View File

@@ -0,0 +1,53 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import IssueCommentAction from '../IssueCommentAction';
import { click } from '../../../../helpers/testUtils';

it('should render correctly', () => {
const element = shallow(
<IssueCommentAction
issueKey="issue-key"
currentPopup=""
onFail={jest.fn()}
onIssueChange={jest.fn()}
toggleComment={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
});

it('should open the popup when the button is clicked', () => {
const toggle = jest.fn();
const element = shallow(
<IssueCommentAction
issueKey="issue-key"
currentPopup=""
onFail={jest.fn()}
onIssueChange={jest.fn()}
toggleComment={toggle}
/>
);
click(element.find('button'));
expect(toggle.mock.calls.length).toBe(1);
element.setProps({ currentPopup: 'comment' });
expect(element).toMatchSnapshot();
});

+ 64
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js View File

@@ -0,0 +1,64 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import moment from 'moment';
import IssueCommentLine from '../IssueCommentLine';
import { click } from '../../../../helpers/testUtils';

const comment = {
key: 'comment-key',
authorName: 'John Doe',
authorAvatar: 'gravatarhash',
htmlText: '<b>test</b>',
createdAt: '2017-03-01T09:36:01+0100',
updatable: true
};

moment.fn.fromNow = jest.fn(() => 'a month ago');

it('should render correctly a comment that is not updatable', () => {
const element = shallow(
<IssueCommentLine
comment={{ ...comment, updatable: false }}
onDelete={jest.fn()}
onEdit={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
});

it('should render correctly a comment that is updatable', () => {
const element = shallow(
<IssueCommentLine comment={comment} onDelete={jest.fn()} onEdit={jest.fn()} />
);
expect(element).toMatchSnapshot();
});

it('should open the right popups when the buttons are clicked', () => {
const element = shallow(
<IssueCommentLine comment={comment} onDelete={jest.fn()} onEdit={jest.fn()} />
);
click(element.find('button.js-issue-comment-edit'));
expect(element.state()).toMatchSnapshot();
click(element.find('button.js-issue-comment-delete'));
expect(element.state()).toMatchSnapshot();
expect(element).toMatchSnapshot();
});

+ 33
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.js View File

@@ -0,0 +1,33 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import IssueMessage from '../IssueMessage';

it('should render with the message and a link to open the rule', () => {
const element = shallow(
<IssueMessage
rule="javascript:S1067"
message="Reduce the number of conditional operators (4) used in the expression"
organization="myorg"
/>
);
expect(element).toMatchSnapshot();
});

+ 70
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js View File

@@ -0,0 +1,70 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import IssueSeverity from '../IssueSeverity';
import { click } from '../../../../helpers/testUtils';

const issue = {
severity: 'BLOCKER'
};

it('should render without the action when the correct rights are missing', () => {
const element = shallow(
<IssueSeverity
canSetSeverity={false}
isOpen={false}
issue={issue}
setIssueProperty={jest.fn()}
togglePopup={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
});

it('should render with the action', () => {
const element = shallow(
<IssueSeverity
canSetSeverity={true}
isOpen={false}
issue={issue}
setIssueProperty={jest.fn()}
togglePopup={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
});

it('should open the popup when the button is clicked', () => {
const toggle = jest.fn();
const element = shallow(
<IssueSeverity
canSetSeverity={true}
isOpen={false}
issue={issue}
setIssueProperty={jest.fn()}
togglePopup={toggle}
/>
);
click(element.find('button'));
expect(toggle.mock.calls).toMatchSnapshot();
element.setProps({ isOpen: true });
expect(element).toMatchSnapshot();
});

+ 77
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js View File

@@ -0,0 +1,77 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import IssueTags from '../IssueTags';
import { click } from '../../../../helpers/testUtils';

const issue = {
key: 'issuekey',
tags: ['mytag', 'test']
};

it('should render without the action when the correct rights are missing', () => {
const element = shallow(
<IssueTags
canSetTags={false}
isOpen={false}
issue={{
transitions: [],
status: 'CLOSED'
}}
onFail={jest.fn()}
onIssueChange={jest.fn()}
togglePopup={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
});

it('should render with the action', () => {
const element = shallow(
<IssueTags
canSetTags={true}
isOpen={false}
issue={issue}
onFail={jest.fn()}
onIssueChange={jest.fn()}
togglePopup={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
});

it('should open the popup when the button is clicked', () => {
const toggle = jest.fn();
const element = shallow(
<IssueTags
canSetTags={true}
isOpen={false}
issue={issue}
onFail={jest.fn()}
onIssueChange={jest.fn()}
togglePopup={toggle}
/>
);
click(element.find('button'));
expect(toggle.mock.calls).toMatchSnapshot();
element.setProps({ isOpen: true });
expect(element).toMatchSnapshot();
});

+ 51
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js View File

@@ -0,0 +1,51 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import IssueTitleBar from '../IssueTitleBar';

const issue = {
line: 26,
creationDate: '2017-03-01T09:36:01+0100',
organization: 'myorg',
key: 'AVsae-CQS-9G3txfbFN2',
rule: 'javascript:S1067',
message: 'Reduce the number of conditional operators (4) used in the expression'
};

it('should render the titlebar correctly', () => {
const element = shallow(
<IssueTitleBar issue={issue} currentPopup="" onFail={jest.fn()} togglePopup={jest.fn()} />
);
expect(element).toMatchSnapshot();
});

it('should render the titlebar with the filter', () => {
const element = shallow(
<IssueTitleBar
issue={issue}
currentPopup=""
onFail={jest.fn()}
onFilterClick={jest.fn()}
togglePopup={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
});

+ 91
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js View File

@@ -0,0 +1,91 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import IssueTransition from '../IssueTransition';
import { click } from '../../../../helpers/testUtils';

const issue = {
transitions: ['confirm', 'resolve', 'falsepositive', 'wontfix'],
status: 'OPEN'
};

it('should render without the action when there is no transitions', () => {
const element = shallow(
<IssueTransition
hasTransitions={false}
isOpen={false}
issue={{
transitions: [],
status: 'CLOSED'
}}
setIssueProperty={jest.fn()}
togglePopup={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
});

it('should render with the action', () => {
const element = shallow(
<IssueTransition
hasTransitions={true}
isOpen={false}
issue={issue}
setIssueProperty={jest.fn()}
togglePopup={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
});

it('should render with a resolution', () => {
const element = shallow(
<IssueTransition
hasTransitions={true}
isOpen={false}
issue={{
transitions: ['reopen'],
status: 'RESOLVED',
resolution: 'FIXED'
}}
setIssueProperty={jest.fn()}
togglePopup={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
});

it('should open the popup when the button is clicked', () => {
const toggle = jest.fn();
const element = shallow(
<IssueTransition
hasTransitions={true}
isOpen={false}
issue={issue}
setIssueProperty={jest.fn()}
togglePopup={toggle}
/>
);
click(element.find('button'));
expect(toggle.mock.calls).toMatchSnapshot();
element.setProps({ isOpen: true });
expect(element).toMatchSnapshot();
});

+ 70
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js View File

@@ -0,0 +1,70 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import IssueType from '../IssueType';
import { click } from '../../../../helpers/testUtils';

const issue = {
type: 'bug'
};

it('should render without the action when the correct rights are missing', () => {
const element = shallow(
<IssueType
canSetSeverity={false}
isOpen={false}
issue={issue}
setIssueProperty={jest.fn()}
togglePopup={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
});

it('should render with the action', () => {
const element = shallow(
<IssueType
canSetSeverity={true}
isOpen={false}
issue={issue}
setIssueProperty={jest.fn()}
togglePopup={jest.fn()}
/>
);
expect(element).toMatchSnapshot();
});

it('should open the popup when the button is clicked', () => {
const toggle = jest.fn();
const element = shallow(
<IssueType
canSetSeverity={true}
isOpen={false}
issue={issue}
setIssueProperty={jest.fn()}
togglePopup={toggle}
/>
);
click(element.find('button'));
expect(toggle.mock.calls).toMatchSnapshot();
element.setProps({ isOpen: true });
expect(element).toMatchSnapshot();
});

+ 111
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap View File

@@ -0,0 +1,111 @@
exports[`test should open the popup when the button is clicked 1`] = `
Array [
Array [
"assign",
Object {
"currentTarget": Object {
"blur": [Function],
},
"preventDefault": [Function],
"stopPropagation": [Function],
"target": Object {
"blur": [Function],
},
},
],
]
`;

exports[`test should open the popup when the button is clicked 2`] = `
<BubblePopupHelper
isOpen={true}
popup={
<SetAssigneePopup
issue={
Object {
"assignee": "john",
"assigneeAvatar": "gravatarhash",
"assigneeName": "John Doe",
}
}
onFail={[Function]}
onSelect={[Function]} />
}
position="bottomleft"
togglePopup={[Function]}>
<button
className="button-link issue-action issue-action-with-options js-issue-assign"
onClick={[Function]}>
<span>
<span
className="text-top">
<Connect(Avatar)
className="little-spacer-right"
hash="gravatarhash"
size={16} />
</span>
<span
className="issue-meta-label">
John Doe
</span>
</span>
<i
className="little-spacer-left icon-dropdown" />
</button>
</BubblePopupHelper>
`;

exports[`test should render with the action 1`] = `
<BubblePopupHelper
isOpen={false}
popup={
<SetAssigneePopup
issue={
Object {
"assignee": "john",
"assigneeAvatar": "gravatarhash",
"assigneeName": "John Doe",
}
}
onFail={[Function]}
onSelect={[Function]} />
}
position="bottomleft"
togglePopup={[Function]}>
<button
className="button-link issue-action issue-action-with-options js-issue-assign"
onClick={[Function]}>
<span>
<span
className="text-top">
<Connect(Avatar)
className="little-spacer-right"
hash="gravatarhash"
size={16} />
</span>
<span
className="issue-meta-label">
John Doe
</span>
</span>
<i
className="little-spacer-left icon-dropdown" />
</button>
</BubblePopupHelper>
`;

exports[`test should render without the action when the correct rights are missing 1`] = `
<span>
<span
className="text-top">
<Connect(Avatar)
className="little-spacer-right"
hash="gravatarhash"
size={16} />
</span>
<span
className="issue-meta-label">
John Doe
</span>
</span>
`;

+ 68
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap View File

@@ -0,0 +1,68 @@
exports[`test should open the popup when the button is clicked 1`] = `
Array [
Array [
"changelog",
undefined,
],
]
`;

exports[`test should open the popup when the button is clicked 2`] = `
<BubblePopupHelper
isOpen={true}
popup={
<ChangelogPopup
issue={
Object {
"author": "john.david.dalton@gmail.com",
"creationDate": "2017-03-01T09:36:01+0100",
"key": "issuekey",
}
}
onFail={[Function]} />
}
position="bottomright"
togglePopup={[Function]}>
<button
className="button-link issue-action issue-action-with-options js-issue-show-changelog"
onClick={[Function]}
title="March 1, 2017 9:36 AM">
<span
className="issue-meta-label">
a month ago
</span>
<i
className="icon-dropdown little-spacer-left" />
</button>
</BubblePopupHelper>
`;

exports[`test should render correctly 1`] = `
<BubblePopupHelper
isOpen={false}
popup={
<ChangelogPopup
issue={
Object {
"author": "john.david.dalton@gmail.com",
"creationDate": "2017-03-01T09:36:01+0100",
"key": "issuekey",
}
}
onFail={[Function]} />
}
position="bottomright"
togglePopup={[Function]}>
<button
className="button-link issue-action issue-action-with-options js-issue-show-changelog"
onClick={[Function]}
title="March 1, 2017 9:36 AM">
<span
className="issue-meta-label">
a month ago
</span>
<i
className="icon-dropdown little-spacer-left" />
</button>
</BubblePopupHelper>
`;

+ 49
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap View File

@@ -0,0 +1,49 @@
exports[`test should open the popup when the button is clicked 1`] = `
<li
className="issue-meta">
<BubblePopupHelper
isOpen={true}
popup={
<CommentPopup
customClass="issue-comment-bubble-popup"
onComment={[Function]}
toggleComment={[Function]} />
}
position="bottomleft"
togglePopup={[Function]}>
<button
className="button-link issue-action js-issue-comment"
onClick={[Function]}>
<span
className="issue-meta-label">
issue.comment.formlink
</span>
</button>
</BubblePopupHelper>
</li>
`;

exports[`test should render correctly 1`] = `
<li
className="issue-meta">
<BubblePopupHelper
isOpen={false}
popup={
<CommentPopup
customClass="issue-comment-bubble-popup"
onComment={[Function]}
toggleComment={[Function]} />
}
position="bottomleft"
togglePopup={[Function]}>
<button
className="button-link issue-action js-issue-comment"
onClick={[Function]}>
<span
className="issue-meta-label">
issue.comment.formlink
</span>
</button>
</BubblePopupHelper>
</li>
`;

+ 205
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap View File

@@ -0,0 +1,205 @@
exports[`test should open the right popups when the buttons are clicked 1`] = `
Object {
"openPopup": "edit",
}
`;

exports[`test should open the right popups when the buttons are clicked 2`] = `
Object {
"openPopup": "delete",
}
`;

exports[`test should open the right popups when the buttons are clicked 3`] = `
<div
className="issue-comment">
<div
className="issue-comment-author"
title="John Doe">
<Connect(Avatar)
className="little-spacer-right"
hash="gravatarhash"
size={16} />
John Doe
</div>
<div
className="issue-comment-text markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<b>test</b>",
}
} />
<div
className="issue-comment-age">
(
a month ago
)
</div>
<div
className="issue-comment-actions">
<BubblePopupHelper
className="bubble-popup-helper-inline"
isOpen={false}
offset={
Object {
"horizontal": -6,
"vertical": 0,
}
}
popup={
<CommentPopup
comment={
Object {
"authorAvatar": "gravatarhash",
"authorName": "John Doe",
"createdAt": "2017-03-01T09:36:01+0100",
"htmlText": "<b>test</b>",
"key": "comment-key",
"updatable": true,
}
}
customClass="issue-edit-comment-bubble-popup"
onComment={[Function]}
placeholder=""
toggleComment={[Function]} />
}
position="bottomright"
togglePopup={[Function]}>
<button
className="js-issue-comment-edit button-link icon-edit icon-half-transparent"
onClick={[Function]} />
</BubblePopupHelper>
<BubblePopupHelper
className="bubble-popup-helper-inline"
isOpen={true}
offset={
Object {
"horizontal": -10,
"vertical": 0,
}
}
popup={
<CommentDeletePopup
onDelete={[Function]} />
}
position="bottomright"
togglePopup={[Function]}>
<button
className="js-issue-comment-delete button-link icon-delete icon-half-transparent"
onClick={[Function]} />
</BubblePopupHelper>
</div>
</div>
`;

exports[`test should render correctly a comment that is not updatable 1`] = `
<div
className="issue-comment">
<div
className="issue-comment-author"
title="John Doe">
<Connect(Avatar)
className="little-spacer-right"
hash="gravatarhash"
size={16} />
John Doe
</div>
<div
className="issue-comment-text markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<b>test</b>",
}
} />
<div
className="issue-comment-age">
(
a month ago
)
</div>
<div
className="issue-comment-actions" />
</div>
`;

exports[`test should render correctly a comment that is updatable 1`] = `
<div
className="issue-comment">
<div
className="issue-comment-author"
title="John Doe">
<Connect(Avatar)
className="little-spacer-right"
hash="gravatarhash"
size={16} />
John Doe
</div>
<div
className="issue-comment-text markdown"
dangerouslySetInnerHTML={
Object {
"__html": "<b>test</b>",
}
} />
<div
className="issue-comment-age">
(
a month ago
)
</div>
<div
className="issue-comment-actions">
<BubblePopupHelper
className="bubble-popup-helper-inline"
isOpen={false}
offset={
Object {
"horizontal": -6,
"vertical": 0,
}
}
popup={
<CommentPopup
comment={
Object {
"authorAvatar": "gravatarhash",
"authorName": "John Doe",
"createdAt": "2017-03-01T09:36:01+0100",
"htmlText": "<b>test</b>",
"key": "comment-key",
"updatable": true,
}
}
customClass="issue-edit-comment-bubble-popup"
onComment={[Function]}
placeholder=""
toggleComment={[Function]} />
}
position="bottomright"
togglePopup={[Function]}>
<button
className="js-issue-comment-edit button-link icon-edit icon-half-transparent"
onClick={[Function]} />
</BubblePopupHelper>
<BubblePopupHelper
className="bubble-popup-helper-inline"
isOpen={false}
offset={
Object {
"horizontal": -10,
"vertical": 0,
}
}
popup={
<CommentDeletePopup
onDelete={[Function]} />
}
position="bottomright"
togglePopup={[Function]}>
<button
className="js-issue-comment-delete button-link icon-delete icon-half-transparent"
onClick={[Function]} />
</BubblePopupHelper>
</div>
</div>
`;

+ 10
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.js.snap View File

@@ -0,0 +1,10 @@
exports[`test should render with the message and a link to open the rule 1`] = `
<div
className="issue-message">
Reduce the number of conditional operators (4) used in the expression
<button
aria-label="issue.rule_details"
className="button-link issue-rule icon-ellipsis-h little-spacer-left"
onClick={[Function]} />
</div>
`;

+ 75
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap View File

@@ -0,0 +1,75 @@
exports[`test should open the popup when the button is clicked 1`] = `
Array [
Array [
"set-severity",
Object {
"currentTarget": Object {
"blur": [Function],
},
"preventDefault": [Function],
"stopPropagation": [Function],
"target": Object {
"blur": [Function],
},
},
],
]
`;

exports[`test should open the popup when the button is clicked 2`] = `
<BubblePopupHelper
isOpen={true}
popup={
<SetSeverityPopup
issue={
Object {
"severity": "BLOCKER",
}
}
onSelect={[Function]} />
}
position="bottomleft"
togglePopup={[Function]}>
<button
className="button-link issue-action issue-action-with-options js-issue-set-severity"
onClick={[Function]}>
<SeverityHelper
className="issue-meta-label little-spacer-right"
severity="BLOCKER" />
<i
className="little-spacer-left icon-dropdown" />
</button>
</BubblePopupHelper>
`;

exports[`test should render with the action 1`] = `
<BubblePopupHelper
isOpen={false}
popup={
<SetSeverityPopup
issue={
Object {
"severity": "BLOCKER",
}
}
onSelect={[Function]} />
}
position="bottomleft"
togglePopup={[Function]}>
<button
className="button-link issue-action issue-action-with-options js-issue-set-severity"
onClick={[Function]}>
<SeverityHelper
className="issue-meta-label little-spacer-right"
severity="BLOCKER" />
<i
className="little-spacer-left icon-dropdown" />
</button>
</BubblePopupHelper>
`;

exports[`test should render without the action when the correct rights are missing 1`] = `
<SeverityHelper
className="issue-meta-label"
severity="BLOCKER" />
`;

+ 89
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap View File

@@ -0,0 +1,89 @@
exports[`test should open the popup when the button is clicked 1`] = `
Array [
Array [
"edit-tags",
Object {
"currentTarget": Object {
"blur": [Function],
},
"preventDefault": [Function],
"stopPropagation": [Function],
"target": Object {
"blur": [Function],
},
},
],
]
`;

exports[`test should open the popup when the button is clicked 2`] = `
<BubblePopupHelper
isOpen={true}
popup={
<SetIssueTagsPopup
onFail={[Function]}
selectedTags={
Array [
"mytag",
"test",
]
}
setTags={[Function]} />
}
position="bottomright"
togglePopup={[Function]}>
<button
className="js-issue-edit-tags button-link issue-action issue-action-with-options"
onClick={[Function]}>
<TagsList
allowUpdate={true}
tags={
Array [
"mytag",
"test",
]
} />
</button>
</BubblePopupHelper>
`;

exports[`test should render with the action 1`] = `
<BubblePopupHelper
isOpen={false}
popup={
<SetIssueTagsPopup
onFail={[Function]}
selectedTags={
Array [
"mytag",
"test",
]
}
setTags={[Function]} />
}
position="bottomright"
togglePopup={[Function]}>
<button
className="js-issue-edit-tags button-link issue-action issue-action-with-options"
onClick={[Function]}>
<TagsList
allowUpdate={true}
tags={
Array [
"mytag",
"test",
]
} />
</button>
</BubblePopupHelper>
`;

exports[`test should render without the action when the correct rights are missing 1`] = `
<TagsList
allowUpdate={false}
tags={
Array [
"issue.no_tag",
]
} />
`;

+ 124
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap View File

@@ -0,0 +1,124 @@
exports[`test should render the titlebar correctly 1`] = `
<table
className="issue-table">
<tbody>
<tr>
<td>
<IssueMessage
message="Reduce the number of conditional operators (4) used in the expression"
organization="myorg"
rule="javascript:S1067" />
</td>
<td
className="issue-table-meta-cell issue-table-meta-cell-first">
<ul
className="list-inline issue-meta-list">
<li
className="issue-meta">
<IssueChangelog
creationDate="2017-03-01T09:36:01+0100"
isOpen={false}
issue={
Object {
"creationDate": "2017-03-01T09:36:01+0100",
"key": "AVsae-CQS-9G3txfbFN2",
"line": 26,
"message": "Reduce the number of conditional operators (4) used in the expression",
"organization": "myorg",
"rule": "javascript:S1067",
}
}
onFail={[Function]}
togglePopup={[Function]} />
</li>
<li
className="issue-meta">
<span
className="issue-meta-label"
title="line_number">
L
26
</span>
</li>
<li
className="issue-meta">
<a
className="js-issue-permalink icon-link"
href="/issues/search#issues=AVsae-CQS-9G3txfbFN2"
target="_blank" />
</li>
</ul>
</td>
</tr>
</tbody>
</table>
`;

exports[`test should render the titlebar with the filter 1`] = `
<table
className="issue-table">
<tbody>
<tr>
<td>
<IssueMessage
message="Reduce the number of conditional operators (4) used in the expression"
organization="myorg"
rule="javascript:S1067" />
</td>
<td
className="issue-table-meta-cell issue-table-meta-cell-first">
<ul
className="list-inline issue-meta-list">
<li
className="issue-meta">
<IssueChangelog
creationDate="2017-03-01T09:36:01+0100"
isOpen={false}
issue={
Object {
"creationDate": "2017-03-01T09:36:01+0100",
"key": "AVsae-CQS-9G3txfbFN2",
"line": 26,
"message": "Reduce the number of conditional operators (4) used in the expression",
"organization": "myorg",
"rule": "javascript:S1067",
}
}
onFail={[Function]}
togglePopup={[Function]} />
</li>
<li
className="issue-meta">
<span
className="issue-meta-label"
title="line_number">
L
26
</span>
</li>
<li
className="issue-meta">
<a
className="js-issue-permalink icon-link"
href="/issues/search#issues=AVsae-CQS-9G3txfbFN2"
target="_blank" />
</li>
<li
className="issue-meta">
<button
aria-label="issue.filter_similar_issues"
className="js-issue-filter button-link issue-action issue-action-with-options"
onClick={[Function]}>
<i
className="icon-filter icon-half-transparent" />
<i
className="icon-dropdown" />
</button>
</li>
</ul>
</td>
</tr>
</tbody>
</table>
`;

+ 108
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap View File

@@ -0,0 +1,108 @@
exports[`test should open the popup when the button is clicked 1`] = `
Array [
Array [
"transition",
Object {
"currentTarget": Object {
"blur": [Function],
},
"preventDefault": [Function],
"stopPropagation": [Function],
"target": Object {
"blur": [Function],
},
},
],
]
`;

exports[`test should open the popup when the button is clicked 2`] = `
<BubblePopupHelper
isOpen={true}
popup={
<SetTransitionPopup
onSelect={[Function]}
transitions={
Array [
"confirm",
"resolve",
"falsepositive",
"wontfix",
]
} />
}
position="bottomleft"
togglePopup={[Function]}>
<button
className="button-link issue-action issue-action-with-options js-issue-transition"
onClick={[Function]}>
<StatusHelper
className="issue-meta-label little-spacer-right"
status="OPEN" />
<i
className="little-spacer-left icon-dropdown" />
</button>
</BubblePopupHelper>
`;

exports[`test should render with a resolution 1`] = `
<BubblePopupHelper
isOpen={false}
popup={
<SetTransitionPopup
onSelect={[Function]}
transitions={
Array [
"reopen",
]
} />
}
position="bottomleft"
togglePopup={[Function]}>
<button
className="button-link issue-action issue-action-with-options js-issue-transition"
onClick={[Function]}>
<StatusHelper
className="issue-meta-label little-spacer-right"
resolution="FIXED"
status="RESOLVED" />
<i
className="little-spacer-left icon-dropdown" />
</button>
</BubblePopupHelper>
`;

exports[`test should render with the action 1`] = `
<BubblePopupHelper
isOpen={false}
popup={
<SetTransitionPopup
onSelect={[Function]}
transitions={
Array [
"confirm",
"resolve",
"falsepositive",
"wontfix",
]
} />
}
position="bottomleft"
togglePopup={[Function]}>
<button
className="button-link issue-action issue-action-with-options js-issue-transition"
onClick={[Function]}>
<StatusHelper
className="issue-meta-label little-spacer-right"
status="OPEN" />
<i
className="little-spacer-left icon-dropdown" />
</button>
</BubblePopupHelper>
`;

exports[`test should render without the action when there is no transitions 1`] = `
<StatusHelper
className="issue-meta-label"
status="CLOSED" />
`;

+ 80
- 0
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap View File

@@ -0,0 +1,80 @@
exports[`test should open the popup when the button is clicked 1`] = `
Array [
Array [
"set-type",
Object {
"currentTarget": Object {
"blur": [Function],
},
"preventDefault": [Function],
"stopPropagation": [Function],
"target": Object {
"blur": [Function],
},
},
],
]
`;

exports[`test should open the popup when the button is clicked 2`] = `
<BubblePopupHelper
isOpen={true}
popup={
<SetTypePopup
issue={
Object {
"type": "bug",
}
}
onSelect={[Function]} />
}
position="bottomleft"
togglePopup={[Function]}>
<button
className="button-link issue-action issue-action-with-options js-issue-set-type"
onClick={[Function]}>
<IssueTypeIcon
className="little-spacer-right"
query="bug" />
issue.type.bug
<i
className="little-spacer-left icon-dropdown" />
</button>
</BubblePopupHelper>
`;

exports[`test should render with the action 1`] = `
<BubblePopupHelper
isOpen={false}
popup={
<SetTypePopup
issue={
Object {
"type": "bug",
}
}
onSelect={[Function]} />
}
position="bottomleft"
togglePopup={[Function]}>
<button
className="button-link issue-action issue-action-with-options js-issue-set-type"
onClick={[Function]}>
<IssueTypeIcon
className="little-spacer-right"
query="bug" />
issue.type.bug
<i
className="little-spacer-left icon-dropdown" />
</button>
</BubblePopupHelper>
`;

exports[`test should render without the action when the correct rights are missing 1`] = `
<span>
<IssueTypeIcon
className="little-spacer-right"
query="bug" />
issue.type.bug
</span>
`;

+ 116
- 0
server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js View File

@@ -0,0 +1,116 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import moment from 'moment';
import { getIssueChangelog } from '../../../api/issues';
import { translate } from '../../../helpers/l10n';
import Avatar from '../../../components/ui/Avatar';
import BubblePopup from '../../../components/common/BubblePopup';
import IssueChangelogDiff from '../components/IssueChangelogDiff';
import type { ChangelogDiff } from '../components/IssueChangelogDiff';
import type { Issue } from '../types';

type Changelog = {
avatar?: string,
creationDate: string,
diffs: Array<ChangelogDiff>,
user: string,
userName: string
};

type Props = {
issue: Issue,
onFail: (Error) => void,
popupPosition?: {}
};

type State = {
changelogs: Array<Changelog>
};

export default class ChangelogPopup extends React.PureComponent {
mounted: boolean;
props: Props;
state: State = {
changelogs: []
};

componentDidMount() {
this.mounted = true;
this.loadChangelog();
}

componentWillUnmount() {
this.mounted = false;
}

loadChangelog() {
getIssueChangelog(this.props.issue.key).then(
changelogs => {
if (this.mounted) {
this.setState({ changelogs });
}
},
this.props.onFail
);
}

render() {
const { issue } = this.props;
const { author } = issue;
return (
<BubblePopup position={this.props.popupPosition} customClass="bubble-popup-bottom-right">
<div className="issue-changelog">
<table className="spaced">
<tbody>
<tr>
<td className="thin text-left text-top nowrap">
{moment(issue.creationDate).format('LLL')}
</td>
<td className="thin text-left text-top nowrap" />
<td className="text-left text-top">
{author ? `${translate('created_by')} ${author}` : translate('created')}
</td>
</tr>

{this.state.changelogs.map((item, idx) => (
<tr key={idx}>
<td className="thin text-left text-top nowrap">
{moment(item.creationDate).format('LLL')}
</td>
<td className="thin text-left text-top nowrap">
{item.userName &&
item.avatar &&
<Avatar className="little-spacer-right" hash={item.avatar} size={16} />}
{item.userName}
</td>
<td className="text-left text-top">
{item.diffs.map(diff => <IssueChangelogDiff key={diff.key} diff={diff} />)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</BubblePopup>
);
}
}

+ 39
- 0
server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js View File

@@ -0,0 +1,39 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { translate } from '../../../helpers/l10n';
import BubblePopup from '../../../components/common/BubblePopup';

type Props = {
onDelete: () => void,
popupPosition?: {}
};

export default function CommentDeletePopup(props: Props) {
return (
<BubblePopup position={props.popupPosition} customClass="bubble-popup-bottom-right">
<div className="text-right">
<div className="spacer-bottom">{translate('issue.comment.delete_confirm_message')}</div>
<button className="button-red" onClick={props.onDelete}>{translate('delete')}</button>
</div>
</BubblePopup>
);
}

+ 113
- 0
server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js View File

@@ -0,0 +1,113 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import classNames from 'classnames';
import BubblePopup from '../../../components/common/BubblePopup';
import MarkdownTips from '../../../components/common/MarkdownTips';
import { translate } from '../../../helpers/l10n';
import type { IssueComment } from '../types';

type Props = {
comment?: IssueComment,
customClass?: string,
onComment: (string) => void,
toggleComment: (boolean) => void,
placeholder: string,
popupPosition?: {}
};

type State = {
textComment: string
};

export default class CommentPopup extends React.PureComponent {
props: Props;
state: State;

constructor(props: Props) {
super(props);
this.state = {
textComment: props.comment ? props.comment.markdown : ''
};
}

handleCommentChange = (evt: SyntheticInputEvent) => {
this.setState({ textComment: evt.target.value });
};

handleCommentClick = () => {
if (this.state.textComment.trim().length > 0) {
this.props.onComment(this.state.textComment);
}
};

handleCancelClick = (evt: MouseEvent) => {
evt.preventDefault();
this.props.toggleComment(false);
};

handleKeyboard = (evt: KeyboardEvent) => {
if (evt.keyCode === 13 && (evt.metaKey || evt.ctrlKey)) {
// Ctrl + Enter
this.handleCommentClick();
} else if ([37, 38, 39, 40].includes(evt.keyCode)) {
// Arrow keys
evt.stopPropagation();
}
};

render() {
const { comment } = this.props;
return (
<BubblePopup
position={this.props.popupPosition}
customClass={classNames(this.props.customClass, 'bubble-popup-bottom-right')}>
<div className="issue-comment-form-text">
<textarea
autoFocus={true}
placeholder={this.props.placeholder}
onChange={this.handleCommentChange}
onKeyDown={this.handleKeyboard}
value={this.state.textComment}
rows="2"
/>
</div>
<div className="issue-comment-form-footer">
<div className="issue-comment-form-actions">
<button
className="js-issue-comment-submit little-spacer-right"
disabled={this.state.textComment.trim().length < 1}
onClick={this.handleCommentClick}>
{comment && translate('save')}
{!comment && translate('issue.comment.submit')}
</button>
<a href="#" className="js-issue-comment-cancel" onClick={this.handleCancelClick}>
{translate('cancel')}
</a>
</div>
<div className="issue-comment-form-tips">
<MarkdownTips />
</div>
</div>
</BubblePopup>
);
}
}

+ 167
- 0
server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js View File

@@ -0,0 +1,167 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import classNames from 'classnames';
import { css } from 'glamor';
import { debounce, map } from 'lodash';
import Avatar from '../../../components/ui/Avatar';
import BubblePopup from '../../../components/common/BubblePopup';
import SelectList from '../../../components/common/SelectList';
import SelectListItem from '../../../components/common/SelectListItem';
import getCurrentUserFromStore from '../../../app/utils/getCurrentUserFromStore';
import { areThereCustomOrganizations } from '../../../store/organizations/utils';
import { searchMembers } from '../../../api/organizations';
import { searchUsers } from '../../../api/users';
import { translate } from '../../../helpers/l10n';
import type { Issue } from '../types';

type User = {
avatar?: string,
email?: string,
login: string,
name: string
};

type Props = {
issue: Issue,
onFail: (Error) => void,
onSelect: (string) => void,
popupPosition?: {}
};

type State = {
query: string,
users: Array<User>,
currentUser: string
};

const LIST_SIZE = 10;
const USER_MARGIN = css({ marginLeft: '24px' });

export default class SetAssigneePopup extends React.PureComponent {
defaultUsersArray: Array<User>;
organizationEnabled: boolean;
props: Props;
state: State;

constructor(props: Props) {
super(props);
this.organizationEnabled = areThereCustomOrganizations();
this.searchUsers = debounce(this.searchUsers, 250);
this.searchMembers = debounce(this.searchMembers, 250);
this.defaultUsersArray = [{ login: '', name: translate('unassigned') }];

const currentUser = getCurrentUserFromStore();
if (currentUser != null) {
this.defaultUsersArray = [currentUser, ...this.defaultUsersArray];
}

this.state = {
query: '',
users: this.defaultUsersArray,
currentUser: currentUser.login
};
}

searchMembers = (query: string) => {
searchMembers({
organization: this.props.issue.projectOrganization,
q: query,
ps: LIST_SIZE
}).then(this.handleSearchResult, this.props.onFail);
};

searchUsers = (query: string) => {
searchUsers(query, LIST_SIZE).then(this.handleSearchResult, this.props.onFail);
};

handleSearchResult = (data: Object) => {
this.setState({
users: data.users,
currentUser: data.users.length > 0 ? data.users[0].login : ''
});
};

handleSearchChange = (evt: SyntheticInputEvent) => {
const query = evt.target.value;
if (query.length < 2) {
this.setState({
query,
users: this.defaultUsersArray,
currentUser: this.defaultUsersArray[0].login
});
} else {
this.setState({ query });
if (this.organizationEnabled) {
this.searchMembers(query);
} else {
this.searchUsers(query);
}
}
};

render() {
return (
<BubblePopup
position={this.props.popupPosition}
customClass="bubble-popup-menu bubble-popup-bottom">
<div className="multi-select">
<div className="search-box menu-search">
<button className="search-box-submit button-clean">
<i className="icon-search-new" />
</button>
<input
type="search"
value={this.state.query}
className="search-box-input"
placeholder={translate('search_verb')}
onChange={this.handleSearchChange}
autoComplete="off"
autoFocus={true}
/>
</div>
<SelectList
items={map(this.state.users, 'login')}
currentItem={this.state.currentUser}
onSelect={this.props.onSelect}>
{this.state.users.map(user => (
<SelectListItem key={user.login} item={user.login}>
{(user.avatar || user.email) &&
<Avatar
className="spacer-right"
email={user.email}
hash={user.avatar}
size={16}
/>}
<span
className={classNames('vertical-middle', {
[USER_MARGIN]: !(user.avatar || user.email)
})}>
{user.name}
</span>
</SelectListItem>
))}
</SelectList>
</div>
</BubblePopup>
);
}
}

+ 86
- 0
server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js View File

@@ -0,0 +1,86 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
//@flow
import React from 'react';
import { debounce, without } from 'lodash';
import TagsSelector from '../../../components/tags/TagsSelector';
import { searchIssueTags } from '../../../api/issues';

type Props = {
popupPosition?: {},
onFail: (Error) => void,
selectedTags: Array<string>,
setTags: (Array<string>) => void
};

type State = {
searchResult: Array<string>
};

const LIST_SIZE = 10;

export default class SetIssueTagsPopup extends React.PureComponent {
props: Props;
state: State;

constructor(props: Props) {
super(props);
this.state = { searchResult: [] };
this.onSearch = debounce(this.onSearch, 250);
}

componentDidMount() {
this.onSearch('');
}

onSearch = (query: string) => {
searchIssueTags({
q: query || '',
ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100)
}).then(
(tags: Array<string>) => {
this.setState({ searchResult: tags });
},
this.props.onFail
);
};

onSelect = (tag: string) => {
this.props.setTags([...this.props.selectedTags, tag]);
};

onUnselect = (tag: string) => {
this.props.setTags(without(this.props.selectedTags, tag));
};

render() {
return (
<TagsSelector
position={this.props.popupPosition}
tags={this.state.searchResult}
selectedTags={this.props.selectedTags}
listSize={LIST_SIZE}
onSearch={this.onSearch}
onSelect={this.onSelect}
onUnselect={this.onUnselect}
/>
);
}
}

+ 59
- 0
server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js View File

@@ -0,0 +1,59 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { translate } from '../../../helpers/l10n';
import BubblePopup from '../../../components/common/BubblePopup';
import SelectList from '../../../components/common/SelectList';
import SelectListItem from '../../../components/common/SelectListItem';
import SeverityIcon from '../../../components/shared/SeverityIcon';
import type { Issue } from '../types';

type Props = {
issue: Issue,
onSelect: (string) => void,
popupPosition?: {}
};

const SEVERITY = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];

export default class SetSeverityPopup extends React.PureComponent {
props: Props;

render() {
return (
<BubblePopup
position={this.props.popupPosition}
customClass="bubble-popup-menu bubble-popup-bottom">
<SelectList
items={SEVERITY}
currentItem={this.props.issue.severity}
onSelect={this.props.onSelect}>
{SEVERITY.map(severity => (
<SelectListItem key={severity} item={severity}>
<SeverityIcon className="little-spacer-right" severity={severity} />
{translate('severity', severity)}
</SelectListItem>
))}
</SelectList>
</BubblePopup>
);
}
}

+ 57
- 0
server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js View File

@@ -0,0 +1,57 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import BubblePopup from '../../../components/common/BubblePopup';
import SelectList from '../../../components/common/SelectList';
import SelectListItem from '../../../components/common/SelectListItem';
import { translate } from '../../../helpers/l10n';

type Props = {
transitions: Array<string>,
onSelect: (string) => void,
popupPosition?: {}
};

export default class SetTransitionPopup extends React.PureComponent {
props: Props;

render() {
const { transitions } = this.props;
return (
<BubblePopup
position={this.props.popupPosition}
customClass="bubble-popup-menu bubble-popup-bottom">
<SelectList items={transitions} currentItem={transitions[0]} onSelect={this.props.onSelect}>
{transitions.map(transition => {
return (
<SelectListItem
key={transition}
item={transition}
title={translate('issue.transition', transition, 'description')}>
{translate('issue.transition', transition)}
</SelectListItem>
);
})}
</SelectList>
</BubblePopup>
);
}
}

+ 59
- 0
server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js View File

@@ -0,0 +1,59 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import { translate } from '../../../helpers/l10n';
import BubblePopup from '../../../components/common/BubblePopup';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import SelectList from '../../../components/common/SelectList';
import SelectListItem from '../../../components/common/SelectListItem';
import type { Issue } from '../types';

type Props = {
issue: Issue,
onSelect: (string) => void,
popupPosition?: {}
};

const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];

export default class SetTypePopup extends React.PureComponent {
props: Props;

render() {
return (
<BubblePopup
position={this.props.popupPosition}
customClass="bubble-popup-menu bubble-popup-bottom">
<SelectList
items={TYPES}
currentItem={this.props.issue.type}
onSelect={this.props.onSelect}>
{TYPES.map(type => (
<SelectListItem key={type} item={type}>
<IssueTypeIcon className="little-spacer-right" query={type} />
{translate('issue.type', type)}
</SelectListItem>
))}
</SelectList>
</BubblePopup>
);
}
}

+ 46
- 0
server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js View File

@@ -0,0 +1,46 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import ChangelogPopup from '../ChangelogPopup';

it('should render the changelog popup correctly', () => {
const element = shallow(
<ChangelogPopup
issue={{
key: 'issuekey',
author: 'john.david.dalton@gmail.com',
creationDate: '2017-03-01T09:36:01+0100'
}}
onFail={jest.fn()}
/>
);
element.setState({
changelogs: [
{
creationDate: '2017-03-01T09:36:01+0100',
userName: 'john.doe',
avatar: 'gravatarhash',
diffs: [{ key: 'severity', newValue: 'MINOR', oldValue: 'CRITICAL' }]
}
]
});
expect(element).toMatchSnapshot();
});

+ 31
- 0
server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js View File

@@ -0,0 +1,31 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import CommentDeletePopup from '../CommentDeletePopup';
import { click } from '../../../../helpers/testUtils';

it('should render the comment delete popup correctly', () => {
const onDelete = jest.fn();
const element = shallow(<CommentDeletePopup onDelete={onDelete} />);
expect(element).toMatchSnapshot();
click(element.find('button'));
expect(onDelete.mock.calls.length).toBe(1);
});

+ 66
- 0
server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js View File

@@ -0,0 +1,66 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import CommentPopup from '../CommentPopup';
import { click } from '../../../../helpers/testUtils';

it('should render the comment popup correctly without existing comment', () => {
const element = shallow(
<CommentPopup
onComment={jest.fn()}
toggleComment={jest.fn()}
placeholder="placeholder test"
customClass="myclass"
/>
);
expect(element).toMatchSnapshot();
});

it('should render the comment popup correctly when changing a comment', () => {
const element = shallow(
<CommentPopup
comment={{
markdown: '*test*'
}}
onComment={jest.fn()}
toggleComment={jest.fn()}
placeholder=""
/>
);
expect(element).toMatchSnapshot();
});

it('should render not allow to send comment with only spaces', () => {
const onComment = jest.fn();
const element = shallow(
<CommentPopup
onComment={onComment}
toggleComment={jest.fn()}
placeholder="placeholder test"
customClass="myclass"
/>
);
click(element.find('button.js-issue-comment-submit'));
expect(onComment.mock.calls.length).toBe(0);
element.setState({ textComment: 'mycomment' });
click(element.find('button.js-issue-comment-submit'));
expect(onComment.mock.calls.length).toBe(1);
});

server/sonar-web/src/main/js/components/shared/severity-helper.js → server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.js View File

@@ -17,21 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import SeverityIcon from './severity-icon';
import { translate } from '../../helpers/l10n';
import SetIssueTagsPopup from '../SetIssueTagsPopup';

export default React.createClass({
render() {
if (!this.props.severity) {
return null;
}
return (
<span>
<SeverityIcon severity={this.props.severity} />
{' '}
{translate('severity', this.props.severity)}
</span>
);
}
it('should render tags popup correctly', () => {
const element = shallow(
<SetIssueTagsPopup onFail={jest.fn()} selectedTags="mytag" setTags={jest.fn()} />
);
element.setState({ searchResult: ['mytag', 'test', 'second'] });
expect(element).toMatchSnapshot();
});

+ 27
- 0
server/sonar-web/src/main/js/components/issue/popups/__tests__/SetSeverityPopup-test.js View File

@@ -0,0 +1,27 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import SetSeverityPopup from '../SetSeverityPopup';

it('should render tags popup correctly', () => {
const element = shallow(<SetSeverityPopup issue={{ severity: 'MAJOR' }} onSelect={jest.fn()} />);
expect(element).toMatchSnapshot();
});

+ 32
- 0
server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTransitionPopup-test.js View File

@@ -0,0 +1,32 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import SetTransitionPopup from '../SetTransitionPopup';

it('should render tags popup correctly', () => {
const element = shallow(
<SetTransitionPopup
onSelect={jest.fn()}
transitions={['confirm', 'resolve', 'falsepositive', 'wontfix']}
/>
);
expect(element).toMatchSnapshot();
});

+ 27
- 0
server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTypePopup-test.js View File

@@ -0,0 +1,27 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import React from 'react';
import SetTypePopup from '../SetTypePopup';

it('should render tags popup correctly', () => {
const element = shallow(<SetTypePopup issue={{ type: 'BUG' }} onSelect={jest.fn()} />);
expect(element).toMatchSnapshot();
});

+ 50
- 0
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap View File

@@ -0,0 +1,50 @@
exports[`test should render the changelog popup correctly 1`] = `
<BubblePopup
customClass="bubble-popup-bottom-right">
<div
className="issue-changelog">
<table
className="spaced">
<tbody>
<tr>
<td
className="thin text-left text-top nowrap">
March 1, 2017 9:36 AM
</td>
<td
className="thin text-left text-top nowrap" />
<td
className="text-left text-top">
created_by john.david.dalton@gmail.com
</td>
</tr>
<tr>
<td
className="thin text-left text-top nowrap">
March 1, 2017 9:36 AM
</td>
<td
className="thin text-left text-top nowrap">
<Connect(Avatar)
className="little-spacer-right"
hash="gravatarhash"
size={16} />
john.doe
</td>
<td
className="text-left text-top">
<IssueChangelogDiff
diff={
Object {
"key": "severity",
"newValue": "MINOR",
"oldValue": "CRITICAL",
}
} />
</td>
</tr>
</tbody>
</table>
</div>
</BubblePopup>
`;

+ 17
- 0
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap View File

@@ -0,0 +1,17 @@
exports[`test should render the comment delete popup correctly 1`] = `
<BubblePopup
customClass="bubble-popup-bottom-right">
<div
className="text-right">
<div
className="spacer-bottom">
issue.comment.delete_confirm_message
</div>
<button
className="button-red"
onClick={[Function]}>
delete
</button>
</div>
</BubblePopup>
`;

+ 75
- 0
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap View File

@@ -0,0 +1,75 @@
exports[`test should render the comment popup correctly when changing a comment 1`] = `
<BubblePopup
customClass="bubble-popup-bottom-right">
<div
className="issue-comment-form-text">
<textarea
autoFocus={true}
onChange={[Function]}
onKeyDown={[Function]}
placeholder=""
rows="2"
value="*test*" />
</div>
<div
className="issue-comment-form-footer">
<div
className="issue-comment-form-actions">
<button
className="js-issue-comment-submit little-spacer-right"
disabled={false}
onClick={[Function]}>
save
</button>
<a
className="js-issue-comment-cancel"
href="#"
onClick={[Function]}>
cancel
</a>
</div>
<div
className="issue-comment-form-tips">
<MarkdownTips />
</div>
</div>
</BubblePopup>
`;

exports[`test should render the comment popup correctly without existing comment 1`] = `
<BubblePopup
customClass="myclass bubble-popup-bottom-right">
<div
className="issue-comment-form-text">
<textarea
autoFocus={true}
onChange={[Function]}
onKeyDown={[Function]}
placeholder="placeholder test"
rows="2"
value="" />
</div>
<div
className="issue-comment-form-footer">
<div
className="issue-comment-form-actions">
<button
className="js-issue-comment-submit little-spacer-right"
disabled={true}
onClick={[Function]}>
issue.comment.submit
</button>
<a
className="js-issue-comment-cancel"
href="#"
onClick={[Function]}>
cancel
</a>
</div>
<div
className="issue-comment-form-tips">
<MarkdownTips />
</div>
</div>
</BubblePopup>
`;

+ 15
- 0
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.js.snap View File

@@ -0,0 +1,15 @@
exports[`test should render tags popup correctly 1`] = `
<TagsSelector
listSize={10}
onSearch={[Function]}
onSelect={[Function]}
onUnselect={[Function]}
selectedTags="mytag"
tags={
Array [
"mytag",
"test",
"second",
]
} />
`;

+ 53
- 0
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap View File

@@ -0,0 +1,53 @@
exports[`test should render tags popup correctly 1`] = `
<BubblePopup
customClass="bubble-popup-menu bubble-popup-bottom">
<SelectList
currentItem="MAJOR"
items={
Array [
"BLOCKER",
"CRITICAL",
"MAJOR",
"MINOR",
"INFO",
]
}
onSelect={[Function]}>
<SelectListItem
item="BLOCKER">
<SeverityIcon
className="little-spacer-right"
severity="BLOCKER" />
severity.BLOCKER
</SelectListItem>
<SelectListItem
item="CRITICAL">
<SeverityIcon
className="little-spacer-right"
severity="CRITICAL" />
severity.CRITICAL
</SelectListItem>
<SelectListItem
item="MAJOR">
<SeverityIcon
className="little-spacer-right"
severity="MAJOR" />
severity.MAJOR
</SelectListItem>
<SelectListItem
item="MINOR">
<SeverityIcon
className="little-spacer-right"
severity="MINOR" />
severity.MINOR
</SelectListItem>
<SelectListItem
item="INFO">
<SeverityIcon
className="little-spacer-right"
severity="INFO" />
severity.INFO
</SelectListItem>
</SelectList>
</BubblePopup>
`;

+ 37
- 0
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap View File

@@ -0,0 +1,37 @@
exports[`test should render tags popup correctly 1`] = `
<BubblePopup
customClass="bubble-popup-menu bubble-popup-bottom">
<SelectList
currentItem="confirm"
items={
Array [
"confirm",
"resolve",
"falsepositive",
"wontfix",
]
}
onSelect={[Function]}>
<SelectListItem
item="confirm"
title="issue.transition.confirm.description">
issue.transition.confirm
</SelectListItem>
<SelectListItem
item="resolve"
title="issue.transition.resolve.description">
issue.transition.resolve
</SelectListItem>
<SelectListItem
item="falsepositive"
title="issue.transition.falsepositive.description">
issue.transition.falsepositive
</SelectListItem>
<SelectListItem
item="wontfix"
title="issue.transition.wontfix.description">
issue.transition.wontfix
</SelectListItem>
</SelectList>
</BubblePopup>
`;

+ 37
- 0
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap View File

@@ -0,0 +1,37 @@
exports[`test should render tags popup correctly 1`] = `
<BubblePopup
customClass="bubble-popup-menu bubble-popup-bottom">
<SelectList
currentItem="BUG"
items={
Array [
"BUG",
"VULNERABILITY",
"CODE_SMELL",
]
}
onSelect={[Function]}>
<SelectListItem
item="BUG">
<IssueTypeIcon
className="little-spacer-right"
query="BUG" />
issue.type.BUG
</SelectListItem>
<SelectListItem
item="VULNERABILITY">
<IssueTypeIcon
className="little-spacer-right"
query="VULNERABILITY" />
issue.type.VULNERABILITY
</SelectListItem>
<SelectListItem
item="CODE_SMELL">
<IssueTypeIcon
className="little-spacer-right"
query="CODE_SMELL" />
issue.type.CODE_SMELL
</SelectListItem>
</SelectList>
</BubblePopup>
`;

+ 32
- 1
server/sonar-web/src/main/js/components/issue/types.js View File

@@ -30,13 +30,44 @@ export type FlowLocation = {
textRange?: TextRange
};

export type IssueComment = {
author?: string,
authorActive?: boolean,
authorAvatar?: string,
authorLogin?: string,
authorName?: string,
createdAt: string,
htmlText: string,
key: string,
markdown: string,
updatable: boolean
};

export type Issue = {
actions: Array<string>,
assignee?: string,
assigneeActive?: string,
assigneeAvatar?: string,
assigneeLogin?: string,
assigneeName?: string,
author?: string,
comments?: Array<IssueComment>,
creationDate: string,
effort?: string,
key: string,
flows: Array<{
locations?: Array<FlowLocation>
}>,
line?: number,
message: string,
organization: string,
projectOrganization: string,
resolution?: string,
rule: string,
severity: string,
textRange: TextRange
status: string,
tags?: Array<string>,
textRange: TextRange,
transitions?: Array<string>,
type: string
};

+ 36
- 0
server/sonar-web/src/main/js/components/shared/SeverityHelper.js View File

@@ -0,0 +1,36 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
//@flow
import React from 'react';
import SeverityIcon from './SeverityIcon';
import { translate } from '../../helpers/l10n';

export default function SeverityHelper(props: { severity: ?string, className?: string }) {
const { severity } = props;
if (!severity) {
return null;
}
return (
<span className={props.className}>
<SeverityIcon className="little-spacer-right" severity={severity} />
{translate('severity', severity)}
</span>
);
}

+ 30
- 0
server/sonar-web/src/main/js/components/shared/SeverityIcon.js View File

@@ -0,0 +1,30 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
//@flow
import React from 'react';
import classNames from 'classnames';

export default function SeverityIcon(props: { severity: ?string, className?: string }) {
if (!props.severity) {
return null;
}
const className = classNames('icon-severity-' + props.severity.toLowerCase(), props.className);
return <i className={className} />;
}

+ 37
- 0
server/sonar-web/src/main/js/components/shared/StatusHelper.js View File

@@ -0,0 +1,37 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
//@flow
import React from 'react';
import StatusIcon from './StatusIcon';
import { translate } from '../../helpers/l10n';

export default function StatusHelper(
props: { resolution?: string, status: string, className?: string }
) {
const resolution = props.resolution != null &&
` (${translate('issue.resolution', props.resolution)})`;
return (
<span className={props.className}>
<StatusIcon className="little-spacer-right" status={props.status} />
{translate('issue.status', props.status)}
{resolution}
</span>
);
}

+ 27
- 0
server/sonar-web/src/main/js/components/shared/StatusIcon.js View File

@@ -0,0 +1,27 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
//@flow
import React from 'react';
import classNames from 'classnames';

export default function StatusIcon(props: { status: string, className?: string }) {
const className = classNames('icon-status-' + props.status.toLowerCase(), props.className);
return <i className={className} />;
}

+ 1
- 6
server/sonar-web/src/main/js/components/tags/TagsList.css View File

@@ -6,16 +6,11 @@
font-size: 12px;
}

.tags-list i.icon-dropdown::before {
top: 1px;
}

.tags-list span {
display: inline-block;
vertical-align: text-top;
vertical-align: middle;
text-align: left;
max-width: 220px;
padding-left: 4px;
padding-right: 4px;
margin-top: 2px;
}

+ 3
- 8
server/sonar-web/src/main/js/components/tags/TagsList.js View File

@@ -25,7 +25,6 @@ import './TagsList.css';
type Props = {
tags: Array<string>,
allowUpdate: boolean,
allowMultiLine: boolean,
customClass?: string
};

@@ -33,23 +32,19 @@ export default class TagsList extends React.PureComponent {
props: Props;

static defaultProps = {
allowUpdate: false,
allowMultiLine: false
allowUpdate: false
};

render() {
const { tags, allowUpdate } = this.props;
const spanClass = classNames({
note: !allowUpdate,
'text-ellipsis': !this.props.allowMultiLine
});
const spanClass = classNames('text-ellipsis', { note: !allowUpdate });
const tagListClass = classNames('tags-list', this.props.customClass);

return (
<span className={tagListClass} title={tags.join(', ')}>
<i className="icon-tags icon-half-transparent" />
<span className={spanClass}>{tags.join(', ')}</span>
{allowUpdate && <i className="icon-dropdown icon-half-transparent" />}
{allowUpdate && <i className="icon-dropdown" />}
</span>
);
}

+ 2
- 2
server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.js View File

@@ -39,9 +39,9 @@ it('should correctly handle a lot of tags', () => {
for (let i = 0; i < 20; i++) {
lotOfTags.push(tags);
}
const taglist = shallow(<TagsList tags={lotOfTags} allowMultiLine={true} />);
const taglist = shallow(<TagsList tags={lotOfTags} />);
expect(taglist.text()).toBe(lotOfTags.join(', '));
expect(taglist.find('span.note').hasClass('text-ellipsis')).toBe(false);
expect(taglist.find('span.note').hasClass('text-ellipsis')).toBe(true);
});

it('should render with a caret on the right if update is allowed', () => {

+ 1
- 1
server/sonar-web/src/main/js/components/ui/IssueTypeIcon.js View File

@@ -23,7 +23,7 @@ import BugIcon from './BugIcon';
import VulnerabilityIcon from './VulnerabilityIcon';
import CodeSmellIcon from './CodeSmellIcon';

export default class IssueTypeIcon extends React.Component {
export default class IssueTypeIcon extends React.PureComponent {
props: {
className?: string,
query: string

+ 2
- 0
server/sonar-web/src/main/js/helpers/testUtils.js View File

@@ -36,3 +36,5 @@ export const change = (element, value) =>
target: { value },
currentTarget: { value }
});

export const keydown = (element, keyCode) => element.simulate('keyDown', { ...mockEvent, keyCode });

+ 13
- 0
server/sonar-web/src/main/js/helpers/urls.js View File

@@ -64,6 +64,15 @@ export function getComponentIssuesUrl(componentKey, query) {
return '/component_issues?id=' + encodeURIComponent(componentKey) + '#' + serializedQuery;
}

/**
* Generate URL for a single issue
* @param {string} issueKey
* @returns {string}
*/
export function getSingleIssueUrl(issueKey) {
return window.baseUrl + '/issues/search#issues=' + issueKey;
}

/**
* Generate URL for a component's drilldown page
* @param {string} componentKey
@@ -140,3 +149,7 @@ export function getDeprecatedActiveRulesUrl(query = {}, organization?: string) {
export const getProjectsUrl = () => {
return window.baseUrl + '/projects';
};

export const getMarkdownHelpUrl = () => {
return window.baseUrl + '/markdown/help';
};

+ 12
- 0
server/sonar-web/src/main/less/components/bubble-popup.less View File

@@ -104,6 +104,18 @@
overflow: auto;
}

.bubble-popup-helper {
position: relative;

&:focus {
outline: none;
}
}

.bubble-popup-helper-inline {
display: inline-block;
}

.bubble-popup-title {
margin-bottom: 5px;
font-weight: 600;

+ 0
- 0
server/sonar-web/src/main/less/components/issues.less View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save