aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/components
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/components')
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js8
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js19
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js20
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js10
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/Line.js19
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js17
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js3
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js19
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js4
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js9
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap10
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap16
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js2
-rw-r--r--server/sonar-web/src/main/js/components/__tests__/issue-test.js197
-rw-r--r--server/sonar-web/src/main/js/components/charts/bar-chart.js20
-rw-r--r--server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js2
-rw-r--r--server/sonar-web/src/main/js/components/common/EmptySearch.js39
-rw-r--r--server/sonar-web/src/main/js/components/common/MarkdownTips.js2
-rw-r--r--server/sonar-web/src/main/js/components/common/SelectList.js77
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/SelectList-test.js6
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap8
-rw-r--r--server/sonar-web/src/main/js/components/common/action-options-view.js1
-rw-r--r--server/sonar-web/src/main/js/components/common/modals.js1
-rw-r--r--server/sonar-web/src/main/js/components/common/popup.js1
-rw-r--r--server/sonar-web/src/main/js/components/controls/Checkbox.js6
-rw-r--r--server/sonar-web/src/main/js/components/controls/DateInput.js6
-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.js36
-rw-r--r--server/sonar-web/src/main/js/components/issue/Issue.js145
-rw-r--r--server/sonar-web/src/main/js/components/issue/IssueView.js48
-rw-r--r--server/sonar-web/src/main/js/components/issue/actions.js60
-rw-r--r--server/sonar-web/src/main/js/components/issue/collections/issues.js101
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js22
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js11
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueTags.js10
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js32
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueTransition.js12
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js69
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueChangelog-test.js7
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueCommentLine-test.js3
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueTitleBar-test.js2
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap55
-rw-r--r--server/sonar-web/src/main/js/components/issue/issue-view.js319
-rw-r--r--server/sonar-web/src/main/js/components/issue/models/issue.js281
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js137
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/ChangelogPopup-test.js2
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/DeleteComment.hbs6
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/comment-form.hbs18
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-assign-form-option.hbs5
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-assign-form.hbs10
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-changelog.hbs37
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-plan-form.hbs13
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-set-severity-form.hbs11
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-set-type-form.hbs11
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-tags-form-option.hbs17
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-tags-form.hbs10
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue-transitions-form.hbs12
-rw-r--r--server/sonar-web/src/main/js/components/issue/templates/issue.hbs182
-rw-r--r--server/sonar-web/src/main/js/components/issue/types.js11
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/assign-form-view.js172
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/comment-form-view.js113
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/set-severity-form-view.js51
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/set-type-form-view.js51
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/tags-form-view.js196
-rw-r--r--server/sonar-web/src/main/js/components/issue/views/transitions-form-view.js40
-rw-r--r--server/sonar-web/src/main/js/components/layout/Page.js (renamed from server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js)25
-rw-r--r--server/sonar-web/src/main/js/components/layout/PageFilters.js (renamed from server/sonar-web/src/main/js/components/shared/qualifier-icon.js)22
-rw-r--r--server/sonar-web/src/main/js/components/layout/PageMain.js (renamed from server/sonar-web/src/main/js/components/issue/views/DeleteCommentView.js)24
-rw-r--r--server/sonar-web/src/main/js/components/layout/PageMainInner.js (renamed from server/sonar-web/src/main/js/components/issue/models/changelog.js)22
-rw-r--r--server/sonar-web/src/main/js/components/layout/PageSide.js73
-rw-r--r--server/sonar-web/src/main/js/components/navigator/workspace-list-view.js1
-rw-r--r--server/sonar-web/src/main/js/components/shared/Organization.js5
-rw-r--r--server/sonar-web/src/main/js/components/shared/QualifierIcon.js (renamed from server/sonar-web/src/main/js/components/issue/views/changelog-view.js)33
-rw-r--r--server/sonar-web/src/main/js/components/shared/__tests__/QualifierIcon-test.js (renamed from server/sonar-web/src/main/js/components/issue/views/issue-popup.js)35
-rw-r--r--server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/QualifierIcon-test.js.snap16
75 files changed, 904 insertions, 2345 deletions
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js
index 0acd2dc123d..41517e76d50 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js
@@ -21,7 +21,6 @@
import { connect } from 'react-redux';
import SourceViewerBase from './SourceViewerBase';
import { receiveFavorites } from '../../store/favorites/duck';
-import { receiveIssues } from '../../store/issues/duck';
const mapStateToProps = null;
@@ -39,11 +38,6 @@ const onReceiveComponent = (component: { key: string, canMarkAsFavorite: boolean
}
};
-const onReceiveIssues = (issues: Array<*>) =>
- dispatch => {
- dispatch(receiveIssues(issues));
- };
-
-const mapDispatchToProps = { onReceiveComponent, onReceiveIssues };
+const mapDispatchToProps = { onReceiveComponent };
export default connect(mapStateToProps, mapDispatchToProps)(SourceViewerBase);
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
index c0cae1208f8..2e66388c3a1 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
@@ -70,10 +70,10 @@ type Props = {
loadIssues: (string, number, number) => Promise<*>,
loadSources: (string, number, number) => Promise<*>,
onLoaded?: (component: Object, sources: Array<*>, issues: Array<*>) => void,
+ onIssueChange?: (Issue) => void,
onIssueSelect?: (string) => void,
onIssueUnselect?: () => void,
onReceiveComponent: ({ canMarkAsFavorite: boolean, fav: boolean, key: string }) => void,
- onReceiveIssues: (issues: Array<*>) => void,
selectedIssue?: string
};
@@ -93,7 +93,7 @@ type State = {
highlightedLine: number | null,
highlightedSymbols: Array<string>,
issues?: Array<Issue>,
- issuesByLine: { [number]: Array<string> },
+ issuesByLine: { [number]: Array<Issue> },
issueLocationsByLine: { [number]: Array<LinearIssueLocation> },
issueSecondaryLocationsByIssueByLine: IndexedIssueLocationsByIssueAndLine,
issueSecondaryLocationMessagesByIssueByLine: IndexedIssueLocationMessagesByIssueAndLine,
@@ -221,10 +221,8 @@ export default class SourceViewerBase extends React.Component {
fetchComponent() {
this.setState({ loading: true });
-
const loadIssues = (component, sources) => {
this.props.loadIssues(this.props.component, 1, LINES).then(issues => {
- this.props.onReceiveIssues(issues);
if (this.mounted) {
const finalSources = sources.slice(0, LINES);
this.setState(
@@ -329,7 +327,6 @@ export default class SourceViewerBase extends React.Component {
const from = Math.max(1, firstSourceLine.line - LINES);
this.props.loadSources(this.props.component, from, firstSourceLine.line - 1).then(sources => {
this.props.loadIssues(this.props.component, from, firstSourceLine.line - 1).then(issues => {
- this.props.onReceiveIssues(issues);
if (this.mounted) {
this.setState(prevState => ({
issues: uniqBy([...issues, ...prevState.issues], issue => issue.key),
@@ -353,7 +350,6 @@ export default class SourceViewerBase extends React.Component {
const toLine = lastSourceLine.line + LINES + 1;
this.props.loadSources(this.props.component, fromLine, toLine).then(sources => {
this.props.loadIssues(this.props.component, fromLine, toLine).then(issues => {
- this.props.onReceiveIssues(issues);
if (this.mounted) {
this.setState(prevState => ({
issues: uniqBy([...prevState.issues, ...issues], issue => issue.key),
@@ -534,6 +530,16 @@ export default class SourceViewerBase extends React.Component {
}));
};
+ handleIssueChange = (issue: Issue) => {
+ this.setState(state => {
+ const issues = state.issues.map(candidate => candidate.key === issue.key ? issue : candidate);
+ return { issues, issuesByLine: issuesByLine(issues) };
+ });
+ if (this.props.onIssueChange) {
+ this.props.onIssueChange(issue);
+ }
+ };
+
renderCode(sources: Array<SourceLine>) {
const hasSourcesBefore = sources.length > 0 && sources[0].line > 1;
return (
@@ -561,6 +567,7 @@ export default class SourceViewerBase extends React.Component {
loadingSourcesBefore={this.state.loadingSourcesBefore}
onCoverageClick={this.handleCoverageClick}
onDuplicationClick={this.handleDuplicationClick}
+ onIssueChange={this.handleIssueChange}
onIssueSelect={this.handleIssueSelect}
onIssueUnselect={this.handleIssueUnselect}
onIssuesOpen={this.handleOpenIssues}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
index 8b9cfb46bd5..64aeedd5ba6 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js
@@ -40,7 +40,7 @@ const ZERO_LINE = {
};
export default class SourceViewerCode extends React.PureComponent {
- props: {
+ props: {|
displayAllIssues: boolean,
duplications?: Array<Duplication>,
duplicationsByLine: { [number]: Array<number> },
@@ -51,7 +51,7 @@ export default class SourceViewerCode extends React.PureComponent {
highlightedLine: number | null,
highlightedSymbols: Array<string>,
issues: Array<Issue>,
- issuesByLine: { [number]: Array<string> },
+ issuesByLine: { [number]: Array<Issue> },
issueLocationsByLine: { [number]: Array<LinearIssueLocation> },
issueSecondaryLocationsByIssueByLine: IndexedIssueLocationsByIssueAndLine,
issueSecondaryLocationMessagesByIssueByLine: IndexedIssueLocationMessagesByIssueAndLine,
@@ -62,6 +62,7 @@ export default class SourceViewerCode extends React.PureComponent {
loadingSourcesBefore: boolean,
onCoverageClick: (SourceLine, HTMLElement) => void,
onDuplicationClick: (number, number) => void,
+ onIssueChange: (Issue) => void,
onIssueSelect: (string) => void,
onIssueUnselect: () => void,
onIssuesOpen: (SourceLine) => void,
@@ -75,13 +76,13 @@ export default class SourceViewerCode extends React.PureComponent {
selectedIssueLocation: IndexedIssueLocation | null,
sources: Array<SourceLine>,
symbolsByLine: { [number]: Array<string> }
- };
+ |};
getDuplicationsForLine(line: SourceLine) {
return this.props.duplicationsByLine[line.line] || EMPTY_ARRAY;
}
- getIssuesForLine(line: SourceLine): Array<string> {
+ getIssuesForLine(line: SourceLine): Array<Issue> {
return this.props.issuesByLine[line.line] || EMPTY_ARRAY;
}
@@ -98,8 +99,11 @@ export default class SourceViewerCode extends React.PureComponent {
}
getSecondaryIssueLocationMessagesForLine(line: SourceLine, issueKey: string) {
- return this.props.issueSecondaryLocationMessagesByIssueByLine[issueKey][line.line] ||
- EMPTY_ARRAY;
+ const index = this.props.issueSecondaryLocationMessagesByIssueByLine;
+ if (index[issueKey] == null) {
+ return EMPTY_ARRAY;
+ }
+ return index[issueKey][line.line] || EMPTY_ARRAY;
}
renderLine = (
@@ -131,7 +135,8 @@ export default class SourceViewerCode extends React.PureComponent {
optimizedHighlightedSymbols = EMPTY_ARRAY;
}
- const optimizedSelectedIssue = selectedIssue != null && issuesForLine.includes(selectedIssue)
+ const optimizedSelectedIssue = selectedIssue != null &&
+ issuesForLine.find(issue => issue.key === selectedIssue)
? selectedIssue
: null;
@@ -165,6 +170,7 @@ export default class SourceViewerCode extends React.PureComponent {
onClick={this.props.onLineClick}
onCoverageClick={this.props.onCoverageClick}
onDuplicationClick={this.props.onDuplicationClick}
+ onIssueChange={this.props.onIssueChange}
onIssueSelect={this.props.onIssueSelect}
onIssueUnselect={this.props.onIssueUnselect}
onIssuesOpen={this.props.onIssuesOpen}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
index 4b65cd32ede..523ceeb192f 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
@@ -20,7 +20,7 @@
// @flow
import React from 'react';
import { Link } from 'react-router';
-import QualifierIcon from '../shared/qualifier-icon';
+import QualifierIcon from '../shared/QualifierIcon';
import FavoriteContainer from '../controls/FavoriteContainer';
import { getProjectUrl, getIssuesUrl } from '../../helpers/urls';
import { collapsedDirFromPath, fileFromPath } from '../../helpers/path';
@@ -44,7 +44,8 @@ export default class SourceViewerHeader extends React.PureComponent {
projectName: string,
q: string,
subProject?: string,
- subProjectName?: string
+ subProjectName?: string,
+ uuid: string
},
openNewWindow: () => void,
showMeasures: () => void
@@ -76,7 +77,8 @@ export default class SourceViewerHeader extends React.PureComponent {
projectName,
q,
subProject,
- subProjectName
+ subProjectName,
+ uuid
} = this.props.component;
const isUnitTest = q === 'UTS';
// TODO check if source viewer is displayed inside workspace
@@ -169,7 +171,7 @@ export default class SourceViewerHeader extends React.PureComponent {
<div className="source-viewer-header-measure">
<span className="source-viewer-header-measure-value">
<Link
- to={getIssuesUrl({ resolved: 'false', componentKeys: key })}
+ to={getIssuesUrl({ resolved: 'false', fileUuids: uuid })}
className="source-viewer-header-external-link"
target="_blank">
{measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js
index 74f3dabbfae..b1d051389a5 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js
@@ -26,7 +26,7 @@ import LineSCM from './LineSCM';
import LineCoverage from './LineCoverage';
import LineDuplications from './LineDuplications';
import LineDuplicationBlock from './LineDuplicationBlock';
-import LineIssuesIndicatorContainer from './LineIssuesIndicatorContainer';
+import LineIssuesIndicator from './LineIssuesIndicator';
import LineCode from './LineCode';
import { TooltipsContainer } from '../../mixins/tooltips-mixin';
import type { SourceLine } from '../types';
@@ -35,8 +35,9 @@ import type {
IndexedIssueLocation,
IndexedIssueLocationMessage
} from '../helpers/indexing';
+import type { Issue } from '../../issue/types';
-type Props = {
+type Props = {|
displayAllIssues: boolean,
displayCoverage: boolean,
displayDuplications: boolean,
@@ -48,12 +49,13 @@ type Props = {
highlighted: boolean,
highlightedSymbols: Array<string>,
issueLocations: Array<LinearIssueLocation>,
- issues: Array<string>,
+ issues: Array<Issue>,
line: SourceLine,
loadDuplications: (SourceLine, HTMLElement) => void,
onClick: (SourceLine, HTMLElement) => void,
onCoverageClick: (SourceLine, HTMLElement) => void,
onDuplicationClick: (number, number) => void,
+ onIssueChange: (Issue) => void,
onIssueSelect: (string) => void,
onIssueUnselect: () => void,
onIssuesOpen: (SourceLine) => void,
@@ -68,7 +70,7 @@ type Props = {
// $FlowFixMe
secondaryIssueLocationMessages: Array<IndexedIssueLocationMessage>,
selectedIssueLocation: IndexedIssueLocation | null
-};
+|};
export default class Line extends React.PureComponent {
props: Props;
@@ -82,7 +84,7 @@ export default class Line extends React.PureComponent {
const { issues } = this.props;
if (issues.length > 0) {
- this.props.onIssueSelect(issues[0]);
+ this.props.onIssueSelect(issues[0].key);
}
}
};
@@ -124,8 +126,8 @@ export default class Line extends React.PureComponent {
{this.props.displayIssues &&
!this.props.displayAllIssues &&
- <LineIssuesIndicatorContainer
- issueKeys={this.props.issues}
+ <LineIssuesIndicator
+ issues={this.props.issues}
line={line}
onClick={this.handleIssuesIndicatorClick}
/>}
@@ -137,9 +139,10 @@ export default class Line extends React.PureComponent {
<LineCode
highlightedSymbols={this.props.highlightedSymbols}
- issueKeys={this.props.issues}
+ issues={this.props.issues}
issueLocations={this.props.issueLocations}
line={line}
+ onIssueChange={this.props.onIssueChange}
onIssueSelect={this.props.onIssueSelect}
onLocationSelect={this.props.onLocationSelect}
onSymbolClick={this.props.onSymbolClick}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js
index b5813f76365..6b18e06791b 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js
@@ -34,12 +34,14 @@ import type {
IndexedIssueLocation,
IndexedIssueLocationMessage
} from '../helpers/indexing';
+import type { Issue } from '../../issue/types';
-type Props = {
+type Props = {|
highlightedSymbols: Array<string>,
- issueKeys: Array<string>,
+ issues: Array<Issue>,
issueLocations: Array<LinearIssueLocation>,
line: SourceLine,
+ onIssueChange: (Issue) => void,
onIssueSelect: (issueKey: string) => void,
onLocationSelect: (flowIndex: number, locationIndex: number) => void,
onSymbolClick: (Array<string>) => void,
@@ -49,7 +51,7 @@ type Props = {
selectedIssue: string | null,
selectedIssueLocation: IndexedIssueLocation | null,
showIssues: boolean
-};
+|};
type State = {
tokens: Tokens
@@ -166,7 +168,7 @@ export default class LineCode extends React.PureComponent {
render() {
const {
highlightedSymbols,
- issueKeys,
+ issues,
issueLocations,
line,
onIssueSelect,
@@ -201,7 +203,7 @@ export default class LineCode extends React.PureComponent {
const finalCode = generateHTML(tokens);
const className = classNames('source-line-code', 'code', {
- 'has-issues': issueKeys.length > 0
+ 'has-issues': issues.length > 0
});
return (
@@ -213,9 +215,10 @@ export default class LineCode extends React.PureComponent {
this.renderSecondaryIssueLocationMessages(secondaryIssueLocationMessages)}
</div>
{showIssues &&
- issueKeys.length > 0 &&
+ issues.length > 0 &&
<LineIssuesList
- issueKeys={issueKeys}
+ issues={issues}
+ onIssueChange={this.props.onIssueChange}
onIssueClick={onIssueSelect}
selectedIssue={selectedIssue}
/>}
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 daf1785ffd2..b7f1c2a176e 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
@@ -23,9 +23,10 @@ import classNames from 'classnames';
import SeverityIcon from '../../shared/SeverityIcon';
import { sortBySeverity } from '../../../helpers/issues';
import type { SourceLine } from '../types';
+import type { Issue } from '../../issue/types';
type Props = {
- issues: Array<{ severity: string }>,
+ issues: Array<Issue>,
line: SourceLine,
onClick: () => void
};
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js
index ca89ab51fae..bff245af97c 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js
@@ -19,10 +19,12 @@
*/
// @flow
import React from 'react';
-import ConnectedIssue from '../../issue/ConnectedIssue';
+import Issue from '../../issue/Issue';
+import type { Issue as IssueType } from '../../issue/types';
type Props = {
- issueKeys: Array<string>,
+ issues: Array<IssueType>,
+ onIssueChange: (IssueType) => void,
onIssueClick: (issueKey: string) => void,
selectedIssue: string | null
};
@@ -31,16 +33,17 @@ export default class LineIssuesList extends React.PureComponent {
props: Props;
render() {
- const { issueKeys, onIssueClick, selectedIssue } = this.props;
+ const { issues, onIssueClick, selectedIssue } = this.props;
return (
<div className="issue-list">
- {issueKeys.map(issueKey => (
- <ConnectedIssue
- issueKey={issueKey}
- key={issueKey}
+ {issues.map(issue => (
+ <Issue
+ issue={issue}
+ key={issue.key}
+ onChange={this.props.onIssueChange}
onClick={onIssueClick}
- selected={selectedIssue === issueKey}
+ selected={selectedIssue === issue.key}
/>
))}
</div>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js
index 3cc6793b214..5cd841a2a5a 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js
@@ -34,7 +34,7 @@ it('render code', () => {
const wrapper = shallow(
<LineCode
highlightedSymbols={['sym1']}
- issueKeys={['issue-1', 'issue-2']}
+ issues={[{ key: 'issue-1' }, { key: 'issue-2' }]}
issueLocations={issueLocations}
line={line}
onIssueSelect={jest.fn()}
@@ -62,7 +62,7 @@ it('should handle empty location message', () => {
const wrapper = shallow(
<LineCode
highlightedSymbols={['sym1']}
- issueKeys={['issue-1', 'issue-2']}
+ issues={[{ key: 'issue-1' }, { key: 'issue-2' }]}
issueLocations={issueLocations}
line={line}
onIssueSelect={jest.fn()}
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js
index ede9d50241c..a9a0e0763b9 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js
@@ -23,15 +23,10 @@ import LineIssuesList from '../LineIssuesList';
it('render issues list', () => {
const line = { line: 3 };
- const issueKeys = ['foo', 'bar'];
+ const issues = [{ key: 'foo' }, { key: 'bar' }];
const onIssueClick = jest.fn();
const wrapper = shallow(
- <LineIssuesList
- issueKeys={issueKeys}
- line={line}
- onIssueClick={onIssueClick}
- selectedIssue="foo"
- />
+ <LineIssuesList issues={issues} line={line} onIssueClick={onIssueClick} selectedIssue="foo" />
);
expect(wrapper).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap
index 3e4499bb1bf..ca94ecedbf7 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap
@@ -22,10 +22,14 @@ exports[`test render code 1`] = `
</div>
</div>
<LineIssuesList
- issueKeys={
+ issues={
Array [
- "issue-1",
- "issue-2",
+ Object {
+ "key": "issue-1",
+ },
+ Object {
+ "key": "issue-2",
+ },
]
}
onIssueClick={[Function]}
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 30bbfaa7779..090d0852df8 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,12 +1,20 @@
exports[`test render issues list 1`] = `
<div
className="issue-list">
- <Connect(BaseIssue)
- issueKey="foo"
+ <BaseIssue
+ issue={
+ Object {
+ "key": "foo",
+ }
+ }
onClick={[Function]}
selected={true} />
- <Connect(BaseIssue)
- issueKey="bar"
+ <BaseIssue
+ issue={
+ Object {
+ "key": "bar",
+ }
+ }
onClick={[Function]}
selected={false} />
</div>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js
index 36bf7e73b3a..d39351c52dc 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js
@@ -64,7 +64,7 @@ export const issuesByLine = (issues: Array<Issue>) => {
if (!(line in index)) {
index[line] = [];
}
- index[line].push(issue.key);
+ index[line].push(issue);
});
return index;
};
diff --git a/server/sonar-web/src/main/js/components/__tests__/issue-test.js b/server/sonar-web/src/main/js/components/__tests__/issue-test.js
deleted file mode 100644
index b0483b8cc21..00000000000
--- a/server/sonar-web/src/main/js/components/__tests__/issue-test.js
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * 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 Issue from '../issue/models/issue';
-
-describe('Model', () => {
- it('should have correct urlRoot', () => {
- const issue = new Issue();
- expect(issue.urlRoot()).toBe('/api/issues');
- });
-
- it('should parse response without root issue object', () => {
- const issue = new Issue();
- const example = { a: 1 };
- expect(issue.parse(example)).toEqual(example);
- });
-
- it('should parse response with the root issue object', () => {
- const issue = new Issue();
- const example = { a: 1 };
- expect(issue.parse({ issue: example })).toEqual(example);
- });
-
- it('should reset attributes (no attributes initially)', () => {
- const issue = new Issue();
- const example = { a: 1 };
- issue.reset(example);
- expect(issue.toJSON()).toEqual(example);
- });
-
- it('should reset attributes (override attribute)', () => {
- const issue = new Issue({ a: 2 });
- const example = { a: 1 };
- issue.reset(example);
- expect(issue.toJSON()).toEqual(example);
- });
-
- it('should reset attributes (different attributes)', () => {
- const issue = new Issue({ a: 2 });
- const example = { b: 1 };
- issue.reset(example);
- expect(issue.toJSON()).toEqual(example);
- });
-
- it('should unset `textRange` of a closed issue', () => {
- const issue = new Issue();
- const result = issue.parse({ issue: { status: 'CLOSED', textRange: { startLine: 5 } } });
- expect(result.textRange).toBeFalsy();
- });
-
- it('should unset `flows` of a closed issue', () => {
- const issue = new Issue();
- const result = issue.parse({ issue: { status: 'CLOSED', flows: [1, 2, 3] } });
- expect(result.flows).toEqual([]);
- });
-
- describe('Actions', () => {
- it('should assign', () => {
- const issue = new Issue({ key: 'issue-key' });
- const spy = jest.fn();
- issue._action = spy;
- issue.assign('admin');
- expect(spy).toBeCalledWith({
- data: { assignee: 'admin', issue: 'issue-key' },
- url: '/api/issues/assign'
- });
- });
-
- it('should unassign', () => {
- const issue = new Issue({ key: 'issue-key' });
- const spy = jest.fn();
- issue._action = spy;
- issue.assign();
- expect(spy).toBeCalledWith({
- data: { assignee: undefined, issue: 'issue-key' },
- url: '/api/issues/assign'
- });
- });
-
- it('should plan', () => {
- const issue = new Issue({ key: 'issue-key' });
- const spy = jest.fn();
- issue._action = spy;
- issue.plan('plan');
- expect(spy).toBeCalledWith({
- data: { plan: 'plan', issue: 'issue-key' },
- url: '/api/issues/plan'
- });
- });
-
- it('should unplan', () => {
- const issue = new Issue({ key: 'issue-key' });
- const spy = jest.fn();
- issue._action = spy;
- issue.plan();
- expect(spy).toBeCalledWith({
- data: { plan: undefined, issue: 'issue-key' },
- url: '/api/issues/plan'
- });
- });
-
- it('should set severity', () => {
- const issue = new Issue({ key: 'issue-key' });
- const spy = jest.fn();
- issue._action = spy;
- issue.setSeverity('BLOCKER');
- expect(spy).toBeCalledWith({
- data: { severity: 'BLOCKER', issue: 'issue-key' },
- url: '/api/issues/set_severity'
- });
- });
- });
-
- describe('#getLinearLocations', () => {
- it('should return single line location', () => {
- const issue = new Issue({
- textRange: { startLine: 1, endLine: 1, startOffset: 0, endOffset: 10 }
- });
- const locations = issue.getLinearLocations();
- expect(locations.length).toBe(1);
-
- expect(locations[0].line).toBe(1);
- expect(locations[0].from).toBe(0);
- expect(locations[0].to).toBe(10);
- });
-
- it('should return location not from 0', () => {
- const issue = new Issue({
- textRange: { startLine: 1, endLine: 1, startOffset: 5, endOffset: 10 }
- });
- const locations = issue.getLinearLocations();
- expect(locations.length).toBe(1);
-
- expect(locations[0].line).toBe(1);
- expect(locations[0].from).toBe(5);
- expect(locations[0].to).toBe(10);
- });
-
- it('should return 2-lines location', () => {
- const issue = new Issue({
- textRange: { startLine: 2, endLine: 3, startOffset: 5, endOffset: 10 }
- });
- const locations = issue.getLinearLocations();
- expect(locations.length).toBe(2);
-
- expect(locations[0].line).toBe(2);
- expect(locations[0].from).toBe(5);
- expect(locations[0].to).toBe(999999);
-
- expect(locations[1].line).toBe(3);
- expect(locations[1].from).toBe(0);
- expect(locations[1].to).toBe(10);
- });
-
- it('should return 3-lines location', () => {
- const issue = new Issue({
- textRange: { startLine: 4, endLine: 6, startOffset: 5, endOffset: 10 }
- });
- const locations = issue.getLinearLocations();
- expect(locations.length).toBe(3);
-
- expect(locations[0].line).toBe(4);
- expect(locations[0].from).toBe(5);
- expect(locations[0].to).toBe(999999);
-
- expect(locations[1].line).toBe(5);
- expect(locations[1].from).toBe(0);
- expect(locations[1].to).toBe(999999);
-
- expect(locations[2].line).toBe(6);
- expect(locations[2].from).toBe(0);
- expect(locations[2].to).toBe(10);
- });
-
- it('should return [] when no location', () => {
- const issue = new Issue();
- const locations = issue.getLinearLocations();
- expect(locations.length).toBe(0);
- });
- });
-});
diff --git a/server/sonar-web/src/main/js/components/charts/bar-chart.js b/server/sonar-web/src/main/js/components/charts/bar-chart.js
index dc4f0082f27..4e1d336dc0d 100644
--- a/server/sonar-web/src/main/js/components/charts/bar-chart.js
+++ b/server/sonar-web/src/main/js/components/charts/bar-chart.js
@@ -21,7 +21,7 @@ import React from 'react';
import { max } from 'd3-array';
import { scaleLinear, scaleBand } from 'd3-scale';
import { ResizeMixin } from './../mixins/resize-mixin';
-import { TooltipsMixin } from './../mixins/tooltips-mixin';
+import { TooltipsContainer } from './../mixins/tooltips-mixin';
export const BarChart = React.createClass({
propTypes: {
@@ -34,7 +34,7 @@ export const BarChart = React.createClass({
onBarClick: React.PropTypes.func
},
- mixins: [ResizeMixin, TooltipsMixin],
+ mixins: [ResizeMixin],
getDefaultProps() {
return {
@@ -162,13 +162,15 @@ export const BarChart = React.createClass({
const yScale = scaleLinear().domain([0, maxY]).range([availableHeight, 0]);
return (
- <svg className="bar-chart" width={this.state.width} height={this.state.height}>
- <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
- {this.renderXTicks(xScale, yScale)}
- {this.renderXValues(xScale, yScale)}
- {this.renderBars(xScale, yScale)}
- </g>
- </svg>
+ <TooltipsContainer>
+ <svg className="bar-chart" width={this.state.width} height={this.state.height}>
+ <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
+ {this.renderXTicks(xScale, yScale)}
+ {this.renderXValues(xScale, yScale)}
+ {this.renderBars(xScale, yScale)}
+ </g>
+ </svg>
+ </TooltipsContainer>
);
}
});
diff --git a/server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js b/server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js
index d91207af777..7b06317b39e 100644
--- a/server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js
+++ b/server/sonar-web/src/main/js/components/charts/treemap-breadcrumbs.js
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import QualifierIcon from '../shared/qualifier-icon';
+import QualifierIcon from '../shared/QualifierIcon';
export const TreemapBreadcrumbs = React.createClass({
propTypes: {
diff --git a/server/sonar-web/src/main/js/components/common/EmptySearch.js b/server/sonar-web/src/main/js/components/common/EmptySearch.js
new file mode 100644
index 00000000000..904a6b2cbad
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/EmptySearch.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 { css } from 'glamor';
+import { translate } from '../../helpers/l10n';
+
+const EmptySearch = () => (
+ <div
+ className={css({
+ padding: '60px 0',
+ border: '1px solid #e6e6e6',
+ borderRadius: 2,
+ textAlign: 'center',
+ color: '#777'
+ })}>
+ <h3>{translate('no_results_search')}</h3>
+ <p className="big-spacer-top">{translate('no_results_search.2')}</p>
+ </div>
+);
+
+export default EmptySearch;
diff --git a/server/sonar-web/src/main/js/components/common/MarkdownTips.js b/server/sonar-web/src/main/js/components/common/MarkdownTips.js
index 2d83b6aeb24..8c5db3a8fe1 100644
--- a/server/sonar-web/src/main/js/components/common/MarkdownTips.js
+++ b/server/sonar-web/src/main/js/components/common/MarkdownTips.js
@@ -25,7 +25,7 @@ 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');
+ window.open(getMarkdownHelpUrl(), 'Markdown', 'height=300,width=600,scrollbars=1,resizable=1');
}
render() {
diff --git a/server/sonar-web/src/main/js/components/common/SelectList.js b/server/sonar-web/src/main/js/components/common/SelectList.js
index ba2f82b34b7..bec5c2e6712 100644
--- a/server/sonar-web/src/main/js/components/common/SelectList.js
+++ b/server/sonar-web/src/main/js/components/common/SelectList.js
@@ -19,6 +19,8 @@
*/
// @flow
import React from 'react';
+import key from 'keymaster';
+import { uniqueId } from 'lodash';
import SelectListItem from './SelectListItem';
type Props = {
@@ -33,7 +35,8 @@ type State = {
};
export default class SelectList extends React.PureComponent {
- list: HTMLElement;
+ currentKeyScope: string;
+ previousKeyScope: string;
props: Props;
state: State;
@@ -45,7 +48,7 @@ export default class SelectList extends React.PureComponent {
}
componentDidMount() {
- this.list.focus();
+ this.attachShortcuts();
}
componentWillReceiveProps(nextProps: Props) {
@@ -57,24 +60,36 @@ export default class SelectList extends React.PureComponent {
}
}
- 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();
+ componentWillUnmount() {
+ this.detachShortcuts();
+ }
+
+ attachShortcuts = () => {
+ this.previousKeyScope = key.getScope();
+ this.currentKeyScope = uniqueId('key-scope');
+ key.setScope(this.currentKeyScope);
+
+ key('down', this.currentKeyScope, () => {
+ this.setState(this.selectNextElement);
+ return false;
+ });
+
+ key('up', this.currentKeyScope, () => {
+ this.setState(this.selectPreviousElement);
+ return false;
+ });
+
+ key('return', this.currentKeyScope, () => {
+ if (this.state.active) {
+ this.handleSelect(this.state.active);
+ }
+ return false;
+ });
+ };
+
+ detachShortcuts = () => {
+ key.setScope(this.previousKeyScope);
+ key.deleteScope(this.currentKeyScope);
};
handleSelect = (item: string) => {
@@ -105,18 +120,18 @@ export default class SelectList extends React.PureComponent {
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}>
+ <ul className="menu">
{hasChildren &&
- React.Children.map(children, child =>
- React.cloneElement(child, {
- active: this.state.active,
- onHover: this.handleHover,
- onSelect: this.handleSelect
- }))}
+ React.Children.map(
+ children,
+ child =>
+ child != null &&
+ React.cloneElement(child, {
+ active: this.state.active,
+ onHover: this.handleHover,
+ onSelect: this.handleSelect
+ })
+ )}
{!hasChildren &&
this.props.items.map(item => (
<SelectListItem
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
index 9c0e88e6aa3..58afbecad26 100644
--- 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
@@ -64,11 +64,11 @@ it('should correclty handle user actions', () => {
))}
</SelectList>
);
- keydown(list.find('ul'), 40);
+ keydown(40);
expect(list.state()).toMatchSnapshot();
- keydown(list.find('ul'), 40);
+ keydown(40);
expect(list.state()).toMatchSnapshot();
- keydown(list.find('ul'), 38);
+ keydown(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__/__snapshots__/SelectList-test.js.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/SelectList-test.js.snap
index 4cf15f469cb..b2d9388c7ef 100644
--- 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
@@ -26,9 +26,7 @@ Array [
exports[`test should render correctly with children 1`] = `
<ul
- className="menu"
- onKeyDown={[Function]}
- tabIndex={0}>
+ className="menu">
<SelectListItem
active="seconditem"
item="item"
@@ -61,9 +59,7 @@ exports[`test should render correctly with children 1`] = `
exports[`test should render correctly without children 1`] = `
<ul
- className="menu"
- onKeyDown={[Function]}
- tabIndex={0}>
+ className="menu">
<SelectListItem
active="seconditem"
item="item"
diff --git a/server/sonar-web/src/main/js/components/common/action-options-view.js b/server/sonar-web/src/main/js/components/common/action-options-view.js
index 976f88eb081..a538e22b88e 100644
--- a/server/sonar-web/src/main/js/components/common/action-options-view.js
+++ b/server/sonar-web/src/main/js/components/common/action-options-view.js
@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import $ from 'jquery';
+import key from 'keymaster';
import PopupView from './popup';
export default PopupView.extend({
diff --git a/server/sonar-web/src/main/js/components/common/modals.js b/server/sonar-web/src/main/js/components/common/modals.js
index 5a1343bd511..a86a262d40c 100644
--- a/server/sonar-web/src/main/js/components/common/modals.js
+++ b/server/sonar-web/src/main/js/components/common/modals.js
@@ -19,6 +19,7 @@
*/
import $ from 'jquery';
import Marionette from 'backbone.marionette';
+import key from 'keymaster';
const EVENT_SCOPE = 'modal';
diff --git a/server/sonar-web/src/main/js/components/common/popup.js b/server/sonar-web/src/main/js/components/common/popup.js
index 532380f71b4..bce61b72938 100644
--- a/server/sonar-web/src/main/js/components/common/popup.js
+++ b/server/sonar-web/src/main/js/components/common/popup.js
@@ -19,6 +19,7 @@
*/
import $ from 'jquery';
import Marionette from 'backbone.marionette';
+import key from 'keymaster';
export default Marionette.ItemView.extend({
className: 'bubble-popup',
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 f5e7289dd45..c81d49b8d8c 100644
--- a/server/sonar-web/src/main/js/components/controls/Checkbox.js
+++ b/server/sonar-web/src/main/js/components/controls/Checkbox.js
@@ -26,7 +26,7 @@ export default class Checkbox extends React.Component {
onCheck: React.PropTypes.func.isRequired,
checked: React.PropTypes.bool.isRequired,
thirdState: React.PropTypes.bool,
- className: React.PropTypes.string
+ className: React.PropTypes.any
};
static defaultProps = {
@@ -44,7 +44,9 @@ export default class Checkbox extends React.Component {
}
render() {
- const className = classNames(this.props.className, 'icon-checkbox', {
+ const className = classNames('icon-checkbox', {
+ // trick to work with glamor
+ [this.props.className]: true,
'icon-checkbox-checked': this.props.checked,
'icon-checkbox-single': this.props.thirdState
});
diff --git a/server/sonar-web/src/main/js/components/controls/DateInput.js b/server/sonar-web/src/main/js/components/controls/DateInput.js
index 52b47cbe189..f4070c12119 100644
--- a/server/sonar-web/src/main/js/components/controls/DateInput.js
+++ b/server/sonar-web/src/main/js/components/controls/DateInput.js
@@ -19,11 +19,13 @@
*/
import $ from 'jquery';
import React from 'react';
+import classNames from 'classnames';
import { pick } from 'lodash';
import './styles.css';
export default class DateInput extends React.Component {
static propTypes = {
+ className: React.PropTypes.string,
value: React.PropTypes.string,
format: React.PropTypes.string,
name: React.PropTypes.string,
@@ -67,12 +69,12 @@ export default class DateInput extends React.Component {
/* eslint max-len: 0 */
return (
- <span className="date-input-control">
+ <span className={classNames('date-input-control', this.props.className)}>
<input
className="date-input-control-input"
ref="input"
type="text"
- initialValue={this.props.value}
+ defaultValue={this.props.value}
readOnly={true}
{...inputProps}
/>
diff --git a/server/sonar-web/src/main/js/components/issue/BaseIssue.js b/server/sonar-web/src/main/js/components/issue/BaseIssue.js
deleted file mode 100644
index d4ada02869b..00000000000
--- a/server/sonar-web/src/main/js/components/issue/BaseIssue.js
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 67d71fe37cc..00000000000
--- a/server/sonar-web/src/main/js/components/issue/ConnectedIssue.js
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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 { connect } from 'react-redux';
-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)
-});
-
-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 a121bf738d0..471a62e04f0 100644
--- a/server/sonar-web/src/main/js/components/issue/Issue.js
+++ b/server/sonar-web/src/main/js/components/issue/Issue.js
@@ -18,14 +18,145 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
-import { connect } from 'react-redux';
-import BaseIssue from './BaseIssue';
-import { onFail } from '../../store/rootActions';
+import React from 'react';
+import IssueView from './IssueView';
import { updateIssue } from './actions';
+import { setIssueAssignee } from '../../api/issues';
+import { onFail } from '../../store/rootActions';
+import type { Issue } from './types';
+
+type Props = {|
+ checked?: boolean,
+ issue: Issue,
+ onChange: (Issue) => void,
+ onCheck?: (string) => void,
+ onClick: (string) => void,
+ onFilter?: (property: string, issue: Issue) => void,
+ selected: boolean
+|};
-const mapDispatchToProps = {
- onIssueChange: updateIssue,
- onFail: error => dispatch => onFail(dispatch)(error)
+type State = {
+ currentPopup: string
};
-export default connect(null, mapDispatchToProps)(BaseIssue);
+export default class BaseIssue extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State;
+
+ static contextTypes = {
+ store: React.PropTypes.object
+ };
+
+ 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) {
+ updateIssue(this.props.onChange, setIssueAssignee({ issue: issue.key, assignee: login }));
+ }
+ this.togglePopup('assign', false);
+ };
+
+ handleFail = (error: Error) => {
+ onFail(this.context.store.dispatch)(error);
+ };
+
+ 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.handleFail}
+ onFilter={this.props.onFilter}
+ onChange={this.props.onChange}
+ togglePopup={this.togglePopup}
+ currentPopup={this.state.currentPopup}
+ selected={this.props.selected}
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/IssueView.js b/server/sonar-web/src/main/js/components/issue/IssueView.js
index 52ee7e95280..3d959f26573 100644
--- a/server/sonar-web/src/main/js/components/issue/IssueView.js
+++ b/server/sonar-web/src/main/js/components/issue/IssueView.js
@@ -20,43 +20,51 @@
// @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 { updateIssue } from './actions';
import { deleteIssueComment, editIssueComment } from '../../api/issues';
import type { Issue } from './types';
-type Props = {
+type Props = {|
checked?: boolean,
currentPopup: string,
issue: Issue,
onAssign: (string) => void,
- onCheck?: () => void,
+ onChange: (Issue) => void,
+ onCheck?: (string) => void,
onClick: (string) => void,
onFail: (Error) => void,
- onFilterClick?: () => void,
- onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void,
+ onFilter?: (property: string, issue: Issue) => void,
selected: boolean,
togglePopup: (string) => void
-};
+|};
export default class IssueView extends React.PureComponent {
props: Props;
- handleClick = (evt: MouseEvent) => {
- evt.preventDefault();
+ handleCheck = (event: Event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ if (this.props.onCheck) {
+ this.props.onCheck(this.props.issue.key);
+ }
+ };
+
+ handleClick = (event: Event & { target: HTMLElement }) => {
+ event.preventDefault();
if (this.props.onClick) {
this.props.onClick(this.props.issue.key);
}
};
editComment = (comment: string, text: string) => {
- this.props.onIssueChange(editIssueComment({ comment, text }));
+ updateIssue(this.props.onChange, editIssueComment({ comment, text }));
};
deleteComment = (comment: string) => {
- this.props.onIssueChange(deleteIssueComment({ comment }));
+ updateIssue(this.props.onChange, deleteIssueComment({ comment }));
};
render() {
@@ -74,13 +82,13 @@ export default class IssueView extends React.PureComponent {
className={issueClass}
data-issue={issue.key}
onClick={this.handleClick}
- tabIndex={0}
- role="listitem">
+ role="listitem"
+ tabIndex={0}>
<IssueTitleBar
issue={issue}
currentPopup={this.props.currentPopup}
onFail={this.props.onFail}
- onFilterClick={this.props.onFilterClick}
+ onFilter={this.props.onFilter}
togglePopup={this.props.togglePopup}
/>
<IssueActionsBar
@@ -89,7 +97,7 @@ export default class IssueView extends React.PureComponent {
onAssign={this.props.onAssign}
onFail={this.props.onFail}
togglePopup={this.props.togglePopup}
- onIssueChange={this.props.onIssueChange}
+ onChange={this.props.onChange}
/>
{issue.comments &&
issue.comments.length > 0 &&
@@ -108,13 +116,13 @@ export default class IssueView extends React.PureComponent {
<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}
+ <a className="js-toggle issue-checkbox-container" href="#" onClick={this.handleCheck}>
+ <i
+ className={classNames('issue-checkbox', 'icon-checkbox', {
+ 'icon-checkbox-checked': this.props.checked
+ })}
/>
- </div>}
+ </a>}
</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
index a0631c17001..a44430520bd 100644
--- a/server/sonar-web/src/main/js/components/issue/actions.js
+++ b/server/sonar-web/src/main/js/components/issue/actions.js
@@ -18,35 +18,41 @@
* 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';
+import type { Issue } from './types';
-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
- )
- ])
+export const updateIssue = (
+ onChange: (Issue) => void,
+ resultPromise: Promise<*>,
+ oldIssue?: Issue,
+ newIssue?: Issue
+) => {
+ const optimisticUpdate = oldIssue != null && newIssue != null;
+
+ if (optimisticUpdate) {
+ // $FlowFixMe `newIssue` is not null, because `optimisticUpdate` is true
+ onChange(newIssue);
+ }
+
+ resultPromise.then(
+ response => {
+ if (!optimisticUpdate) {
+ const issue = parseIssueFromResponse(
+ response.issue,
+ response.components,
+ response.users,
+ response.rules
);
- },
- error => {
- onFail(dispatch)(error);
- if (oldIssue && newIssue) {
- dispatch(receiveIssues([oldIssue]));
- }
+ onChange(issue);
+ }
+ },
+ error => {
+ onFail(error);
+ if (optimisticUpdate) {
+ // $FlowFixMe `oldIssue` is not null, because `optimisticUpdate` is true
+ onChange(oldIssue);
}
- );
- };
+ }
+ );
+};
diff --git a/server/sonar-web/src/main/js/components/issue/collections/issues.js b/server/sonar-web/src/main/js/components/issue/collections/issues.js
deleted file mode 100644
index 69ac37b1beb..00000000000
--- a/server/sonar-web/src/main/js/components/issue/collections/issues.js
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * 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 Backbone from 'backbone';
-import Issue from '../models/issue';
-
-export default Backbone.Collection.extend({
- model: Issue,
-
- url() {
- return window.baseUrl + '/api/issues/search';
- },
-
- _injectRelational(issue, source, baseField, lookupField) {
- const baseValue = issue[baseField];
- if (baseValue != null && Array.isArray(source) && source.length > 0) {
- const lookupValue = source.find(candidate => candidate[lookupField] === baseValue);
- if (lookupValue != null) {
- Object.keys(lookupValue).forEach(key => {
- const newKey = baseField + key.charAt(0).toUpperCase() + key.slice(1);
- issue[newKey] = lookupValue[key];
- });
- }
- }
- return issue;
- },
-
- _injectCommentsRelational(issue, users) {
- if (issue.comments) {
- const that = this;
- const newComments = issue.comments.map(comment => {
- let newComment = { ...comment, author: comment.login };
- delete newComment.login;
- newComment = that._injectRelational(newComment, users, 'author', 'login');
- return newComment;
- });
- issue = { ...issue, comments: newComments };
- }
- return issue;
- },
-
- _prepareClosed(issue) {
- if (issue.status === 'CLOSED') {
- issue.flows = [];
- delete issue.textRange;
- }
- return issue;
- },
-
- ensureTextRange(issue) {
- if (issue.line && !issue.textRange) {
- // FIXME 999999
- issue.textRange = {
- startLine: issue.line,
- endLine: issue.line,
- startOffset: 0,
- endOffset: 999999
- };
- }
- return issue;
- },
-
- parse(r) {
- const that = this;
-
- this.paging = {
- p: r.p,
- ps: r.ps,
- total: r.total,
- maxResultsReached: r.p * r.ps >= r.total
- };
-
- return r.issues.map(issue => {
- issue = that._injectRelational(issue, r.components, 'component', 'key');
- issue = that._injectRelational(issue, r.components, 'project', 'key');
- issue = that._injectRelational(issue, r.components, 'subProject', 'key');
- issue = that._injectRelational(issue, r.rules, 'rule', 'key');
- issue = that._injectRelational(issue, r.users, 'assignee', 'login');
- issue = that._injectCommentsRelational(issue, r.users);
- issue = that._prepareClosed(issue);
- issue = that.ensureTextRange(issue);
- return issue;
- });
- }
-});
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
index e60bc87c991..9006cd9a19e 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.js
@@ -25,6 +25,7 @@ import IssueSeverity from './IssueSeverity';
import IssueTags from './IssueTags';
import IssueTransition from './IssueTransition';
import IssueType from './IssueType';
+import { updateIssue } from '../actions';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import type { Issue } from '../types';
@@ -32,8 +33,8 @@ type Props = {
issue: Issue,
currentPopup: string,
onAssign: (string) => void,
+ onChange: (Issue) => void,
onFail: (Error) => void,
- onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void,
togglePopup: (string) => void
};
@@ -63,15 +64,18 @@ export default class IssueActionsBar extends React.PureComponent {
const { issue } = this.props;
if (issue[property] !== value) {
const newIssue = { ...issue, [property]: value };
- this.props.onIssueChange(apiCall({ issue: issue.key, [property]: value }), issue, newIssue);
+ updateIssue(
+ this.props.onChange,
+ apiCall({ issue: issue.key, [property]: value }),
+ issue,
+ newIssue
+ );
}
this.props.togglePopup(popup, false);
};
toggleComment = (open?: boolean, placeholder?: string) => {
- this.setState({
- commentPlaceholder: placeholder || ''
- });
+ this.setState({ commentPlaceholder: placeholder || '' });
this.props.togglePopup('comment', open);
};
@@ -112,8 +116,8 @@ export default class IssueActionsBar extends React.PureComponent {
isOpen={this.props.currentPopup === 'transition' && hasTransitions}
issue={issue}
hasTransitions={hasTransitions}
+ onChange={this.props.onChange}
togglePopup={this.props.togglePopup}
- setIssueProperty={this.setIssueProperty}
/>
</li>
<li className="issue-meta">
@@ -134,10 +138,10 @@ export default class IssueActionsBar extends React.PureComponent {
</li>}
{canComment &&
<IssueCommentAction
- issueKey={issue.key}
commentPlaceholder={this.state.commentPlaceholder}
currentPopup={this.props.currentPopup}
- onIssueChange={this.props.onIssueChange}
+ issueKey={issue.key}
+ onChange={this.props.onChange}
toggleComment={this.toggleComment}
/>}
</ul>
@@ -149,8 +153,8 @@ export default class IssueActionsBar extends React.PureComponent {
isOpen={this.props.currentPopup === 'edit-tags' && canSetTags}
canSetTags={canSetTags}
issue={issue}
+ onChange={this.props.onChange}
onFail={this.props.onFail}
- onIssueChange={this.props.onIssueChange}
togglePopup={this.props.togglePopup}
/>
</li>
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
index f5f6bf5b8d3..8111815e942 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentAction.js
@@ -19,25 +19,26 @@
*/
// @flow
import React from 'react';
+import { updateIssue } from '../actions';
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,
+type Props = {|
commentPlaceholder: string,
currentPopup: string,
- onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void,
+ issueKey: string,
+ onChange: (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 }));
+ updateIssue(this.props.onChange, addIssueComment({ issue: this.props.issueKey, text }));
this.props.toggleComment(false);
};
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
index ab850061b7d..c7cebdf74d7 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueTags.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js
@@ -19,6 +19,7 @@
*/
// @flow
import React from 'react';
+import { updateIssue } from '../actions';
import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import SetIssueTagsPopup from '../popups/SetIssueTagsPopup';
import TagsList from '../../../components/tags/TagsList';
@@ -26,14 +27,14 @@ import { setIssueTags } from '../../../api/issues';
import { translate } from '../../../helpers/l10n';
import type { Issue } from '../types';
-type Props = {
+type Props = {|
canSetTags: boolean,
isOpen: boolean,
issue: Issue,
+ onChange: (Issue) => void,
onFail: (Error) => void,
- onIssueChange: (Promise<*>, oldIssue?: Issue, newIssue?: Issue) => void,
togglePopup: (string) => void
-};
+|};
export default class IssueTags extends React.PureComponent {
props: Props;
@@ -45,7 +46,8 @@ export default class IssueTags extends React.PureComponent {
setTags = (tags: Array<string>) => {
const { issue } = this.props;
const newIssue = { ...issue, tags };
- this.props.onIssueChange(
+ updateIssue(
+ this.props.onChange,
setIssueTags({ issue: issue.key, tags: tags.join(',') }),
issue,
newIssue
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
index 4f847049f54..55ef295f55d 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js
@@ -19,23 +19,27 @@
*/
// @flow
import React from 'react';
+import { Link } from 'react-router';
import IssueChangelog from './IssueChangelog';
import IssueMessage from './IssueMessage';
+import SimilarIssuesFilter from './SimilarIssuesFilter';
import { getSingleIssueUrl } from '../../../helpers/urls';
import { translate } from '../../../helpers/l10n';
import type { Issue } from '../types';
-type Props = {
+type Props = {|
issue: Issue,
currentPopup: string,
onFail: (Error) => void,
- onFilterClick?: () => void,
+ onFilter?: (property: string, issue: Issue) => void,
togglePopup: (string) => void
-};
+|};
+
+const stopPropagation = (event: Event) => event.stopPropagation();
export default function IssueTitleBar(props: Props) {
const { issue } = props;
- const hasSimilarIssuesFilter = props.onFilterClick != null;
+ const hasSimilarIssuesFilter = props.onFilter != null;
return (
<table className="issue-table">
@@ -66,21 +70,21 @@ export default function IssueTitleBar(props: Props) {
</span>
</li>}
<li className="issue-meta">
- <a
+ <Link
className="js-issue-permalink icon-link"
- href={getSingleIssueUrl(issue.key)}
- target="_blank"
+ onClick={stopPropagation}
+ to={getSingleIssueUrl(issue.key)}
/>
</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>
+ <SimilarIssuesFilter
+ isOpen={props.currentPopup === 'similarIssues'}
+ issue={issue}
+ togglePopup={props.togglePopup}
+ onFail={props.onFail}
+ onFilter={props.onFilter}
+ />
</li>}
</ul>
</td>
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
index 03cd4e41d86..24e3625d529 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.js
@@ -19,6 +19,7 @@
*/
// @flow
import React from 'react';
+import { updateIssue } from '../actions';
import BubblePopupHelper from '../../../components/common/BubblePopupHelper';
import SetTransitionPopup from '../popups/SetTransitionPopup';
import StatusHelper from '../../../components/shared/StatusHelper';
@@ -29,15 +30,20 @@ type Props = {
hasTransitions: boolean,
isOpen: boolean,
issue: Issue,
- setIssueProperty: (string, string, apiCall: (Object) => Promise<*>, string) => void,
+ onChange: (Issue) => void,
togglePopup: (string) => void
};
export default class IssueTransition extends React.PureComponent {
props: Props;
- setTransition = (transition: string) =>
- this.props.setIssueProperty('transition', 'transition', setIssueTransition, transition);
+ setTransition = (transition: string) => {
+ updateIssue(
+ this.props.onChange,
+ setIssueTransition({ issue: this.props.issue.key, transition })
+ );
+ this.toggleSetTransition();
+ };
toggleSetTransition = (open?: boolean) => {
this.props.togglePopup('transition', open);
diff --git a/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js b/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js
new file mode 100644
index 00000000000..c28593d7c89
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/components/SimilarIssuesFilter.js
@@ -0,0 +1,69 @@
+/*
+ * 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 SimilarIssuesPopup from '../popups/SimilarIssuesPopup';
+import { translate } from '../../../helpers/l10n';
+import type { Issue } from '../types';
+
+type Props = {|
+ isOpen: boolean,
+ issue: Issue,
+ togglePopup: (string) => void,
+ onFail: (Error) => void,
+ onFilter: (property: string, issue: Issue) => void
+|};
+
+export default class SimilarIssuesFilter extends React.PureComponent {
+ props: Props;
+
+ handleClick = (evt: SyntheticInputEvent) => {
+ evt.preventDefault();
+ this.togglePopup();
+ };
+
+ handleFilter = (property: string, issue: Issue) => {
+ this.togglePopup(false);
+ this.props.onFilter(property, issue);
+ };
+
+ togglePopup = (open?: boolean) => {
+ this.props.togglePopup('similarIssues', open);
+ };
+
+ render() {
+ return (
+ <BubblePopupHelper
+ isOpen={this.props.isOpen}
+ position="bottomright"
+ togglePopup={this.togglePopup}
+ popup={<SimilarIssuesPopup issue={this.props.issue} onFilter={this.handleFilter} />}>
+ <button
+ className="js-issue-filter button-link issue-action issue-action-with-options"
+ aria-label={translate('issue.filter_similar_issues')}
+ onClick={this.handleClick}>
+ <i className="icon-filter icon-half-transparent" />{' '}
+ <i className="icon-dropdown" />
+ </button>
+ </BubblePopupHelper>
+ );
+ }
+}
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
index ca4a95ff08b..608112423d5 100644
--- 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
@@ -19,7 +19,6 @@
*/
import { shallow } from 'enzyme';
import React from 'react';
-import moment from 'moment';
import IssueChangelog from '../IssueChangelog';
import { click } from '../../../../helpers/testUtils';
@@ -29,7 +28,11 @@ const issue = {
creationDate: '2017-03-01T09:36:01+0100'
};
-moment.fn.fromNow = jest.fn(() => 'a month ago');
+jest.mock('moment', () =>
+ () => ({
+ format: () => 'March 1, 2017 9:36 AM',
+ fromNow: () => 'a month ago'
+ }));
it('should render correctly', () => {
const element = shallow(
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
index d681183f2c3..9096b729386 100644
--- 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
@@ -19,7 +19,6 @@
*/
import { shallow } from 'enzyme';
import React from 'react';
-import moment from 'moment';
import IssueCommentLine from '../IssueCommentLine';
import { click } from '../../../../helpers/testUtils';
@@ -32,7 +31,7 @@ const comment = {
updatable: true
};
-moment.fn.fromNow = jest.fn(() => 'a month ago');
+jest.mock('moment', () => () => ({ fromNow: () => 'a month ago' }));
it('should render correctly a comment that is not updatable', () => {
const element = shallow(
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
index 3e110b92f36..1d3b7ac4e0e 100644
--- 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
@@ -43,7 +43,7 @@ it('should render the titlebar with the filter', () => {
issue={issue}
currentPopup=""
onFail={jest.fn()}
- onFilterClick={jest.fn()}
+ onFilter={jest.fn()}
togglePopup={jest.fn()}
/>
);
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
index f51811bbd0f..e00752935ca 100644
--- 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
@@ -42,10 +42,19 @@ exports[`test should render the titlebar correctly 1`] = `
</li>
<li
className="issue-meta">
- <a
+ <Link
className="js-issue-permalink icon-link"
- href="/issues/search#issues=AVsae-CQS-9G3txfbFN2"
- target="_blank" />
+ onClick={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/issues",
+ "query": Object {
+ "issues": "AVsae-CQS-9G3txfbFN2",
+ },
+ }
+ } />
</li>
</ul>
</td>
@@ -98,23 +107,37 @@ exports[`test should render the titlebar with the filter 1`] = `
</li>
<li
className="issue-meta">
- <a
+ <Link
className="js-issue-permalink icon-link"
- href="/issues/search#issues=AVsae-CQS-9G3txfbFN2"
- target="_blank" />
+ onClick={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/issues",
+ "query": Object {
+ "issues": "AVsae-CQS-9G3txfbFN2",
+ },
+ }
+ } />
</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>
+ <SimilarIssuesFilter
+ 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]}
+ onFilter={[Function]}
+ togglePopup={[Function]} />
</li>
</ul>
</td>
diff --git a/server/sonar-web/src/main/js/components/issue/issue-view.js b/server/sonar-web/src/main/js/components/issue/issue-view.js
deleted file mode 100644
index e9b4c47cfcd..00000000000
--- a/server/sonar-web/src/main/js/components/issue/issue-view.js
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- * 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 $ from 'jquery';
-import Backbone from 'backbone';
-import Marionette from 'backbone.marionette';
-import ChangeLog from './models/changelog';
-import ChangeLogView from './views/changelog-view';
-import TransitionsFormView from './views/transitions-form-view';
-import AssignFormView from './views/assign-form-view';
-import CommentFormView from './views/comment-form-view';
-import DeleteCommentView from './views/DeleteCommentView';
-import SetSeverityFormView from './views/set-severity-form-view';
-import SetTypeFormView from './views/set-type-form-view';
-import TagsFormView from './views/tags-form-view';
-import Template from './templates/issue.hbs';
-import getCurrentUserFromStore from '../../app/utils/getCurrentUserFromStore';
-
-export default Marionette.ItemView.extend({
- template: Template,
-
- modelEvents: {
- change: 'notifyAndRender',
- transition: 'onTransition'
- },
-
- className() {
- const hasCheckbox = this.options.onCheck != null;
- return hasCheckbox ? 'issue issue-with-checkbox' : 'issue';
- },
-
- events() {
- return {
- click: 'handleClick',
- 'click .js-issue-comment': 'onComment',
- 'click .js-issue-comment-edit': 'editComment',
- 'click .js-issue-comment-delete': 'deleteComment',
- 'click .js-issue-transition': 'transition',
- 'click .js-issue-set-severity': 'setSeverity',
- 'click .js-issue-set-type': 'setType',
- 'click .js-issue-assign': 'assign',
- 'click .js-issue-assign-to-me': 'assignToMe',
- 'click .js-issue-plan': 'plan',
- 'click .js-issue-show-changelog': 'showChangeLog',
- 'click .js-issue-rule': 'showRule',
- 'click .js-issue-edit-tags': 'editTags',
- 'click .js-issue-locations': 'showLocations',
- 'click .js-issue-filter': 'filterSimilarIssues',
- 'click .js-toggle': 'onIssueCheck',
- 'click .js-issue-permalink': 'onPermalinkClick'
- };
- },
-
- notifyAndRender() {
- const { onIssueChange } = this.options;
- if (onIssueChange) {
- onIssueChange(this.model.toJSON());
- }
-
- // if ConnectedIssue is used, this view can be destroyed just after onIssueChange()
- if (!this.isDestroyed) {
- this.render();
- }
- },
-
- onRender() {
- this.$el.attr('data-key', this.model.get('key'));
- },
-
- disableControls() {
- this.$(':input').prop('disabled', true);
- },
-
- enableControls() {
- this.$(':input').prop('disabled', false);
- },
-
- resetIssue(options) {
- const that = this;
- const key = this.model.get('key');
- const componentUuid = this.model.get('componentUuid');
- this.model.reset({ key, componentUuid }, { silent: true });
- return this.model.fetch(options).done(() => that.trigger('reset'));
- },
-
- showChangeLog(e) {
- e.preventDefault();
- e.stopPropagation();
- const that = this;
- const t = $(e.currentTarget);
- const changeLog = new ChangeLog();
- return changeLog
- .fetch({
- data: { issue: this.model.get('key') }
- })
- .done(() => {
- if (that.popup) {
- that.popup.destroy();
- }
- that.popup = new ChangeLogView({
- triggerEl: t,
- bottomRight: true,
- collection: changeLog,
- issue: that.model
- });
- that.popup.render();
- });
- },
-
- updateAfterAction(response) {
- if (this.popup) {
- this.popup.destroy();
- }
- if (response) {
- this.model.set(this.model.parse(response));
- }
- },
-
- onComment(e) {
- e.stopPropagation();
- this.comment();
- },
-
- comment(options) {
- $('body').click();
- this.popup = new CommentFormView({
- triggerEl: this.$('.js-issue-comment'),
- bottom: true,
- issue: this.model,
- detailView: this,
- additionalOptions: options
- });
- this.popup.render();
- },
-
- editComment(e) {
- e.stopPropagation();
- $('body').click();
- const commentEl = $(e.currentTarget).closest('.issue-comment');
- const commentKey = commentEl.data('comment-key');
- const comment = this.model.get('comments').find(comment => comment.key === commentKey);
- this.popup = new CommentFormView({
- triggerEl: $(e.currentTarget),
- bottomRight: true,
- model: new Backbone.Model(comment),
- issue: this.model,
- detailView: this
- });
- this.popup.render();
- },
-
- deleteComment(e) {
- e.stopPropagation();
- $('body').click();
- const commentEl = $(e.currentTarget).closest('.issue-comment');
- const commentKey = commentEl.data('comment-key');
- this.popup = new DeleteCommentView({
- triggerEl: $(e.currentTarget),
- bottomRight: true,
- onDelete: () => {
- this.disableControls();
- $.ajax({
- type: 'POST',
- url: window.baseUrl + '/api/issues/delete_comment?key=' + commentKey
- }).done(r => this.updateAfterAction(r));
- }
- });
- this.popup.render();
- },
-
- transition(e) {
- e.stopPropagation();
- $('body').click();
- this.popup = new TransitionsFormView({
- triggerEl: $(e.currentTarget),
- bottom: true,
- model: this.model,
- view: this
- });
- this.popup.render();
- },
-
- setSeverity(e) {
- e.stopPropagation();
- $('body').click();
- this.popup = new SetSeverityFormView({
- triggerEl: $(e.currentTarget),
- bottom: true,
- model: this.model
- });
- this.popup.render();
- },
-
- setType(e) {
- e.stopPropagation();
- $('body').click();
- this.popup = new SetTypeFormView({
- triggerEl: $(e.currentTarget),
- bottom: true,
- model: this.model
- });
- this.popup.render();
- },
-
- assign(e) {
- e.stopPropagation();
- $('body').click();
- this.popup = new AssignFormView({
- triggerEl: $(e.currentTarget),
- bottom: true,
- model: this.model
- });
- this.popup.render();
- },
-
- assignToMe() {
- const view = new AssignFormView({
- model: this.model,
- triggerEl: $('body')
- });
- const currentUser = getCurrentUserFromStore();
- view.submit(currentUser.login, currentUser.name);
- view.destroy();
- },
-
- showRule(e) {
- e.preventDefault();
- e.stopPropagation();
- const ruleKey = this.model.get('rule');
- // lazy load Workspace
- const Workspace = require('../workspace/main').default;
- Workspace.openRule({ key: ruleKey, organization: this.model.get('projectOrganization') });
- },
-
- action(action) {
- this.disableControls();
- return this.model
- .customAction(action)
- .done(r => this.updateAfterAction(r))
- .fail(() => this.enableControls());
- },
-
- editTags(e) {
- e.stopPropagation();
- $('body').click();
- this.popup = new TagsFormView({
- triggerEl: $(e.currentTarget),
- bottomRight: true,
- model: this.model
- });
- this.popup.render();
- },
-
- showLocations() {
- this.model.trigger('locations', this.model);
- },
-
- select() {
- this.$el.addClass('selected');
- },
-
- unselect() {
- this.$el.removeClass('selected');
- },
-
- onTransition(transition) {
- if (transition === 'falsepositive' || transition === 'wontfix') {
- this.comment({ fromTransition: true });
- }
- },
-
- handleClick(e) {
- e.preventDefault();
- const { onClick } = this.options;
- if (onClick) {
- onClick(this.model.get('key'));
- }
- },
-
- filterSimilarIssues(e) {
- this.options.onFilterClick(e);
- },
-
- onIssueCheck(e) {
- this.options.onCheck(e);
- },
-
- onPermalinkClick(e) {
- e.stopPropagation();
- },
-
- serializeData() {
- const issueKey = encodeURIComponent(this.model.get('key'));
- return {
- ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
- permalink: window.baseUrl + '/issues/search#issues=' + issueKey,
- hasSecondaryLocations: this.model.get('flows').length,
- hasSimilarIssuesFilter: this.options.onFilterClick != null,
- hasCheckbox: this.options.onCheck != null,
- checked: this.options.checked
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/models/issue.js b/server/sonar-web/src/main/js/components/issue/models/issue.js
deleted file mode 100644
index 1abeee02e24..00000000000
--- a/server/sonar-web/src/main/js/components/issue/models/issue.js
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
- * 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 Backbone from 'backbone';
-
-export default Backbone.Model.extend({
- idAttribute: 'key',
-
- defaults() {
- return {
- flows: []
- };
- },
-
- url() {
- return window.baseUrl + '/api/issues';
- },
-
- urlRoot() {
- return window.baseUrl + '/api/issues';
- },
-
- parse(r) {
- let issue = Array.isArray(r.issues) && r.issues.length > 0 ? r.issues[0] : r.issue;
- if (issue) {
- issue = this._injectRelational(issue, r.components, 'component', 'key');
- issue = this._injectRelational(issue, r.components, 'project', 'key');
- issue = this._injectRelational(issue, r.components, 'subProject', 'key');
- issue = this._injectRelational(issue, r.rules, 'rule', 'key');
- issue = this._injectRelational(issue, r.users, 'assignee', 'login');
- issue = this._injectCommentsRelational(issue, r.users);
- issue = this._prepareClosed(issue);
- issue = this.ensureTextRange(issue);
- return issue;
- } else {
- return r;
- }
- },
-
- _injectRelational(issue, source, baseField, lookupField) {
- const baseValue = issue[baseField];
- if (baseValue != null && Array.isArray(source) && source.length > 0) {
- const lookupValue = source.find(candidate => candidate[lookupField] === baseValue);
- if (lookupValue != null) {
- Object.keys(lookupValue).forEach(key => {
- const newKey = baseField + key.charAt(0).toUpperCase() + key.slice(1);
- issue[newKey] = lookupValue[key];
- });
- }
- }
- return issue;
- },
-
- _injectCommentsRelational(issue, users) {
- if (issue.comments) {
- const newComments = issue.comments.map(comment => {
- let newComment = { ...comment, author: comment.login };
- delete newComment.login;
- newComment = this._injectRelational(newComment, users, 'author', 'login');
- return newComment;
- });
- return { ...issue, comments: newComments };
- }
- return issue;
- },
-
- _prepareClosed(issue) {
- if (issue.status === 'CLOSED') {
- issue.flows = [];
- delete issue.textRange;
- }
- return issue;
- },
-
- ensureTextRange(issue) {
- if (issue.line && !issue.textRange) {
- // FIXME 999999
- issue.textRange = {
- startLine: issue.line,
- endLine: issue.line,
- startOffset: 0,
- endOffset: 999999
- };
- }
- return issue;
- },
-
- sync(method, model, options) {
- const opts = options || {};
- opts.contentType = 'application/x-www-form-urlencoded';
- if (method === 'read') {
- Object.assign(opts, {
- type: 'GET',
- url: this.urlRoot() + '/search',
- data: {
- issues: model.id,
- additionalFields: '_all'
- }
- });
- }
- if (method === 'create') {
- Object.assign(opts, {
- type: 'POST',
- url: this.urlRoot() + '/create',
- data: {
- component: model.get('component'),
- line: model.get('line'),
- message: model.get('message'),
- rule: model.get('rule'),
- severity: model.get('severity')
- }
- });
- }
- const xhr = (options.xhr = Backbone.ajax(opts));
- model.trigger('request', model, xhr, opts);
- return xhr;
- },
-
- /**
- * Reset issue attributes (delete old, replace with new)
- * @param attrs
- * @param options
- * @returns {Object}
- */
- reset(attrs, options) {
- for (const key in this.attributes) {
- if (this.attributes.hasOwnProperty(key) && !(key in attrs)) {
- attrs[key] = void 0;
- }
- }
- return this.set(attrs, options);
- },
-
- /**
- * Do an action over an issue
- * @param {Object|null} options Options for jQuery ajax
- * @returns {jqXHR}
- * @private
- */
- _action(options) {
- const that = this;
- const success = function(r) {
- const attrs = that.parse(r);
- that.reset(attrs);
- if (options.success) {
- options.success(that, r, options);
- }
- };
- const opts = { type: 'POST', ...options, success };
- const xhr = (options.xhr = Backbone.ajax(opts));
- this.trigger('request', this, xhr, opts);
- return xhr;
- },
-
- /**
- * Assign issue
- * @param {String|null} assignee Assignee, can be null to unassign issue
- * @param {Object|null} options Options for jQuery ajax
- * @returns {jqXHR}
- */
- assign(assignee, options) {
- const opts = {
- url: this.urlRoot() + '/assign',
- data: { issue: this.id, assignee },
- ...options
- };
- return this._action(opts);
- },
-
- /**
- * Plan issue
- * @param {String|null} plan Action Plan, can be null to unplan issue
- * @param {Object|null} options Options for jQuery ajax
- * @returns {jqXHR}
- */
- plan(plan, options) {
- const opts = {
- url: this.urlRoot() + '/plan',
- data: { issue: this.id, plan },
- ...options
- };
- return this._action(opts);
- },
-
- /**
- * Set severity of issue
- * @param {String|null} severity Severity
- * @param {Object|null} options Options for jQuery ajax
- * @returns {jqXHR}
- */
- setSeverity(severity, options) {
- const opts = {
- url: this.urlRoot() + '/set_severity',
- data: { issue: this.id, severity },
- ...options
- };
- return this._action(opts);
- },
-
- /**
- * Do transition on issue
- * @param {String|null} transition Transition
- * @param {Object|null} options Options for jQuery ajax
- * @returns {jqXHR}
- */
- transition(transition, options) {
- const that = this;
- const opts = {
- url: this.urlRoot() + '/do_transition',
- data: { issue: this.id, transition },
- ...options
- };
- return this._action(opts).done(() => {
- that.trigger('transition', transition);
- });
- },
-
- /**
- * Set type of issue
- * @param {String|null} issueType Issue type
- * @param {Object|null} options Options for jQuery ajax
- * @returns {jqXHR}
- */
- setType(issueType, options) {
- const opts = {
- url: this.urlRoot() + '/set_type',
- data: { issue: this.id, type: issueType },
- ...options
- };
- return this._action(opts);
- },
-
- /**
- * Do a custom (plugin) action
- * @param {String} actionKey Action Key
- * @param {Object|null} options Options for jQuery ajax
- * @returns {jqXHR}
- */
- customAction(actionKey, options) {
- const opts = {
- type: 'POST',
- url: this.urlRoot() + '/do_action',
- data: { issue: this.id, actionKey },
- ...options
- };
- const xhr = Backbone.ajax(opts);
- this.trigger('request', this, xhr, opts);
- return xhr;
- },
-
- getLinearLocations() {
- const textRange = this.get('textRange');
- if (!textRange) {
- return [];
- }
- const locations = [];
- for (let line = textRange.startLine; line <= textRange.endLine; line++) {
- // TODO fix 999999
- const from = line === textRange.startLine ? textRange.startOffset : 0;
- const to = line === textRange.endLine ? textRange.endOffset : 999999;
- locations.push({ line, from, to });
- }
- return locations;
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js
new file mode 100644
index 00000000000..88d352e9950
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/issue/popups/SimilarIssuesPopup.js
@@ -0,0 +1,137 @@
+/*
+ * 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 SeverityHelper from '../../../components/shared/SeverityHelper';
+import StatusHelper from '../../../components/shared/StatusHelper';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
+import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
+import Avatar from '../../../components/ui/Avatar';
+import { translate } from '../../../helpers/l10n';
+import { fileFromPath, limitComponentName } from '../../../helpers/path';
+import type { Issue } from '../types';
+
+type Props = {|
+ issue: Issue,
+ onFilter: (property: string, issue: Issue) => void,
+ popupPosition?: {}
+|};
+
+export default class SimilarIssuesPopup extends React.PureComponent {
+ props: Props;
+
+ handleSelect = (property: string) => {
+ this.props.onFilter(property, this.props.issue);
+ };
+
+ render() {
+ const { issue } = this.props;
+
+ const items = [
+ 'type',
+ 'severity',
+ 'status',
+ 'resolution',
+ 'assignee',
+ 'rule',
+ ...(issue.tags || []).map(tag => `tag###${tag}`),
+ 'project',
+ // $FlowFixMe items are filtered later
+ issue.subProject ? 'module' : undefined,
+ 'file'
+ ].filter(item => item);
+
+ return (
+ <BubblePopup
+ position={this.props.popupPosition}
+ customClass="bubble-popup-menu bubble-popup-bottom-right">
+ <header className="menu-search">
+ <h6>{translate('issue.filter_similar_issues')}</h6>
+ </header>
+
+ <SelectList currentItem={items[0]} items={items} onSelect={this.handleSelect}>
+ <SelectListItem item="type">
+ <IssueTypeIcon className="little-spacer-right" query={issue.type} />
+ {translate('issue.type', issue.type)}
+ </SelectListItem>
+
+ <SelectListItem item="severity">
+ <SeverityHelper severity={issue.severity} />
+ </SelectListItem>
+
+ <SelectListItem item="status">
+ <StatusHelper status={issue.status} />
+ </SelectListItem>
+
+ <SelectListItem item="resolution">
+ {issue.resolution != null
+ ? translate('issue.resolution', issue.resolution)
+ : translate('unresolved')}
+ </SelectListItem>
+
+ <SelectListItem item="assignee">
+ {issue.assignee != null
+ ? <span>
+ {translate('assigned_to')}
+ <Avatar
+ className="little-spacer-left little-spacer-right"
+ hash={issue.assigneeAvatar}
+ size={16}
+ />
+ {issue.assigneeName}
+ </span>
+ : translate('unassigned')}
+ </SelectListItem>
+
+ <SelectListItem item="rule">
+ {limitComponentName(issue.ruleName)}
+ </SelectListItem>
+
+ {issue.tags != null &&
+ issue.tags.map(tag => (
+ <SelectListItem key={`tag###${tag}`} item={`tag###${tag}`}>
+ <i className="icon-tags icon-half-transparent little-spacer-right" />
+ {tag}
+ </SelectListItem>
+ ))}
+
+ <SelectListItem item="project">
+ <QualifierIcon className="little-spacer-right" qualifier="TRK" />
+ {issue.projectName}
+ </SelectListItem>
+
+ {issue.subProject != null &&
+ <SelectListItem item="module">
+ <QualifierIcon className="little-spacer-right" qualifier="BRC" />
+ {issue.subProjectName}
+ </SelectListItem>}
+
+ <SelectListItem item="file">
+ <QualifierIcon className="little-spacer-right" qualifier={issue.componentQualifier} />
+ {fileFromPath(issue.componentLongName)}
+ </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
index 6c4f9d5977e..35d5c05b5f2 100644
--- 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
@@ -21,6 +21,8 @@ import { shallow } from 'enzyme';
import React from 'react';
import ChangelogPopup from '../ChangelogPopup';
+jest.mock('moment', () => () => ({ format: () => 'March 1, 2017 9:36 AM' }));
+
it('should render the changelog popup correctly', () => {
const element = shallow(
<ChangelogPopup
diff --git a/server/sonar-web/src/main/js/components/issue/templates/DeleteComment.hbs b/server/sonar-web/src/main/js/components/issue/templates/DeleteComment.hbs
deleted file mode 100644
index 939bf523509..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/DeleteComment.hbs
+++ /dev/null
@@ -1,6 +0,0 @@
-<div class="text-right">
- <div class="spacer-bottom">{{t 'issue.comment.delete_confirm_message'}}</div>
- <button class="button-red">{{t 'delete'}}</button>
-</div>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/comment-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/comment-form.hbs
deleted file mode 100644
index d88b8a7da8d..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/comment-form.hbs
+++ /dev/null
@@ -1,18 +0,0 @@
-<div class="issue-comment-form-text">
- <textarea rows="2" {{#if options.fromTransition}}placeholder="Please tell why?"{{/if}}>{{show raw markdown}}</textarea>
-</div>
-
-<div class="issue-comment-form-footer">
- <div class="issue-comment-form-actions">
- <div class="button-group">
- <button class="js-issue-comment-submit" disabled>
- {{#if id}}{{t 'save'}}{{else}}{{t 'issue.comment.submit'}}{{/if}}
- </button>
- </div>
- <a class="js-issue-comment-cancel">{{t 'cancel'}}</a>
- </div>
-
- <div class="issue-comment-form-tips">{{> ../../common/templates/_markdown-tips }}</div>
-</div>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form-option.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form-option.hbs
deleted file mode 100644
index 29550cde4da..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form-option.hbs
+++ /dev/null
@@ -1,5 +0,0 @@
-<li>
- <a href="#" class="js-issue-assignee" data-value="{{id}}" data-text="{{text}}">
- {{text}}
- </a>
-</li>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form.hbs
deleted file mode 100644
index 64d2d0d7166..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-assign-form.hbs
+++ /dev/null
@@ -1,10 +0,0 @@
-<div class="search-box menu-search">
- <button class="search-box-submit button-clean">
- <i class="icon-search-new"></i>
- </button>
- <input class="search-box-input" type="search" placeholder="{{t 'search_verb'}}" value="{{query}}">
-</div>
-
-<ul class="menu"></ul>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-changelog.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-changelog.hbs
deleted file mode 100644
index 7ba2e7c2937..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-changelog.hbs
+++ /dev/null
@@ -1,37 +0,0 @@
-<div class="issue-changelog">
- <table class="spaced">
- <tbody>
-
- <tr>
- <td class="thin text-left text-top" nowrap>{{dt issue.creationDate}}</td>
- <td class="thin text-left text-top" nowrap></td>
- <td class="text-left text-top">
- {{#if issue.author}}
- {{t 'created_by'}} {{issue.author}}
- {{else}}
- {{t 'created'}}
- {{/if}}
- </td>
- </tr>
-
- {{#each items}}
- <tr>
- <td class="thin text-left text-top" nowrap>{{dt creationDate}}</td>
- <td class="thin text-left text-top" nowrap>
- {{#if userName}}
- {{#ifShowAvatars}}{{avatarHelperNew avatar 16}}{{/ifShowAvatars}}
- {{/if}}
- {{userName}}
- </td>
- <td class="text-left text-top">
- {{#each diffs}}
- {{changelog this}}<br>
- {{/each}}
- </td>
- </tr>
- {{/each}}
- </tbody>
- </table>
-</div>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-plan-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-plan-form.hbs
deleted file mode 100644
index 9184dd34b64..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-plan-form.hbs
+++ /dev/null
@@ -1,13 +0,0 @@
-<ul class="menu">
- {{#each items}}
- {{#notEq status 'CLOSED'}}
- <li>
- <a href="#" class="js-issue-assignee" data-value="{{key}}" data-text="{{name}}">
- {{name}}
- </a>
- </li>
- {{/notEq}}
- {{/each}}
-</ul>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-set-severity-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-set-severity-form.hbs
deleted file mode 100644
index ea6a3a92b15..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-set-severity-form.hbs
+++ /dev/null
@@ -1,11 +0,0 @@
-<ul class="menu">
- {{#each items}}
- <li>
- <a href="#" class="js-issue-severity" data-value="{{this}}">
- {{severityIcon this}} {{t 'severity' this}}
- </a>
- </li>
- {{/each}}
-</ul>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-set-type-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-set-type-form.hbs
deleted file mode 100644
index 3f42921aba2..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-set-type-form.hbs
+++ /dev/null
@@ -1,11 +0,0 @@
-<ul class="menu">
- {{#each items}}
- <li>
- <a href="#" class="js-issue-type" data-value="{{this}}">
- {{issueType this}}
- </a>
- </li>
- {{/each}}
-</ul>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form-option.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form-option.hbs
deleted file mode 100644
index 90df7aa6e62..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form-option.hbs
+++ /dev/null
@@ -1,17 +0,0 @@
-<li>
- <a href="#" data-value="{{tag}}" data-text="{{tag}}"
- {{#if selected}}data-selected{{/if}}>
-
- {{#if selected}}
- <i class="icon-checkbox icon-checkbox-checked"></i>
- {{else}}
- <i class="icon-checkbox"></i>
- {{/if}}
-
- {{#if custom}}
- + {{tag}}
- {{else}}
- {{tag}}
- {{/if}}
- </a>
-</li>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form.hbs
deleted file mode 100644
index 64d2d0d7166..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-tags-form.hbs
+++ /dev/null
@@ -1,10 +0,0 @@
-<div class="search-box menu-search">
- <button class="search-box-submit button-clean">
- <i class="icon-search-new"></i>
- </button>
- <input class="search-box-input" type="search" placeholder="{{t 'search_verb'}}" value="{{query}}">
-</div>
-
-<ul class="menu"></ul>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue-transitions-form.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue-transitions-form.hbs
deleted file mode 100644
index ef8ae2f24af..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue-transitions-form.hbs
+++ /dev/null
@@ -1,12 +0,0 @@
-<ul class="menu">
- {{#each transitions}}
- <li>
- <a href="#" class="js-issue-transition" data-value="{{this}}"
- title="{{t 'issue.transition' this 'description'}}" data-placement="right" data-container="body">
- {{t 'issue.transition' this}}
- </a>
- </li>
- {{/each}}
-</ul>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/components/issue/templates/issue.hbs b/server/sonar-web/src/main/js/components/issue/templates/issue.hbs
deleted file mode 100644
index 72e59a58a1f..00000000000
--- a/server/sonar-web/src/main/js/components/issue/templates/issue.hbs
+++ /dev/null
@@ -1,182 +0,0 @@
-<div class="issue-inner">
-
- <table class="issue-table">
- <tr>
- <td>
- <div class="issue-message">
- {{message}}&nbsp;
- <button class="button-link js-issue-rule issue-rule icon-ellipsis-h"
- aria-label="{{t 'issue.rule_details'}}"></button>
- </div>
- </td>
-
- <td class="issue-table-meta-cell issue-table-meta-cell-first">
- <ul class="list-inline issue-meta-list">
- <li class="issue-meta">
- <button class="button-link issue-action issue-action-with-options js-issue-show-changelog" title="{{dt creationDate}}">
- <span class="issue-meta-label">{{fromNow creationDate}}</span>&nbsp;<i class="icon-dropdown"></i>
- </button>
- </li>
-
- {{#if line}}
- <li class="issue-meta">
- <span class="issue-meta-label" title="{{t 'line_number'}}">L{{line}}</span>
- </li>
- {{/if}}
-
- {{#if hasSecondaryLocations}}
- <li class="issue-meta issue-meta-locations">
- <button class="button-link issue-action js-issue-locations">
- <i class="icon-issue-flow"></i>
- </button>
- </li>
- {{/if}}
-
- <li class="issue-meta">
- <a class="js-issue-permalink icon-link" href="{{permalink}}" target="_blank"></a>
- </li>
-
- {{#if hasSimilarIssuesFilter}}
- <li class="issue-meta">
- <button class="button-link issue-action issue-action-with-options js-issue-filter"
- aria-label="{{t "issue.filter_similar_issues"}}">
- <i class="icon-filter icon-half-transparent"></i>&nbsp;<i class="icon-dropdown"></i>
- </button>
- </li>
- {{/if}}
- </ul>
- </td>
- </tr>
- </table>
-
- <table class="issue-table">
- <tr>
- <td>
- <ul class="list-inline issue-meta-list">
- <li class="issue-meta">
- {{#inArray actions "set_severity"}}
- <button class="button-link issue-action issue-action-with-options js-issue-set-type">
- {{issueTypeIcon this.type}} {{issueType this.type}}&nbsp;<i class="icon-dropdown"></i>
- </button>
- {{else}}
- {{issueTypeIcon this.type}} {{issueType this.type}}
- {{/inArray}}
- </li>
-
- <li class="issue-meta">
- {{#inArray actions "set_severity"}}
- <button class="button-link issue-action issue-action-with-options js-issue-set-severity">
- <span class="issue-meta-label">{{severityHelper severity}}</span>&nbsp;<i class="icon-dropdown"></i>
- </button>
- {{else}}
- {{severityHelper severity}}
- {{/inArray}}
- </li>
-
- <li class="issue-meta">
- {{#notEmpty transitions}}
- <button class="button-link issue-action issue-action-with-options js-issue-transition">
- <span class="issue-meta-label">{{statusHelper status resolution}}</span>&nbsp;<i
- class="icon-dropdown"></i>
- </button>
- {{else}}
- {{statusHelper status resolution}}
- {{/notEmpty}}
- </li>
-
- <li class="issue-meta">
- {{#inArray actions "assign"}}
- <button class="button-link issue-action issue-action-with-options js-issue-assign">
- {{#if assignee}}
- {{#ifShowAvatars}}
- <span class="text-top">{{avatarHelperNew assigneeAvatar 16}}</span>
- {{/ifShowAvatars}}
- {{/if}}
- <span class="issue-meta-label">{{#if assignee}}{{assigneeName}}{{else}}{{t 'unassigned'}}{{/if}}</span>&nbsp;<i
- class="icon-dropdown"></i>
- </button>
- {{else}}
- {{#if assignee}}
- {{#ifShowAvatars}}
- <span class="text-top">{{avatarHelperNew assigneeAvatar 16}}</span>
- {{/ifShowAvatars}}
- {{/if}}
- <span class="issue-meta-label">{{#if assignee}}{{assigneeName}}{{else}}{{t 'unassigned'}}{{/if}}</span>
- {{/inArray}}
- </li>
-
- {{#if debt}}
- <li class="issue-meta">
- <span class="issue-meta-label">
- {{tp 'issue.x_effort' debt}}
- </span>
- </li>
- {{/if}}
-
- {{#inArray actions "comment"}}
- <li class="issue-meta">
- <button class="button-link issue-action js-issue-comment"><span
- class="issue-meta-label">{{t 'issue.comment.formlink' }}</span></button>
- </li>
- {{/inArray}}
- </ul>
-
- {{#inArray actions "assign_to_me"}}
- <button class="button-link hidden js-issue-assign-to-me"></button>
- {{/inArray}}
- </td>
-
- <td class="issue-table-meta-cell">
- <ul class="list-inline">
- <li class="issue-meta js-issue-tags">
- {{#inArray actions "set_tags"}}
- <button class="button-link issue-action issue-action-with-options js-issue-edit-tags">
- <span>
- <i class="icon-tags icon-half-transparent"></i>&nbsp;<span>{{#if tags}}{{join tags ', '}}{{else}}{{t 'issue.no_tag'}}{{/if}}</span>
- </span>&nbsp;<i class="icon-dropdown"></i>
- </button>
- {{else}}
- <span>
- <i class="icon-tags icon-half-transparent"></i>&nbsp;<span>{{#if tags}}{{join tags ', '}}{{else}}{{t 'issue.no_tag'}}{{/if}}</span>
- </span>
- {{/inArray}}
- </li>
- </ul>
- </td>
- </tr>
- </table>
-
- {{#notEmpty comments}}
- <div class="issue-comments">
- {{#each comments}}
- <div class="issue-comment" data-comment-key="{{key}}">
- <div class="issue-comment-author" title="{{authorName}}">
- {{#ifShowAvatars}}{{avatarHelperNew authorAvatar 16}}{{else}}
- <i class="icon-comment icon-half-transparent"></i>{{/ifShowAvatars}}&nbsp;{{authorName}}
- </div>
- <div class="issue-comment-text markdown">{{{show html htmlText}}}</div>
- <div class="issue-comment-age">({{fromNow createdAt}})</div>
- <div class="issue-comment-actions">
- {{#if updatable}}
- <button class="js-issue-comment-edit button-link icon-edit icon-half-transparent"></button>
- <button class="js-issue-comment-delete button-link icon-delete icon-half-transparent"
- data-confirm-msg="{{t 'issue.comment.delete_confirm_message'}}"></button>
- {{/if}}
- </div>
- </div>
- {{/each}}
- </div>
- {{/notEmpty}}
-
-</div>
-
-<a class="issue-navigate js-issue-navigate">
- <i class="issue-navigate-to-left icon-chevron-left"></i>
- <i class="issue-navigate-to-right icon-chevron-right"></i>
-</a>
-
-{{#if hasCheckbox}}
- <div class="js-toggle issue-checkbox-container">
- <i class="issue-checkbox icon-checkbox {{#if checked}}icon-checkbox-checked{{/if}}"></i>
- </div>
-{{/if}}
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 690c38146cb..4a07b129eeb 100644
--- a/server/sonar-web/src/main/js/components/issue/types.js
+++ b/server/sonar-web/src/main/js/components/issue/types.js
@@ -52,6 +52,10 @@ export type Issue = {
assigneeName?: string,
author?: string,
comments?: Array<IssueComment>,
+ component: string,
+ componentLongName: string,
+ componentQualifier: string,
+ componentUuid: string,
creationDate: string,
effort?: string,
key: string,
@@ -61,11 +65,18 @@ export type Issue = {
line?: number,
message: string,
organization: string,
+ project: string,
+ projectName: string,
projectOrganization: string,
+ projectUuid: string,
resolution?: string,
rule: string,
+ ruleName: string,
severity: string,
status: string,
+ subProject?: string,
+ subProjectName?: string,
+ subProjectUuid?: string,
tags?: Array<string>,
textRange: TextRange,
transitions?: Array<string>,
diff --git a/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js b/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js
deleted file mode 100644
index a3e81ef0dae..00000000000
--- a/server/sonar-web/src/main/js/components/issue/views/assign-form-view.js
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * 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 $ from 'jquery';
-import { debounce, uniqBy } from 'lodash';
-import ActionOptionsView from '../../common/action-options-view';
-import Template from '../templates/issue-assign-form.hbs';
-import OptionTemplate from '../templates/issue-assign-form-option.hbs';
-import { translate } from '../../../helpers/l10n';
-import getCurrentUserFromStore from '../../../app/utils/getCurrentUserFromStore';
-import { areThereCustomOrganizations } from '../../../store/organizations/utils';
-
-export default ActionOptionsView.extend({
- template: Template,
- optionTemplate: OptionTemplate,
-
- events() {
- return {
- ...ActionOptionsView.prototype.events.apply(this, arguments),
- 'click input': 'onInputClick',
- 'keydown input': 'onInputKeydown',
- 'keyup input': 'onInputKeyup'
- };
- },
-
- initialize() {
- ActionOptionsView.prototype.initialize.apply(this, arguments);
- this.assignees = null;
- this.organizationKey = areThereCustomOrganizations()
- ? this.model.get('projectOrganization')
- : null;
- this.debouncedSearch = debounce(this.search, 250);
- },
-
- getAssignee() {
- return this.model.get('assignee');
- },
-
- getAssigneeName() {
- return this.model.get('assigneeName');
- },
-
- onRender() {
- const that = this;
- ActionOptionsView.prototype.onRender.apply(this, arguments);
- this.renderTags();
- setTimeout(
- () => {
- that.$('input').focus();
- },
- 100
- );
- },
-
- renderTags() {
- this.$('.menu').empty();
- this.getAssignees().forEach(this.renderAssignee, this);
- this.bindUIElements();
- this.selectInitialOption();
- },
-
- renderAssignee(assignee) {
- const html = this.optionTemplate(assignee);
- this.$('.menu').append(html);
- },
-
- selectOption(e) {
- const assignee = $(e.currentTarget).data('value');
- const assigneeName = $(e.currentTarget).data('text');
- this.submit(assignee, assigneeName);
- return ActionOptionsView.prototype.selectOption.apply(this, arguments);
- },
-
- submit(assignee) {
- return this.model.assign(assignee);
- },
-
- onInputClick(e) {
- e.stopPropagation();
- },
-
- onInputKeydown(e) {
- this.query = this.$('input').val();
- if (e.keyCode === 38) {
- this.selectPreviousOption();
- }
- if (e.keyCode === 40) {
- this.selectNextOption();
- }
- if (e.keyCode === 13) {
- this.selectActiveOption();
- }
- if (e.keyCode === 27) {
- this.destroy();
- }
- if ([9, 13, 27, 38, 40].indexOf(e.keyCode) !== -1) {
- return false;
- }
- },
-
- onInputKeyup() {
- let query = this.$('input').val();
- if (query !== this.query) {
- if (query.length < 2) {
- query = '';
- }
- this.query = query;
- this.debouncedSearch(query);
- }
- },
-
- search(query) {
- const that = this;
- if (query.length > 1) {
- const searchUrl = this.organizationKey != null
- ? '/organizations/search_members'
- : '/users/search';
- const queryData = { q: query };
- if (this.organizationKey != null) {
- queryData.organization = this.organizationKey;
- }
- $.get(window.baseUrl + '/api' + searchUrl, queryData).done(data => {
- that.resetAssignees(data.users);
- });
- } else {
- this.resetAssignees();
- }
- },
-
- resetAssignees(users) {
- if (users) {
- this.assignees = users.map(user => {
- return { id: user.login, text: user.name };
- });
- } else {
- this.assignees = null;
- }
- this.renderTags();
- },
-
- getAssignees() {
- if (this.assignees) {
- return this.assignees;
- }
- const currentUser = getCurrentUserFromStore();
- const assignees = [
- { id: currentUser.login, text: currentUser.name },
- { id: '', text: translate('unassigned') }
- ];
- return this.makeUnique(assignees);
- },
-
- makeUnique(assignees) {
- return uniqBy(assignees, assignee => assignee.id);
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/views/comment-form-view.js b/server/sonar-web/src/main/js/components/issue/views/comment-form-view.js
deleted file mode 100644
index 52d68bcd7c0..00000000000
--- a/server/sonar-web/src/main/js/components/issue/views/comment-form-view.js
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * 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 $ from 'jquery';
-import PopupView from '../../common/popup';
-import Template from '../templates/comment-form.hbs';
-
-export default PopupView.extend({
- className: 'bubble-popup issue-comment-bubble-popup',
- template: Template,
-
- ui: {
- textarea: '.issue-comment-form-text textarea',
- cancelButton: '.js-issue-comment-cancel',
- submitButton: '.js-issue-comment-submit'
- },
-
- events: {
- click: 'onClick',
- 'keydown @ui.textarea': 'onKeydown',
- 'keyup @ui.textarea': 'toggleSubmit',
- 'click @ui.cancelButton': 'cancel',
- 'click @ui.submitButton': 'submit'
- },
-
- onRender() {
- const that = this;
- PopupView.prototype.onRender.apply(this, arguments);
- setTimeout(
- () => {
- that.ui.textarea.focus();
- },
- 100
- );
- },
-
- toggleSubmit() {
- this.ui.submitButton.prop('disabled', this.ui.textarea.val().length === 0);
- },
-
- onClick(e) {
- e.stopPropagation();
- },
-
- onKeydown(e) {
- if (e.keyCode === 27) {
- this.destroy();
- }
- if (e.keyCode === 13 && (e.metaKey || e.ctrlKey)) {
- this.submit();
- }
- },
-
- cancel() {
- this.options.detailView.updateAfterAction();
- },
-
- disableForm() {
- this.$(':input').prop('disabled', true);
- },
-
- enableForm() {
- this.$(':input').prop('disabled', false);
- },
-
- submit() {
- const text = this.ui.textarea.val();
-
- if (!text.length) {
- return;
- }
-
- const update = this.model && this.model.has('key');
- const method = update ? 'edit_comment' : 'add_comment';
- const url = window.baseUrl + '/api/issues/' + method;
- const data = { text };
- if (update) {
- data.key = this.model.get('key');
- } else {
- data.issue = this.options.issue.id;
- }
- this.disableForm();
- this.options.detailView.disableControls();
- $.post(url, data).done(r => this.options.detailView.updateAfterAction(r)).fail(() => {
- this.enableForm();
- this.options.detailView.enableControls();
- });
- },
-
- serializeData() {
- const options = { fromTransition: false, ...this.options.additionalOptions };
- return {
- ...PopupView.prototype.serializeData.apply(this, arguments),
- options
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/views/set-severity-form-view.js b/server/sonar-web/src/main/js/components/issue/views/set-severity-form-view.js
deleted file mode 100644
index b30c689e1e9..00000000000
--- a/server/sonar-web/src/main/js/components/issue/views/set-severity-form-view.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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 $ from 'jquery';
-import ActionOptionsView from '../../common/action-options-view';
-import Template from '../templates/issue-set-severity-form.hbs';
-
-export default ActionOptionsView.extend({
- template: Template,
-
- getTransition() {
- return this.model.get('severity');
- },
-
- selectInitialOption() {
- return this.makeActive(this.getOptions().filter(`[data-value="${this.getTransition()}"]`));
- },
-
- selectOption(e) {
- const severity = $(e.currentTarget).data('value');
- this.submit(severity);
- return ActionOptionsView.prototype.selectOption.apply(this, arguments);
- },
-
- submit(severity) {
- return this.model.setSeverity(severity);
- },
-
- serializeData() {
- return {
- ...ActionOptionsView.prototype.serializeData.apply(this, arguments),
- items: ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/views/set-type-form-view.js b/server/sonar-web/src/main/js/components/issue/views/set-type-form-view.js
deleted file mode 100644
index 719d679e762..00000000000
--- a/server/sonar-web/src/main/js/components/issue/views/set-type-form-view.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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 $ from 'jquery';
-import ActionOptionsView from '../../common/action-options-view';
-import Template from '../templates/issue-set-type-form.hbs';
-
-export default ActionOptionsView.extend({
- template: Template,
-
- getType() {
- return this.model.get('type');
- },
-
- selectInitialOption() {
- return this.makeActive(this.getOptions().filter(`[data-value="${this.getType()}"]`));
- },
-
- selectOption(e) {
- const issueType = $(e.currentTarget).data('value');
- this.submit(issueType);
- return ActionOptionsView.prototype.selectOption.apply(this, arguments);
- },
-
- submit(issueType) {
- return this.model.setType(issueType);
- },
-
- serializeData() {
- return {
- ...ActionOptionsView.prototype.serializeData.apply(this, arguments),
- items: ['BUG', 'VULNERABILITY', 'CODE_SMELL']
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/views/tags-form-view.js b/server/sonar-web/src/main/js/components/issue/views/tags-form-view.js
deleted file mode 100644
index 87b1287bee9..00000000000
--- a/server/sonar-web/src/main/js/components/issue/views/tags-form-view.js
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * 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 $ from 'jquery';
-import { debounce, difference, without } from 'lodash';
-import ActionOptionsView from '../../common/action-options-view';
-import Template from '../templates/issue-tags-form.hbs';
-import OptionTemplate from '../templates/issue-tags-form-option.hbs';
-
-export default ActionOptionsView.extend({
- template: Template,
- optionTemplate: OptionTemplate,
-
- modelEvents: {
- 'change:tags': 'renderTags'
- },
-
- events() {
- return {
- ...ActionOptionsView.prototype.events.apply(this, arguments),
- 'click input': 'onInputClick',
- 'keydown input': 'onInputKeydown',
- 'keyup input': 'onInputKeyup'
- };
- },
-
- initialize() {
- ActionOptionsView.prototype.initialize.apply(this, arguments);
- this.query = '';
- this.tags = [];
- this.selected = 0;
- this.debouncedSearch = debounce(this.search, 250);
- this.requestTags();
- },
-
- requestTags(query) {
- const that = this;
- return $.get(window.baseUrl + '/api/issues/tags', { ps: 10, q: query }).done(data => {
- that.tags = data.tags;
- that.renderTags();
- });
- },
-
- onRender() {
- const that = this;
- ActionOptionsView.prototype.onRender.apply(this, arguments);
- this.renderTags();
- setTimeout(
- () => {
- that.$('input').focus();
- },
- 100
- );
- },
-
- selectInitialOption() {
- this.selected = Math.max(Math.min(this.selected, this.getOptions().length - 1), 0);
- this.makeActive(this.getOptions().eq(this.selected));
- },
-
- filterTags(tags) {
- return tags.filter(tag => tag.indexOf(this.query) !== -1);
- },
-
- renderTags() {
- this.$('.menu').empty();
- this.filterTags(this.getTags()).forEach(this.renderSelectedTag, this);
- this.filterTags(difference(this.tags, this.getTags())).forEach(this.renderTag, this);
- if (
- this.query.length > 0 &&
- this.tags.indexOf(this.query) === -1 &&
- this.getTags().indexOf(this.query) === -1
- ) {
- this.renderCustomTag(this.query);
- }
- this.selectInitialOption();
- },
-
- renderSelectedTag(tag) {
- const html = this.optionTemplate({
- tag,
- selected: true,
- custom: false
- });
- return this.$('.menu').append(html);
- },
-
- renderTag(tag) {
- const html = this.optionTemplate({
- tag,
- selected: false,
- custom: false
- });
- return this.$('.menu').append(html);
- },
-
- renderCustomTag(tag) {
- const html = this.optionTemplate({
- tag,
- selected: false,
- custom: true
- });
- return this.$('.menu').append(html);
- },
-
- selectOption(e) {
- e.preventDefault();
- e.stopPropagation();
- let tags = this.getTags().slice();
- const tag = $(e.currentTarget).data('value');
- if ($(e.currentTarget).data('selected') != null) {
- tags = without(tags, tag);
- } else {
- tags.push(tag);
- }
- this.selected = this.getOptions().index($(e.currentTarget));
- return this.submit(tags);
- },
-
- submit(tags) {
- const that = this;
- const _tags = this.getTags();
- this.model.set({ tags });
- return $.ajax({
- type: 'POST',
- url: window.baseUrl + '/api/issues/set_tags',
- data: {
- key: this.model.id,
- tags: tags.join()
- }
- }).fail(() => that.model.set({ tags: _tags }));
- },
-
- onInputClick(e) {
- e.stopPropagation();
- },
-
- onInputKeydown(e) {
- this.query = this.$('input').val();
- if (e.keyCode === 38) {
- this.selectPreviousOption();
- }
- if (e.keyCode === 40) {
- this.selectNextOption();
- }
- if (e.keyCode === 13) {
- this.selectActiveOption();
- }
- if (e.keyCode === 27) {
- this.destroy();
- }
- if ([9, 13, 27, 38, 40].indexOf(e.keyCode) !== -1) {
- return false;
- }
- },
-
- onInputKeyup() {
- const query = this.$('input').val();
- if (query !== this.query) {
- this.query = query;
- this.debouncedSearch(query);
- }
- },
-
- search(query) {
- this.query = query;
- return this.requestTags(query);
- },
-
- resetAssignees(users) {
- this.assignees = users.map(user => {
- return { id: user.login, text: user.name };
- });
- this.renderTags();
- },
-
- getTags() {
- return this.model.get('tags') || [];
- }
-});
diff --git a/server/sonar-web/src/main/js/components/issue/views/transitions-form-view.js b/server/sonar-web/src/main/js/components/issue/views/transitions-form-view.js
deleted file mode 100644
index 0a44b5a4b22..00000000000
--- a/server/sonar-web/src/main/js/components/issue/views/transitions-form-view.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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 $ from 'jquery';
-import ActionOptionsView from '../../common/action-options-view';
-import Template from '../templates/issue-transitions-form.hbs';
-
-export default ActionOptionsView.extend({
- template: Template,
-
- selectInitialOption() {
- this.makeActive(this.getOptions().first());
- },
-
- selectOption(e) {
- const transition = $(e.currentTarget).data('value');
- this.submit(transition);
- return ActionOptionsView.prototype.selectOption.apply(this, arguments);
- },
-
- submit(transition) {
- return this.model.transition(transition);
- }
-});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js b/server/sonar-web/src/main/js/components/layout/Page.js
index 8d15af06288..a8adef56e19 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js
+++ b/server/sonar-web/src/main/js/components/layout/Page.js
@@ -18,12 +18,25 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
-import { connect } from 'react-redux';
-import LineIssuesIndicator from './LineIssuesIndicator';
-import { getIssueByKey } from '../../../store/rootReducer';
+import React from 'react';
+import { css } from 'glamor';
-const mapStateToProps = (state, ownProps: { issueKeys: Array<string> }) => ({
- issues: ownProps.issueKeys.map(issueKey => getIssueByKey(state, issueKey))
+type Props = {
+ className?: string,
+ children?: React.Element<*>
+};
+
+const styles = css({
+ display: 'flex',
+ alignItems: 'stretch',
+ width: '100%',
+ flexGrow: 1
});
-export default connect(mapStateToProps)(LineIssuesIndicator);
+const Page = ({ className, children, ...other }: Props) => (
+ <div className={styles + (className ? ` ${className}` : '')} {...other}>
+ {children}
+ </div>
+);
+
+export default Page;
diff --git a/server/sonar-web/src/main/js/components/shared/qualifier-icon.js b/server/sonar-web/src/main/js/components/layout/PageFilters.js
index d3e8aead051..f969366de69 100644
--- a/server/sonar-web/src/main/js/components/shared/qualifier-icon.js
+++ b/server/sonar-web/src/main/js/components/layout/PageFilters.js
@@ -17,14 +17,18 @@
* 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 { css } from 'glamor';
-export default React.createClass({
- render() {
- if (!this.props.qualifier) {
- return null;
- }
- const className = 'icon-qualifier-' + this.props.qualifier.toLowerCase();
- return <i className={className} />;
- }
-});
+type Props = {
+ children?: React.Element<*>
+};
+
+const PageSide = (props: Props) => (
+ <div className={css({ width: 260, padding: 20 })}>
+ {props.children}
+ </div>
+);
+
+export default PageSide;
diff --git a/server/sonar-web/src/main/js/components/issue/views/DeleteCommentView.js b/server/sonar-web/src/main/js/components/layout/PageMain.js
index 3360de2f416..6195a1f651a 100644
--- a/server/sonar-web/src/main/js/components/issue/views/DeleteCommentView.js
+++ b/server/sonar-web/src/main/js/components/layout/PageMain.js
@@ -17,18 +17,18 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import PopupView from '../../common/popup';
-import Template from '../templates/DeleteComment.hbs';
+// @flow
+import React from 'react';
+import { css } from 'glamor';
-export default PopupView.extend({
- template: Template,
+type Props = {
+ children?: React.Element<*>
+};
- events: {
- 'click button': 'handleSubmit'
- },
+const PageMain = (props: Props) => (
+ <div className={css({ flexGrow: 1, minWidth: 740, padding: 20 })}>
+ {props.children}
+ </div>
+);
- handleSubmit(e) {
- e.preventDefault();
- this.options.onDelete();
- }
-});
+export default PageMain;
diff --git a/server/sonar-web/src/main/js/components/issue/models/changelog.js b/server/sonar-web/src/main/js/components/layout/PageMainInner.js
index bf1150a310a..41beed6518f 100644
--- a/server/sonar-web/src/main/js/components/issue/models/changelog.js
+++ b/server/sonar-web/src/main/js/components/layout/PageMainInner.js
@@ -17,14 +17,18 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import Backbone from 'backbone';
+// @flow
+import React from 'react';
+import { css } from 'glamor';
-export default Backbone.Collection.extend({
- url() {
- return window.baseUrl + '/api/issues/changelog';
- },
+type Props = {
+ children?: React.Element<*>
+};
- parse(r) {
- return r.changelog;
- }
-});
+const PageMainInner = (props: Props) => (
+ <div className={css({ minWidth: 740, maxWidth: 980 })}>
+ {props.children}
+ </div>
+);
+
+export default PageMainInner;
diff --git a/server/sonar-web/src/main/js/components/layout/PageSide.js b/server/sonar-web/src/main/js/components/layout/PageSide.js
new file mode 100644
index 00000000000..0488fbfceb9
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/layout/PageSide.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 { css, media } from 'glamor';
+
+type Props = {
+ children?: React.Element<*>,
+ top?: number
+};
+
+const width = css(
+ {
+ width: 'calc(50vw - 360px)'
+ },
+ media('(max-width: 1320px)', { width: 300 })
+);
+
+const sideStyles = css(width, {
+ flexGrow: 0,
+ flexShrink: 0,
+ borderRight: '1px solid #e6e6e6',
+ backgroundColor: '#f3f3f3'
+});
+
+const sideStickyStyles = css(width, {
+ position: 'fixed',
+ zIndex: 40,
+ top: 0,
+ bottom: 0,
+ left: 0,
+ overflowY: 'auto',
+ overflowX: 'hidden',
+ backgroundColor: '#f3f3f3'
+});
+
+const sideInnerStyles = css(
+ {
+ width: 300,
+ marginLeft: 'calc(50vw - 660px)',
+ backgroundColor: '#f3f3f3'
+ },
+ media('(max-width: 1320px)', { marginLeft: 0 })
+);
+
+const PageSide = (props: Props) => (
+ <div className={sideStyles}>
+ <div className={sideStickyStyles} style={{ top: props.top || 30 }}>
+ <div className={sideInnerStyles}>
+ {props.children}
+ </div>
+ </div>
+ </div>
+);
+
+export default PageSide;
diff --git a/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js b/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js
index d47b6e82ba4..b7b35b539c5 100644
--- a/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js
+++ b/server/sonar-web/src/main/js/components/navigator/workspace-list-view.js
@@ -20,6 +20,7 @@
import $ from 'jquery';
import { throttle } from 'lodash';
import Marionette from 'backbone.marionette';
+import key from 'keymaster';
const BOTTOM_OFFSET = 60;
diff --git a/server/sonar-web/src/main/js/components/shared/Organization.js b/server/sonar-web/src/main/js/components/shared/Organization.js
index 807abd3d11e..ac723440c9c 100644
--- a/server/sonar-web/src/main/js/components/shared/Organization.js
+++ b/server/sonar-web/src/main/js/components/shared/Organization.js
@@ -29,6 +29,7 @@ type OwnProps = {
type Props = {
link?: boolean,
+ linkClassName?: string,
organizationKey: string,
organization: { key: string, name: string } | null,
shouldBeDisplayed: boolean
@@ -51,7 +52,9 @@ class Organization extends React.Component {
return (
<span>
{this.props.link
- ? <OrganizationLink organization={organization}>{organization.name}</OrganizationLink>
+ ? <OrganizationLink className={this.props.linkClassName} organization={organization}>
+ {organization.name}
+ </OrganizationLink>
: organization.name}
<span className="slash-separator" />
</span>
diff --git a/server/sonar-web/src/main/js/components/issue/views/changelog-view.js b/server/sonar-web/src/main/js/components/shared/QualifierIcon.js
index b04b56abd6a..82ed9f7e5e1 100644
--- a/server/sonar-web/src/main/js/components/issue/views/changelog-view.js
+++ b/server/sonar-web/src/main/js/components/shared/QualifierIcon.js
@@ -17,20 +17,27 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import PopupView from '../../common/popup';
-import Template from '../templates/issue-changelog.hbs';
+import React from 'react';
+import classNames from 'classnames';
-export default PopupView.extend({
- template: Template,
+type Props = {
+ className?: string,
+ qualifier: ?string
+};
- collectionEvents: {
- sync: 'render'
- },
+export default class QualifierIcon extends React.PureComponent {
+ props: Props;
- serializeData() {
- return {
- ...PopupView.prototype.serializeData.apply(this, arguments),
- issue: this.options.issue.toJSON()
- };
+ render() {
+ if (!this.props.qualifier) {
+ return null;
+ }
+
+ const className = classNames(
+ 'icon-qualifier-' + this.props.qualifier.toLowerCase(),
+ this.props.className
+ );
+
+ return <i className={className} />;
}
-});
+}
diff --git a/server/sonar-web/src/main/js/components/issue/views/issue-popup.js b/server/sonar-web/src/main/js/components/shared/__tests__/QualifierIcon-test.js
index 96488cd1e45..857ccbaf450 100644
--- a/server/sonar-web/src/main/js/components/issue/views/issue-popup.js
+++ b/server/sonar-web/src/main/js/components/shared/__tests__/QualifierIcon-test.js
@@ -17,30 +17,19 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import PopupView from '../../common/popup';
+import React from 'react';
+import { shallow } from 'enzyme';
+import QualifierIcon from '../QualifierIcon';
-export default PopupView.extend({
- className: 'bubble-popup issue-bubble-popup',
-
- template() {
- return '<div class="bubble-popup-arrow"></div>';
- },
-
- events() {
- return {
- 'click .js-issue-form-cancel': 'destroy'
- };
- },
-
- onRender() {
- PopupView.prototype.onRender.apply(this, arguments);
- this.options.view.$el.appendTo(this.$el);
- this.options.view.render();
- },
+it('should render icon', () => {
+ expect(shallow(<QualifierIcon qualifier="TRK" />)).toMatchSnapshot();
+ expect(shallow(<QualifierIcon qualifier="trk" />)).toMatchSnapshot();
+});
- onDestroy() {
- this.options.view.destroy();
- },
+it('should not render icon', () => {
+ expect(shallow(<QualifierIcon qualifier={null} />)).toMatchSnapshot();
+});
- attachCloseEvents() {}
+it('should render with custom class', () => {
+ expect(shallow(<QualifierIcon className="spacer-right" qualifier="TRK" />)).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/QualifierIcon-test.js.snap b/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/QualifierIcon-test.js.snap
new file mode 100644
index 00000000000..58ac761a183
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/QualifierIcon-test.js.snap
@@ -0,0 +1,16 @@
+exports[`test should not render icon 1`] = `null`;
+
+exports[`test should render icon 1`] = `
+<i
+ className="icon-qualifier-trk" />
+`;
+
+exports[`test should render icon 2`] = `
+<i
+ className="icon-qualifier-trk" />
+`;
+
+exports[`test should render with custom class 1`] = `
+<i
+ className="icon-qualifier-trk spacer-right" />
+`;