aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/api/issues.js67
-rw-r--r--server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js94
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js6
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap5
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.js.snap1
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js2
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js2
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap4
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap4
-rw-r--r--server/sonar-web/src/main/js/components/common/BubblePopupHelper.js109
-rw-r--r--server/sonar-web/src/main/js/components/common/MarkdownTips.js44
-rw-r--r--server/sonar-web/src/main/js/components/common/MultiSelect.js6
-rw-r--r--server/sonar-web/src/main/js/components/common/SelectList.js133
-rw-r--r--server/sonar-web/src/main/js/components/common/SelectListItem.js76
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js139
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/MarkdownTips-test.js (renamed from server/sonar-web/src/main/js/components/shared/severity-icon.js)12
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js75
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/SelectListItem-test.js44
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap148
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.js.snap29
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap83
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectListItem-test.js.snap59
-rw-r--r--server/sonar-web/src/main/js/components/controls/Checkbox.js5
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/Checkbox-test.js7
-rw-r--r--server/sonar-web/src/main/js/components/issue/BaseIssue.js153
-rw-r--r--server/sonar-web/src/main/js/components/issue/ConnectedIssue.js11
-rw-r--r--server/sonar-web/src/main/js/components/issue/Issue.js134
-rw-r--r--server/sonar-web/src/main/js/components/issue/IssueView.js121
-rw-r--r--server/sonar-web/src/main/js/components/issue/actions.js52
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js164
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueAssign.js85
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js65
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.js71
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js72
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js122
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueMessage.js53
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js70
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueTags.js90
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js91
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueTransition.js80
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueType.js73
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js75
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js62
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js53
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js64
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.js33
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js70
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js77
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js51
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js91
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js70
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap111
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap68
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap49
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap205
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.js.snap10
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap75
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap89
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap124
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap108
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap80
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js116
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js39
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js113
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js167
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js86
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js59
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js57
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js59
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js46
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js31
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js66
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.js (renamed from server/sonar-web/src/main/js/components/shared/severity-helper.js)23
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/SetSeverityPopup-test.js27
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTransitionPopup-test.js32
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTypePopup-test.js27
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap50
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap17
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap75
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.js.snap15
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap53
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap37
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap37
-rw-r--r--server/sonar-web/src/main/js/components/issue/types.js33
-rw-r--r--server/sonar-web/src/main/js/components/shared/SeverityHelper.js36
-rw-r--r--server/sonar-web/src/main/js/components/shared/SeverityIcon.js30
-rw-r--r--server/sonar-web/src/main/js/components/shared/StatusHelper.js37
-rw-r--r--server/sonar-web/src/main/js/components/shared/StatusIcon.js27
-rw-r--r--server/sonar-web/src/main/js/components/tags/TagsList.css7
-rw-r--r--server/sonar-web/src/main/js/components/tags/TagsList.js11
-rw-r--r--server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.js4
-rw-r--r--server/sonar-web/src/main/js/components/ui/IssueTypeIcon.js2
-rw-r--r--server/sonar-web/src/main/js/helpers/testUtils.js2
-rw-r--r--server/sonar-web/src/main/js/helpers/urls.js13
-rw-r--r--server/sonar-web/src/main/less/components/bubble-popup.less12
-rw-r--r--server/sonar-web/src/main/less/components/issues.less6
-rw-r--r--server/sonar-web/src/main/less/components/menu.less6
-rw-r--r--server/sonar-web/src/main/less/init/forms.less7
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties1
103 files changed, 5503 insertions, 197 deletions
diff --git a/server/sonar-web/src/main/js/api/issues.js b/server/sonar-web/src/main/js/api/issues.js
index cb5bfeb951e..912f8b2f6d3 100644
--- a/server/sonar-web/src/main/js/api/issues.js
+++ b/server/sonar-web/src/main/js/api/issues.js
@@ -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(),
diff --git a/server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js b/server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js
new file mode 100644
index 00000000000..cadec2974e3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js
index 33152d954f4..6b2509eab90 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js
+++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js
@@ -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>
);
}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap
index a92d8b33fb1..b36fabcffcf 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap
@@ -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 [
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js
index 1db66ab8553..14b7d37e613 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js
+++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js
@@ -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}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.js.snap
index 8eda732fdbb..017cb79cab6 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ProjectCard-test.js.snap
@@ -60,7 +60,6 @@ exports[`test should display tags 1`] = `
</Link>
</h2>
<TagsList
- allowMultiLine={false}
allowUpdate={false}
customClass="spacer-left"
tags={
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js
index 5497d9ad0fb..d6f03008f15 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/SeverityChange.js
@@ -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 = {
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js
index d6f5e10e353..8597dd0ab8c 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/changelog/__tests__/SeverityChange-test.js
@@ -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);
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js
index ad79758344a..0e5e292b694 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonResults.js
@@ -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';
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js
index 41b3b90bb12..d346aa2f480 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonResults-test.js
@@ -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(
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js
index 78383dac350..daf1785ffd2 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js
@@ -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';
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap
index a945f7600ad..fecacdb7488 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap
@@ -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">
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap
index 9279cc173b3..30bbfaa7779 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap
@@ -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} />
diff --git a/server/sonar-web/src/main/js/components/common/BubblePopupHelper.js b/server/sonar-web/src/main/js/components/common/BubblePopupHelper.js
new file mode 100644
index 00000000000..d298166d630
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/BubblePopupHelper.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/common/MarkdownTips.js b/server/sonar-web/src/main/js/components/common/MarkdownTips.js
new file mode 100644
index 00000000000..2d83b6aeb24
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/MarkdownTips.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/common/MultiSelect.js b/server/sonar-web/src/main/js/components/common/MultiSelect.js
index ecc74825395..2faa0f2ef81 100644
--- a/server/sonar-web/src/main/js/components/common/MultiSelect.js
+++ b/server/sonar-web/src/main/js/components/common/MultiSelect.js
@@ -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]);
diff --git a/server/sonar-web/src/main/js/components/common/SelectList.js b/server/sonar-web/src/main/js/components/common/SelectList.js
new file mode 100644
index 00000000000..ba2f82b34b7
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/SelectList.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/common/SelectListItem.js b/server/sonar-web/src/main/js/components/common/SelectListItem.js
new file mode 100644
index 00000000000..9d432fd7faf
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/SelectListItem.js
@@ -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();
+ }
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js b/server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js
new file mode 100644
index 00000000000..d17527119b7
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/__tests__/BubblePopupHelper-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/shared/severity-icon.js b/server/sonar-web/src/main/js/components/common/__tests__/MarkdownTips-test.js
index 6c5e5d4a452..366d79dd3fd 100644
--- a/server/sonar-web/src/main/js/components/shared/severity-icon.js
+++ b/server/sonar-web/src/main/js/components/common/__tests__/MarkdownTips-test.js
@@ -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();
});
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js b/server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js
new file mode 100644
index 00000000000..9c0e88e6aa3
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js
@@ -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
+});
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/SelectListItem-test.js b/server/sonar-web/src/main/js/components/common/__tests__/SelectListItem-test.js
new file mode 100644
index 00000000000..235be5bfae6
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/__tests__/SelectListItem-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap
new file mode 100644
index 00000000000..5892f90b6f8
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BubblePopupHelper-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.js.snap
new file mode 100644
index 00000000000..864d20d48b4
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MarkdownTips-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap
new file mode 100644
index 00000000000..4cf15f469cb
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectListItem-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectListItem-test.js.snap
new file mode 100644
index 00000000000..4aaf6adfdc8
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectListItem-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/controls/Checkbox.js b/server/sonar-web/src/main/js/components/controls/Checkbox.js
index 40554f1027d..f5e7289dd45 100644
--- a/server/sonar-web/src/main/js/components/controls/Checkbox.js
+++ b/server/sonar-web/src/main/js/components/controls/Checkbox.js
@@ -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
});
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/Checkbox-test.js b/server/sonar-web/src/main/js/components/controls/__tests__/Checkbox-test.js
index c6692cc751e..8d299009cbd 100644
--- a/server/sonar-web/src/main/js/components/controls/__tests__/Checkbox-test.js
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/Checkbox-test.js
@@ -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);
+});
diff --git a/server/sonar-web/src/main/js/components/issue/BaseIssue.js b/server/sonar-web/src/main/js/components/issue/BaseIssue.js
new file mode 100644
index 00000000000..d4ada02869b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/BaseIssue.js
@@ -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}
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/ConnectedIssue.js b/server/sonar-web/src/main/js/components/issue/ConnectedIssue.js
index 28be4c7ba4b..67d71fe37cc 100644
--- a/server/sonar-web/src/main/js/components/issue/ConnectedIssue.js
+++ b/server/sonar-web/src/main/js/components/issue/ConnectedIssue.js
@@ -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);
diff --git a/server/sonar-web/src/main/js/components/issue/Issue.js b/server/sonar-web/src/main/js/components/issue/Issue.js
index 67f0083f87e..a121bf738d0 100644
--- a/server/sonar-web/src/main/js/components/issue/Issue.js
+++ b/server/sonar-web/src/main/js/components/issue/Issue.js
@@ -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);
diff --git a/server/sonar-web/src/main/js/components/issue/IssueView.js b/server/sonar-web/src/main/js/components/issue/IssueView.js
new file mode 100644
index 00000000000..52ee7e95280
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/IssueView.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/actions.js b/server/sonar-web/src/main/js/components/issue/actions.js
new file mode 100644
index 00000000000..a0631c17001
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/actions.js
@@ -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]));
+ }
+ }
+ );
+ };
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js
new file mode 100644
index 00000000000..e60bc87c991
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js b/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js
new file mode 100644
index 00000000000..836ef6a5a8c
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueAssign.js
@@ -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();
+ }
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js
new file mode 100644
index 00000000000..864886af959
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueChangelog.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.js b/server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.js
new file mode 100644
index 00000000000..4006a140956
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.js
@@ -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>;
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js
new file mode 100644
index 00000000000..f5f6bf5b8d3
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js
new file mode 100644
index 00000000000..6e2b9595997
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js
new file mode 100644
index 00000000000..ccbd4f1ec89
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js b/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js
new file mode 100644
index 00000000000..7e52e538742
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueSeverity.js
@@ -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} />;
+ }
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTags.js b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js
new file mode 100644
index 00000000000..ab850061b7d
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js
@@ -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}
+ />
+ );
+ }
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js
new file mode 100644
index 00000000000..4f847049f54
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js
new file mode 100644
index 00000000000..03cd4e41d86
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js
@@ -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}
+ />
+ );
+ }
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueType.js b/server/sonar-web/src/main/js/components/issue/components/IssueType.js
new file mode 100644
index 00000000000..df1d8740031
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueType.js
@@ -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>
+ );
+ }
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js
new file mode 100644
index 00000000000..9fb88a41d00
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueAssign-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js
new file mode 100644
index 00000000000..ca4a95ff08b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js
new file mode 100644
index 00000000000..c8ac8e05986
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentAction-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js
new file mode 100644
index 00000000000..d681183f2c3
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.js
new file mode 100644
index 00000000000..ca0c7f1ad5e
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js
new file mode 100644
index 00000000000..528110e91f5
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueSeverity-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js
new file mode 100644
index 00000000000..ac17925d7bc
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTags-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js
new file mode 100644
index 00000000000..3e110b92f36
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js
new file mode 100644
index 00000000000..a450a1ed2cc
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTransition-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js
new file mode 100644
index 00000000000..0ae65de665c
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueType-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap
new file mode 100644
index 00000000000..f2b10d1c0d1
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueAssign-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap
new file mode 100644
index 00000000000..291bae1a354
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueChangelog-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap
new file mode 100644
index 00000000000..600ded2a0bd
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentAction-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap
new file mode 100644
index 00000000000..7be3a2fb768
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.js.snap
new file mode 100644
index 00000000000..208f8ba1a45
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueMessage-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap
new file mode 100644
index 00000000000..5703fe176f6
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueSeverity-test.js.snap
@@ -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" />
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap
new file mode 100644
index 00000000000..c699b819c61
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap
@@ -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",
+ ]
+ } />
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap
new file mode 100644
index 00000000000..f51811bbd0f
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap
new file mode 100644
index 00000000000..c03d91d40d1
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTransition-test.js.snap
@@ -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" />
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap
new file mode 100644
index 00000000000..4ba45b49706
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueType-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js b/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js
new file mode 100644
index 00000000000..49bdee8218e
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/ChangelogPopup.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js b/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js
new file mode 100644
index 00000000000..3e06c95c07d
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/CommentDeletePopup.js
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js
new file mode 100644
index 00000000000..cb079327d53
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js
new file mode 100644
index 00000000000..c700c3a341f
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js
new file mode 100644
index 00000000000..f8584f59f07
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js
@@ -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}
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js
new file mode 100644
index 00000000000..bef971615d5
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/SetSeverityPopup.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js
new file mode 100644
index 00000000000..d1c83099551
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/SetTransitionPopup.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js
new file mode 100644
index 00000000000..7bd10d57ede
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/SetTypePopup.js
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js
new file mode 100644
index 00000000000..6c4f9d5977e
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js
new file mode 100644
index 00000000000..64a42433849
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentDeletePopup-test.js
@@ -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);
+});
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js
new file mode 100644
index 00000000000..2eac23a4e32
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/CommentPopup-test.js
@@ -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);
+});
diff --git a/server/sonar-web/src/main/js/components/shared/severity-helper.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.js
index 544afa754d1..c9d1c0a4e23 100644
--- a/server/sonar-web/src/main/js/components/shared/severity-helper.js
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.js
@@ -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();
});
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetSeverityPopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetSeverityPopup-test.js
new file mode 100644
index 00000000000..ed0f67680b8
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetSeverityPopup-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTransitionPopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTransitionPopup-test.js
new file mode 100644
index 00000000000..4d54d66b413
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTransitionPopup-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTypePopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTypePopup-test.js
new file mode 100644
index 00000000000..70b7f7a95a6
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetTypePopup-test.js
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap
new file mode 100644
index 00000000000..aa5c866b59c
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/ChangelogPopup-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap
new file mode 100644
index 00000000000..23e0864e519
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentDeletePopup-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap
new file mode 100644
index 00000000000..b4ab112f14f
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/CommentPopup-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.js.snap
new file mode 100644
index 00000000000..91a5c0b4d6a
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.js.snap
@@ -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",
+ ]
+ } />
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap
new file mode 100644
index 00000000000..0c329d01adc
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetSeverityPopup-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap
new file mode 100644
index 00000000000..08072d269b9
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTransitionPopup-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap
new file mode 100644
index 00000000000..a6719f89bf5
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetTypePopup-test.js.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/issue/types.js b/server/sonar-web/src/main/js/components/issue/types.js
index 9d3982f8f28..690c38146cb 100644
--- a/server/sonar-web/src/main/js/components/issue/types.js
+++ b/server/sonar-web/src/main/js/components/issue/types.js
@@ -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
};
diff --git a/server/sonar-web/src/main/js/components/shared/SeverityHelper.js b/server/sonar-web/src/main/js/components/shared/SeverityHelper.js
new file mode 100644
index 00000000000..2ac4e149f1e
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/shared/SeverityHelper.js
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/shared/SeverityIcon.js b/server/sonar-web/src/main/js/components/shared/SeverityIcon.js
new file mode 100644
index 00000000000..aea18812a32
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/shared/SeverityIcon.js
@@ -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} />;
+}
diff --git a/server/sonar-web/src/main/js/components/shared/StatusHelper.js b/server/sonar-web/src/main/js/components/shared/StatusHelper.js
new file mode 100644
index 00000000000..092e3524b70
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/shared/StatusHelper.js
@@ -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>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/shared/StatusIcon.js b/server/sonar-web/src/main/js/components/shared/StatusIcon.js
new file mode 100644
index 00000000000..b98b4cccfd3
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/shared/StatusIcon.js
@@ -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} />;
+}
diff --git a/server/sonar-web/src/main/js/components/tags/TagsList.css b/server/sonar-web/src/main/js/components/tags/TagsList.css
index 886a83ff628..8271abfeef5 100644
--- a/server/sonar-web/src/main/js/components/tags/TagsList.css
+++ b/server/sonar-web/src/main/js/components/tags/TagsList.css
@@ -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;
}
diff --git a/server/sonar-web/src/main/js/components/tags/TagsList.js b/server/sonar-web/src/main/js/components/tags/TagsList.js
index 6f5e9d29029..08a3d4c91b1 100644
--- a/server/sonar-web/src/main/js/components/tags/TagsList.js
+++ b/server/sonar-web/src/main/js/components/tags/TagsList.js
@@ -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>
);
}
diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.js b/server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.js
index 9eec4be8ed2..438730aff86 100644
--- a/server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.js
+++ b/server/sonar-web/src/main/js/components/tags/__tests__/TagsList-test.js
@@ -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', () => {
diff --git a/server/sonar-web/src/main/js/components/ui/IssueTypeIcon.js b/server/sonar-web/src/main/js/components/ui/IssueTypeIcon.js
index 95e9b372da0..32365063111 100644
--- a/server/sonar-web/src/main/js/components/ui/IssueTypeIcon.js
+++ b/server/sonar-web/src/main/js/components/ui/IssueTypeIcon.js
@@ -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
diff --git a/server/sonar-web/src/main/js/helpers/testUtils.js b/server/sonar-web/src/main/js/helpers/testUtils.js
index 05104c5482a..602eb3c5786 100644
--- a/server/sonar-web/src/main/js/helpers/testUtils.js
+++ b/server/sonar-web/src/main/js/helpers/testUtils.js
@@ -36,3 +36,5 @@ export const change = (element, value) =>
target: { value },
currentTarget: { value }
});
+
+export const keydown = (element, keyCode) => element.simulate('keyDown', { ...mockEvent, keyCode });
diff --git a/server/sonar-web/src/main/js/helpers/urls.js b/server/sonar-web/src/main/js/helpers/urls.js
index 1d4097f95c7..34b3b06d89d 100644
--- a/server/sonar-web/src/main/js/helpers/urls.js
+++ b/server/sonar-web/src/main/js/helpers/urls.js
@@ -65,6 +65,15 @@ export function getComponentIssuesUrl(componentKey, query) {
}
/**
+ * 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
* @param {string} metric
@@ -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';
+};
diff --git a/server/sonar-web/src/main/less/components/bubble-popup.less b/server/sonar-web/src/main/less/components/bubble-popup.less
index f3ab73adbd1..1986221db05 100644
--- a/server/sonar-web/src/main/less/components/bubble-popup.less
+++ b/server/sonar-web/src/main/less/components/bubble-popup.less
@@ -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;
diff --git a/server/sonar-web/src/main/less/components/issues.less b/server/sonar-web/src/main/less/components/issues.less
index 8202664e4cc..658462e620c 100644
--- a/server/sonar-web/src/main/less/components/issues.less
+++ b/server/sonar-web/src/main/less/components/issues.less
@@ -161,6 +161,7 @@
max-width: 540px;
max-height: 320px;
overflow: auto;
+ white-space: normal;
}
.issue-comments {
@@ -242,6 +243,11 @@ input.issue-action-options-search {
}
}
+.issue-edit-comment-bubble-popup {
+ width: 440px;
+ font-size: @smallFontSize;
+}
+
.issue-comment-form-text {
}
diff --git a/server/sonar-web/src/main/less/components/menu.less b/server/sonar-web/src/main/less/components/menu.less
index 1d75e2cef98..08ff56e2fc3 100644
--- a/server/sonar-web/src/main/less/components/menu.less
+++ b/server/sonar-web/src/main/less/components/menu.less
@@ -32,6 +32,10 @@
background-color: #fff;
background-clip: padding-box;
+ &:focus {
+ outline: none;
+ }
+
> li > a,
> li > span {
display: block;
@@ -78,7 +82,7 @@
padding: 4px 16px 0;
.search-box-input { font-size: @smallFontSize; }
-
+
.search-box-submit { vertical-align: baseline; }
}
diff --git a/server/sonar-web/src/main/less/init/forms.less b/server/sonar-web/src/main/less/init/forms.less
index bca41f0afcf..2871f7e7908 100644
--- a/server/sonar-web/src/main/less/init/forms.less
+++ b/server/sonar-web/src/main/less/init/forms.less
@@ -198,11 +198,16 @@ input[type="submit"].button-grey {
line-height: inherit;
transition: all 0.2s ease;
- &:hover, &:active, &:focus {
+ &:hover, &:focus {
background: transparent;
color: @blue;
}
+ &:active {
+ box-shadow: none;
+ outline: thin dotted #CCC;
+ }
+
&:disabled,
&:disabled:hover,
&:disabled:active,
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 5efbb96248a..9804977b607 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -607,6 +607,7 @@ issue.unassign.submit=Unassign
issue.assign.to_me=to me
issue.comment.formlink=Comment
issue.comment.submit=Comment
+issue.comment.tell_why=Please tell why?
issue.comment.delete_confirm_title=Delete Comment
issue.comment.delete_confirm_message=Do you want to delete this comment?
issue.comment.delete_confirm_button=Delete