aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
-rw-r--r--server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js6
-rw-r--r--server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js6
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js2
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentName.js2
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/controller.js1
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/init.js3
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js1
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js1
-rw-r--r--server/sonar-web/src/main/js/apps/component-issues/init.js130
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js303
-rw-r--r--server/sonar-web/src/main/js/apps/issues/HeaderView.js60
-rw-r--r--server/sonar-web/src/main/js/apps/issues/component-viewer/main.js132
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/App.js649
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/AppContainer.js (renamed from server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js)52
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js522
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js72
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js (renamed from server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js)50
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js106
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesList.js62
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js68
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/ListItem.js106
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js60
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/PageActions.js77
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/SearchSelect.js122
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/SearchSelect-test.js49
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/SearchSelect-test.js.snap48
-rw-r--r--server/sonar-web/src/main/js/apps/issues/controller.js217
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets-view.js63
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js135
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/author-facet.js60
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/base-facet.js41
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js176
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js85
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/file-facet.js61
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js40
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/language-facet.js84
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/mode-facet.js40
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/module-facet.js46
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/project-facet.js112
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js61
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js65
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js95
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js31
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/status-facet.js31
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js72
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/type-facet.js31
-rw-r--r--server/sonar-web/src/main/js/apps/issues/init.js87
-rw-r--r--server/sonar-web/src/main/js/apps/issues/issue-filter-view.js40
-rw-r--r--server/sonar-web/src/main/js/apps/issues/layout.js62
-rw-r--r--server/sonar-web/src/main/js/apps/issues/models/issues.js108
-rw-r--r--server/sonar-web/src/main/js/apps/issues/models/state.js81
-rw-r--r--server/sonar-web/src/main/js/apps/issues/redirects.js55
-rw-r--r--server/sonar-web/src/main/js/apps/issues/router.js35
-rw-r--r--server/sonar-web/src/main/js/apps/issues/routes.js13
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js168
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js99
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js276
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js120
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js67
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js116
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js114
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js68
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js113
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js160
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js119
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js126
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js (renamed from server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js)43
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js212
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js112
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js123
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js102
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js95
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js53
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap167
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap112
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetBox.js (renamed from server/sonar-web/src/main/js/apps/issues/facets/context-facet.js)24
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetFooter.js (renamed from server/sonar-web/src/main/js/apps/projects/components/NoProjects.js)21
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js83
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js71
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItemsList.js33
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetBox-test.js (renamed from server/sonar-web/src/main/js/apps/component-issues/routes.js)18
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetFooter-test.js (renamed from server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js)14
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js61
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItem-test.js68
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItemsList-test.js (renamed from server/sonar-web/src/main/js/apps/issues/models/issue.js)15
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetBox-test.js.snap7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetFooter-test.js.snap10
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap132
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap90
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItemsList-test.js.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/issues/styles.css23
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs145
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs26
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs11
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs42
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs16
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs12
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs15
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs12
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs16
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs24
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs13
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs13
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs13
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs89
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs12
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs80
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs26
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs5
-rw-r--r--server/sonar-web/src/main/js/apps/issues/utils.js229
-rw-r--r--server/sonar-web/src/main/js/apps/issues/workspace-header-view.js151
-rw-r--r--server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js162
-rw-r--r--server/sonar-web/src/main/js/apps/issues/workspace-list-view.js125
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js2
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js2
-rw-r--r--server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap73
-rw-r--r--server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js2
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projects-admin/projects.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/AllProjects.js62
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/App.js2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/projects/styles.css8
-rw-r--r--server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js2
131 files changed, 5571 insertions, 3809 deletions
diff --git a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js
index 3ae7d82992d..fd231aa512b 100644
--- a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js
+++ b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypes.js
@@ -43,7 +43,7 @@ export default class EntryIssueTypes extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
- to={getIssuesUrl({ resolved: false, types: 'BUG' })}
+ to={getIssuesUrl({ resolved: 'false', types: 'BUG' })}
className="about-page-issue-type-link">
{formatMeasure(bugs, 'SHORT_INT')}
</Link>
@@ -56,7 +56,7 @@ export default class EntryIssueTypes extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
- to={getIssuesUrl({ resolved: false, types: 'VULNERABILITY' })}
+ to={getIssuesUrl({ resolved: 'false', types: 'VULNERABILITY' })}
className="about-page-issue-type-link">
{formatMeasure(vulnerabilities, 'SHORT_INT')}
</Link>
@@ -69,7 +69,7 @@ export default class EntryIssueTypes extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
- to={getIssuesUrl({ resolved: false, types: 'CODE_SMELL' })}
+ to={getIssuesUrl({ resolved: 'false', types: 'CODE_SMELL' })}
className="about-page-issue-type-link">
{formatMeasure(codeSmells, 'SHORT_INT')}
</Link>
diff --git a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js
index 3b974ffd770..11db35cac3b 100644
--- a/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js
+++ b/server/sonar-web/src/main/js/apps/about/components/EntryIssueTypesForSonarQubeDotCom.js
@@ -43,7 +43,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
- to={getIssuesUrl({ resolved: false, types: 'BUG' })}
+ to={getIssuesUrl({ resolved: 'false', types: 'BUG' })}
className="about-page-issue-type-link">
{formatMeasure(bugs, 'SHORT_INT')}
</Link>
@@ -56,7 +56,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
- to={getIssuesUrl({ resolved: false, types: 'VULNERABILITY' })}
+ to={getIssuesUrl({ resolved: 'false', types: 'VULNERABILITY' })}
className="about-page-issue-type-link">
{formatMeasure(vulnerabilities, 'SHORT_INT')}
</Link>
@@ -69,7 +69,7 @@ export default class EntryIssueTypesForSonarQubeDotCom extends React.Component {
<tr>
<td className="about-page-issue-type-number">
<Link
- to={getIssuesUrl({ resolved: false, types: 'CODE_SMELL' })}
+ to={getIssuesUrl({ resolved: 'false', types: 'CODE_SMELL' })}
className="about-page-issue-type-link">
{formatMeasure(codeSmells, 'SHORT_INT')}
</Link>
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js
index 6c7d13c15b7..2a9fb4157ae 100644
--- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js
+++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.js
@@ -21,7 +21,7 @@
import React from 'react';
import { Link } from 'react-router';
import TaskType from './TaskType';
-import QualifierIcon from '../../../components/shared/qualifier-icon';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
import Organization from '../../../components/shared/Organization';
import { Task } from '../types';
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js
index 9ad27ba80ec..76ea80e652f 100644
--- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js
@@ -20,7 +20,7 @@
import React from 'react';
import { Link } from 'react-router';
import Truncated from './Truncated';
-import QualifierIcon from '../../../components/shared/qualifier-icon';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
function getTooltip(component) {
const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS';
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/controller.js b/server/sonar-web/src/main/js/apps/coding-rules/controller.js
index 01943b685bf..dd15fb39b92 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/controller.js
+++ b/server/sonar-web/src/main/js/apps/coding-rules/controller.js
@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import $ from 'jquery';
+import key from 'keymaster';
import Controller from '../../components/navigator/controller';
import Rule from './models/rule';
import RuleDetailsView from './rule-details-view';
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/init.js b/server/sonar-web/src/main/js/apps/coding-rules/init.js
index 57e689c5178..437505c5eaf 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/init.js
+++ b/server/sonar-web/src/main/js/apps/coding-rules/init.js
@@ -22,6 +22,7 @@ import $ from 'jquery';
import { sortBy } from 'lodash';
import Backbone from 'backbone';
import Marionette from 'backbone.marionette';
+import key from 'keymaster';
import State from './models/state';
import Layout from './layout';
import Rules from './models/rules';
@@ -105,7 +106,7 @@ App.on('start', function(options: {
});
this.layout.filtersRegion.show(this.filtersView);
- window.key.setScope('list');
+ key.setScope('list');
this.router = new Router({
app: this
});
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js
index 307765e54c6..722e3f22d3e 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js
+++ b/server/sonar-web/src/main/js/apps/coding-rules/rule-details-view.js
@@ -21,6 +21,7 @@ import $ from 'jquery';
import { union } from 'lodash';
import Backbone from 'backbone';
import Marionette from 'backbone.marionette';
+import key from 'keymaster';
import Rules from './models/rules';
import MetaView from './rule/rule-meta-view';
import DescView from './rule/rule-description-view';
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js b/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js
index 1e32a633a9a..b218d6939b9 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js
+++ b/server/sonar-web/src/main/js/apps/coding-rules/workspace-list-view.js
@@ -17,6 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import key from 'keymaster';
import WorkspaceListView from '../../components/navigator/workspace-list-view';
import WorkspaceListItemView from './workspace-list-item-view';
import WorkspaceListEmptyView from './workspace-list-empty-view';
diff --git a/server/sonar-web/src/main/js/apps/component-issues/init.js b/server/sonar-web/src/main/js/apps/component-issues/init.js
deleted file mode 100644
index 4b5abd4eb72..00000000000
--- a/server/sonar-web/src/main/js/apps/component-issues/init.js
+++ /dev/null
@@ -1,130 +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 { difference } from 'lodash';
-import Backbone from 'backbone';
-import Marionette from 'backbone.marionette';
-import State from '../issues/models/state';
-import Layout from '../issues/layout';
-import Issues from '../issues/models/issues';
-import Facets from '../../components/navigator/models/facets';
-import Controller from '../issues/controller';
-import Router from '../issues/router';
-import WorkspaceListView from '../issues/workspace-list-view';
-import WorkspaceHeaderView from '../issues/workspace-header-view';
-import FacetsView from './../issues/facets-view';
-import HeaderView from './../issues/HeaderView';
-
-const App = new Marionette.Application();
-const init = function({ el, component, currentUser }) {
- this.config = {
- resource: component.id,
- resourceName: component.name,
- resourceQualifier: component.qualifier
- };
- this.state = new State({
- canBulkChange: currentUser.isLoggedIn,
- isContext: true,
- contextQuery: { componentUuids: this.config.resource },
- contextComponentUuid: this.config.resource,
- contextComponentName: this.config.resourceName,
- contextComponentQualifier: this.config.resourceQualifier,
- contextOrganization: component.organization
- });
- this.updateContextFacets();
- this.list = new Issues();
- this.facets = new Facets();
-
- this.layout = new Layout({ app: this, el });
- this.layout.render();
- $('#footer').addClass('search-navigator-footer');
-
- this.controller = new Controller({ app: this });
-
- this.issuesView = new WorkspaceListView({
- app: this,
- collection: this.list
- });
- this.layout.workspaceListRegion.show(this.issuesView);
- this.issuesView.bindScrollEvents();
-
- this.workspaceHeaderView = new WorkspaceHeaderView({
- app: this,
- collection: this.list
- });
- this.layout.workspaceHeaderRegion.show(this.workspaceHeaderView);
-
- this.facetsView = new FacetsView({
- app: this,
- collection: this.facets
- });
- this.layout.facetsRegion.show(this.facetsView);
-
- this.headerView = new HeaderView({
- app: this
- });
- this.layout.filtersRegion.show(this.headerView);
-
- key.setScope('list');
- App.router = new Router({ app: App });
- Backbone.history.start();
-};
-
-App.getContextQuery = function() {
- return { componentUuids: this.config.resource };
-};
-
-App.getRestrictedFacets = function() {
- return {
- TRK: ['projectUuids'],
- BRC: ['projectUuids'],
- DIR: ['projectUuids', 'moduleUuids', 'directories'],
- DEV: ['authors'],
- DEV_PRJ: ['projectUuids', 'authors']
- };
-};
-
-App.updateContextFacets = function() {
- const facets = this.state.get('facets');
- const allFacets = this.state.get('allFacets');
- const facetsFromServer = this.state.get('facetsFromServer');
- return this.state.set({
- facets,
- allFacets: difference(allFacets, this.getRestrictedFacets()[this.config.resourceQualifier]),
- facetsFromServer: difference(
- facetsFromServer,
- this.getRestrictedFacets()[this.config.resourceQualifier]
- )
- });
-};
-
-App.on('start', options => {
- init.call(App, options);
-});
-
-export default function(el, component, currentUser) {
- App.start({ el, component, currentUser });
-
- return () => {
- Backbone.history.stop();
- App.layout.destroy();
- $('#footer').removeClass('search-navigator-footer');
- };
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js
index 03d857462bf..5261badeaf2 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/Breadcrumb.js
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import QualifierIcon from '../../../../components/shared/qualifier-icon';
+import QualifierIcon from '../../../../components/shared/QualifierIcon';
import { isDiffMetric, formatLeak } from '../../utils';
import { formatMeasure } from '../../../../helpers/measures';
diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js
index 1ccdd7eed1b..43d0d8bd422 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ComponentCell.js
@@ -19,7 +19,7 @@
*/
import React from 'react';
import classNames from 'classnames';
-import QualifierIcon from '../../../../components/shared/qualifier-icon';
+import QualifierIcon from '../../../../components/shared/QualifierIcon';
import { splitPath } from '../../../../helpers/path';
import { getComponentUrl } from '../../../../helpers/urls';
diff --git a/server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js b/server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js
deleted file mode 100644
index 1c9b447b231..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/BulkChangeForm.js
+++ /dev/null
@@ -1,303 +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 { debounce, sortBy } from 'lodash';
-import ModalForm from '../../components/common/modal-form';
-import Template from './templates/BulkChangeForm.hbs';
-import getCurrentUserFromStore from '../../app/utils/getCurrentUserFromStore';
-import { searchIssues, searchIssueTags, bulkChangeIssues } from '../../api/issues';
-import { searchUsers } from '../../api/users';
-import { searchMembers } from '../../api/organizations';
-import { translate, translateWithParameters } from '../../helpers/l10n';
-
-const LIMIT = 500;
-const INPUT_WIDTH = '250px';
-const MINIMUM_QUERY_LENGTH = 2;
-const UNASSIGNED = '<UNASSIGNED>';
-
-type Issue = {
- actions?: Array<string>,
- assignee: string | null,
- transitions?: Array<string>
-};
-
-const hasAction = (action: string) =>
- (issue: Issue) => issue.actions && issue.actions.includes(action);
-
-export default ModalForm.extend({
- template: Template,
-
- initialize() {
- this.issues = null;
- this.paging = null;
- this.tags = null;
- this.loadIssues();
- this.loadTags();
- },
-
- loadIssues() {
- const { query } = this.options;
- searchIssues({
- ...query,
- additionalFields: 'actions,transitions',
- ps: LIMIT
- }).then(r => {
- this.issues = r.issues;
- this.paging = r.paging;
- this.render();
- });
- },
-
- loadTags() {
- searchIssueTags().then(r => {
- this.tags = r.tags;
- this.render();
- });
- },
-
- assigneeSearch(defaultOptions) {
- const { context } = this.options;
- return debounce(
- query => {
- if (query.term.length === 0) {
- query.callback({ results: defaultOptions });
- } else if (query.term.length >= MINIMUM_QUERY_LENGTH) {
- const onSuccess = r => {
- query.callback({
- results: r.users.map(user => ({
- id: user.login,
- text: `${user.name} (${user.login})`
- }))
- });
- };
- if (context.isContext) {
- searchMembers({ organization: context.organization, q: query.term }).then(onSuccess);
- } else {
- searchUsers(query.term).then(onSuccess);
- }
- }
- },
- 250
- );
- },
-
- prepareAssigneeSelect() {
- const input = this.$('#assignee');
- if (input.length) {
- const canBeAssignedToMe = this.issues && this.canBeAssignedToMe(this.issues);
- const currentUser = getCurrentUserFromStore();
- const canBeUnassigned = this.issues && this.canBeUnassigned(this.issues);
- const defaultOptions = [];
- if (canBeAssignedToMe && currentUser.isLoggedIn) {
- defaultOptions.push({
- id: currentUser.login,
- text: `${currentUser.name} (${currentUser.login})`
- });
- }
- if (canBeUnassigned) {
- defaultOptions.push({ id: UNASSIGNED, text: translate('unassigned') });
- }
-
- input.select2({
- allowClear: false,
- placeholder: translate('search_verb'),
- width: INPUT_WIDTH,
- formatNoMatches: () => translate('select2.noMatches'),
- formatSearching: () => translate('select2.searching'),
- formatInputTooShort: () =>
- translateWithParameters('select2.tooShort', MINIMUM_QUERY_LENGTH),
- query: this.assigneeSearch(defaultOptions)
- });
-
- input.on('change', () => this.$('#assign-action').prop('checked', true));
- }
- },
-
- prepareTypeSelect() {
- this.$('#type')
- .select2({
- minimumResultsForSearch: 999,
- width: INPUT_WIDTH
- })
- .on('change', () => this.$('#set-type-action').prop('checked', true));
- },
-
- prepareSeveritySelect() {
- const format = state =>
- state.id
- ? `<i class="icon-severity-${state.id.toLowerCase()}"></i> ${state.text}`
- : state.text;
- this.$('#severity')
- .select2({
- minimumResultsForSearch: 999,
- width: INPUT_WIDTH,
- formatResult: format,
- formatSelection: format
- })
- .on('change', () => this.$('#set-severity-action').prop('checked', true));
- },
-
- prepareTagsInput() {
- this.$('#add_tags')
- .select2({
- width: INPUT_WIDTH,
- tags: this.tags
- })
- .on('change', () => this.$('#add-tags-action').prop('checked', true));
-
- this.$('#remove_tags')
- .select2({
- width: INPUT_WIDTH,
- tags: this.tags
- })
- .on('change', () => this.$('#remove-tags-action').prop('checked', true));
- },
-
- onRender() {
- ModalForm.prototype.onRender.apply(this, arguments);
- this.prepareAssigneeSelect();
- this.prepareTypeSelect();
- this.prepareSeveritySelect();
- this.prepareTagsInput();
- },
-
- onFormSubmit() {
- ModalForm.prototype.onFormSubmit.apply(this, arguments);
- const query = {};
-
- const assignee = this.$('#assignee').val();
- if (this.$('#assign-action').is(':checked') && assignee != null) {
- query['assign'] = assignee === UNASSIGNED ? '' : assignee;
- }
-
- const type = this.$('#type').val();
- if (this.$('#set-type-action').is(':checked') && type) {
- query['set_type'] = type;
- }
-
- const severity = this.$('#severity').val();
- if (this.$('#set-severity-action').is(':checked') && severity) {
- query['set_severity'] = severity;
- }
-
- const addedTags = this.$('#add_tags').val();
- if (this.$('#add-tags-action').is(':checked') && addedTags) {
- query['add_tags'] = addedTags;
- }
-
- const removedTags = this.$('#remove_tags').val();
- if (this.$('#remove-tags-action').is(':checked') && removedTags) {
- query['remove_tags'] = removedTags;
- }
-
- const transition = this.$('[name="do_transition.transition"]:checked').val();
- if (transition) {
- query['do_transition'] = transition;
- }
-
- const comment = this.$('#comment').val();
- if (comment) {
- query['comment'] = comment;
- }
-
- const sendNotifications = this.$('#send-notifications').is(':checked');
- if (sendNotifications) {
- query['sendNotifications'] = sendNotifications;
- }
-
- this.disableForm();
- this.showSpinner();
-
- const issueKeys = this.issues.map(issue => issue.key);
- bulkChangeIssues(issueKeys, query).then(
- () => {
- this.destroy();
- this.options.onChange();
- },
- (e: Object) => {
- this.enableForm();
- this.hideSpinner();
- e.response.json().then(r => this.showErrors(r.errors, r.warnings));
- }
- );
- },
-
- canBeAssigned(issues: Array<Issue>) {
- return issues.filter(hasAction('assign')).length;
- },
-
- canBeAssignedToMe(issues: Array<Issue>) {
- return issues.filter(hasAction('assign_to_me')).length;
- },
-
- canBeUnassigned(issues: Array<Issue>) {
- return issues.filter(issue => issue.assignee).length;
- },
-
- canChangeType(issues: Array<Issue>) {
- return issues.filter(hasAction('set_type')).length;
- },
-
- canChangeSeverity(issues: Array<Issue>) {
- return issues.filter(hasAction('set_severity')).length;
- },
-
- canChangeTags(issues: Array<Issue>) {
- return issues.filter(hasAction('set_tags')).length;
- },
-
- canBeCommented(issues: Array<Issue>) {
- return issues.filter(hasAction('comment')).length;
- },
-
- availableTransitions(issues: Array<Issue>) {
- const transitions = {};
- issues.forEach(issue => {
- if (issue.transitions) {
- issue.transitions.forEach(t => {
- if (transitions[t] != null) {
- transitions[t]++;
- } else {
- transitions[t] = 1;
- }
- });
- }
- });
- return sortBy(Object.keys(transitions)).map(transition => ({
- transition,
- count: transitions[transition]
- }));
- },
-
- serializeData() {
- return {
- ...ModalForm.prototype.serializeData.apply(this, arguments),
- isLoaded: this.issues != null && this.tags != null,
- issues: this.issues,
- limitReached: this.paging && this.paging.total > LIMIT,
- canBeAssigned: this.issues && this.canBeAssigned(this.issues),
- canChangeType: this.issues && this.canChangeType(this.issues),
- canChangeSeverity: this.issues && this.canChangeSeverity(this.issues),
- canChangeTags: this.issues && this.canChangeTags(this.issues),
- canBeCommented: this.issues && this.canBeCommented(this.issues),
- availableTransitions: this.issues && this.availableTransitions(this.issues)
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/HeaderView.js b/server/sonar-web/src/main/js/apps/issues/HeaderView.js
deleted file mode 100644
index 596996f9d5c..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/HeaderView.js
+++ /dev/null
@@ -1,60 +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 Marionette from 'backbone.marionette';
-import Template from './templates/facets/issues-my-issues-facet.hbs';
-
-export default Marionette.ItemView.extend({
- template: Template,
- className: 'issues-header-inner',
-
- events: {
- 'change [name="issues-page-my"]': 'onMyIssuesChange'
- },
-
- initialize() {
- this.listenTo(this.options.app.state, 'change:query', this.render);
- },
-
- onMyIssuesChange() {
- const mode = this.$('[name="issues-page-my"]:checked').val();
- if (mode === 'my') {
- this.options.app.state.updateFilter({
- assigned_to_me: 'true',
- assignees: null,
- assigned: null
- });
- } else {
- this.options.app.state.updateFilter({
- assigned_to_me: null,
- assignees: null,
- assigned: null
- });
- }
- },
- serializeData() {
- const me = !!this.options.app.state.get('query').assigned_to_me;
- return {
- ...Marionette.ItemView.prototype.serializeData.apply(this, arguments),
- me,
- isContext: this.options.app.state.get('isContext'),
- user: this.options.app.state.get('user')
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js b/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js
deleted file mode 100644
index a1dcb64d7dc..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js
+++ /dev/null
@@ -1,132 +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 React from 'react';
-import { render, unmountComponentAtNode } from 'react-dom';
-import Marionette from 'backbone.marionette';
-import SourceViewer from '../../../components/SourceViewer/SourceViewer';
-import WithStore from '../../../components/shared/WithStore';
-
-export default Marionette.ItemView.extend({
- template() {
- return '<div></div>';
- },
-
- initialize(options) {
- this.handleLoadIssues = this.handleLoadIssues.bind(this);
- this.scrollToBaseIssue = this.scrollToBaseIssue.bind(this);
- this.selectIssue = this.selectIssue.bind(this);
- this.listenTo(options.app.state, 'change:selectedIndex', this.select);
- },
-
- onRender() {
- this.showViewer();
- },
-
- onDestroy() {
- this.unbindShortcuts();
- unmountComponentAtNode(this.el);
- },
-
- handleLoadIssues(component: string) {
- // TODO fromLine: number, toLine: number
- const issues = this.options.app.list.toJSON().filter(issue => issue.componentKey === component);
- return Promise.resolve(issues);
- },
-
- showViewer(onLoaded) {
- if (!this.baseIssue) {
- return;
- }
-
- const componentKey = this.baseIssue.get('component');
-
- render(
- <WithStore>
- <SourceViewer
- aroundLine={this.baseIssue.get('line')}
- component={componentKey}
- displayAllIssues={true}
- loadIssues={this.handleLoadIssues}
- onLoaded={onLoaded}
- onIssueSelect={this.selectIssue}
- selectedIssue={this.baseIssue.get('key')}
- />
- </WithStore>,
- this.el
- );
- },
-
- openFileByIssue(issue) {
- this.baseIssue = issue;
- this.selectedIssue = issue.get('key');
- this.showViewer(this.scrollToBaseIssue);
- this.bindShortcuts();
- },
-
- bindShortcuts() {
- key('up', 'componentViewer', () => {
- this.options.app.controller.selectPrev();
- return false;
- });
- key('down', 'componentViewer', () => {
- this.options.app.controller.selectNext();
- return false;
- });
- key('left,backspace', 'componentViewer', () => {
- this.options.app.controller.closeComponentViewer();
- return false;
- });
- },
-
- unbindShortcuts() {
- key.deleteScope('componentViewer');
- },
-
- select() {
- const selected = this.options.app.state.get('selectedIndex');
- const selectedIssue = this.options.app.list.at(selected);
-
- if (selectedIssue.get('component') === this.baseIssue.get('component')) {
- this.baseIssue = selectedIssue;
- this.showViewer(this.scrollToBaseIssue);
- this.scrollToBaseIssue();
- } else {
- this.options.app.controller.showComponentViewer(selectedIssue);
- }
- },
-
- scrollToLine(line) {
- const row = this.$(`[data-line-number=${line}]`);
- const topOffset = $(window).height() / 2 - 60;
- const goal = row.length > 0 ? row.offset().top - topOffset : 0;
- $(window).scrollTop(goal);
- },
-
- selectIssue(issueKey) {
- const issue = this.options.app.list.find(model => model.get('key') === issueKey);
- const index = this.options.app.list.indexOf(issue);
- this.options.app.state.set({ selectedIndex: index });
- },
-
- scrollToBaseIssue() {
- this.scrollToLine(this.baseIssue.get('line'));
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js
new file mode 100644
index 00000000000..73574c611b5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/App.js
@@ -0,0 +1,649 @@
+/*
+ * 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 Helmet from 'react-helmet';
+import key from 'keymaster';
+import { keyBy, without } from 'lodash';
+import HeaderPanel from './HeaderPanel';
+import PageActions from './PageActions';
+import FiltersHeader from './FiltersHeader';
+import MyIssuesFilter from './MyIssuesFilter';
+import Sidebar from '../sidebar/Sidebar';
+import IssuesList from './IssuesList';
+import ComponentBreadcrumbs from './ComponentBreadcrumbs';
+import IssuesSourceViewer from './IssuesSourceViewer';
+import BulkChangeModal from './BulkChangeModal';
+import {
+ parseQuery,
+ areMyIssuesSelected,
+ areQueriesEqual,
+ getOpen,
+ serializeQuery,
+ parseFacets
+} from '../utils';
+import type {
+ Query,
+ Paging,
+ Facet,
+ ReferencedComponent,
+ ReferencedUser,
+ ReferencedLanguage,
+ Component,
+ CurrentUser
+} from '../utils';
+import ListFooter from '../../../components/controls/ListFooter';
+import EmptySearch from '../../../components/common/EmptySearch';
+import Page from '../../../components/layout/Page';
+import PageMain from '../../../components/layout/PageMain';
+import PageMainInner from '../../../components/layout/PageMainInner';
+import PageSide from '../../../components/layout/PageSide';
+import PageFilters from '../../../components/layout/PageFilters';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { scrollToElement } from '../../../helpers/scrolling';
+import type { Issue } from '../../../components/issue/types';
+
+type Props = {
+ component?: Component,
+ currentUser: CurrentUser,
+ fetchIssues: () => Promise<*>,
+ location: { pathname: string, query: { [string]: string } },
+ onRequestFail: (Error) => void,
+ router: { push: () => void, replace: () => void }
+};
+
+type State = {
+ bulkChange: 'all' | 'selected' | null,
+ checked: Array<string>,
+ facets: { [string]: Facet },
+ issues: Array<Issue>,
+ loading: boolean,
+ myIssues: boolean,
+ openFacets: { [string]: boolean },
+ paging?: Paging,
+ query: Query,
+ referencedComponents: { [string]: ReferencedComponent },
+ referencedLanguages: { [string]: ReferencedLanguage },
+ referencedRules: { [string]: { name: string } },
+ referencedUsers: { [string]: ReferencedUser },
+ selected?: string
+};
+
+const DEFAULT_QUERY = { resolved: 'false' };
+
+export default class App extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ bulkChange: null,
+ checked: [],
+ facets: {},
+ issues: [],
+ loading: true,
+ myIssues: areMyIssuesSelected(props.location.query),
+ openFacets: { resolutions: true, types: true },
+ query: parseQuery(props.location.query),
+ referencedComponents: {},
+ referencedLanguages: {},
+ referencedRules: {},
+ referencedUsers: {},
+ selected: getOpen(props.location.query)
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+
+ const footer = document.getElementById('footer');
+ if (footer) {
+ footer.classList.add('search-navigator-footer');
+ }
+
+ this.attachShortcuts();
+ this.fetchFirstIssues();
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ const open = getOpen(nextProps.location.query);
+ if (open != null && open !== this.state.selected) {
+ this.setState({ selected: open });
+ }
+ this.setState({
+ myIssues: areMyIssuesSelected(nextProps.location.query),
+ query: parseQuery(nextProps.location.query)
+ });
+ }
+
+ componentDidUpdate(prevProps: Props, prevState: State) {
+ const { query } = this.props.location;
+ const { query: prevQuery } = prevProps.location;
+ if (
+ !areQueriesEqual(prevQuery, query) ||
+ areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query)
+ ) {
+ this.fetchFirstIssues();
+ } else if (prevState.selected !== this.state.selected) {
+ const open = getOpen(query);
+ if (!open) {
+ this.scrollToSelectedIssue();
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ this.detachShortcuts();
+
+ const footer = document.getElementById('footer');
+ if (footer) {
+ footer.classList.remove('search-navigator-footer');
+ }
+
+ this.mounted = false;
+ }
+
+ attachShortcuts() {
+ key.setScope('issues');
+ key('up', 'issues', () => {
+ this.selectPreviousIssue();
+ return false;
+ });
+ key('down', 'issues', () => {
+ this.selectNextIssue();
+ return false;
+ });
+ key('right', 'issues', () => {
+ this.openSelectedIssue();
+ return false;
+ });
+ key('left', 'issues', () => {
+ this.closeIssue();
+ return false;
+ });
+ }
+
+ detachShortcuts() {
+ key.deleteScope('issues');
+ }
+
+ getSelectedIndex(): ?number {
+ const { issues, selected } = this.state;
+ const index = issues.findIndex(issue => issue.key === selected);
+ return index !== -1 ? index : null;
+ }
+
+ selectNextIssue = () => {
+ const { issues } = this.state;
+ const selectedIndex = this.getSelectedIndex();
+ if (issues != null && selectedIndex != null && selectedIndex < issues.length - 1) {
+ if (getOpen(this.props.location.query)) {
+ this.openIssue(issues[selectedIndex + 1].key);
+ } else {
+ this.setState({ selected: issues[selectedIndex + 1].key });
+ }
+ }
+ };
+
+ selectPreviousIssue = () => {
+ const { issues } = this.state;
+ const selectedIndex = this.getSelectedIndex();
+ if (issues != null && selectedIndex != null && selectedIndex > 0) {
+ if (getOpen(this.props.location.query)) {
+ this.openIssue(issues[selectedIndex - 1].key);
+ } else {
+ this.setState({ selected: issues[selectedIndex - 1].key });
+ }
+ }
+ };
+
+ openIssue = (issue: string) => {
+ const path = {
+ pathname: this.props.location.pathname,
+ query: {
+ ...serializeQuery(this.state.query),
+ id: this.props.component && this.props.component.key,
+ myIssues: this.state.myIssues ? 'true' : undefined,
+ open: issue
+ }
+ };
+ const open = getOpen(this.props.location.query);
+ if (open) {
+ this.props.router.replace(path);
+ } else {
+ this.props.router.push(path);
+ }
+ };
+
+ closeIssue = () => {
+ if (this.state.query) {
+ this.props.router.push({
+ pathname: this.props.location.pathname,
+ query: {
+ ...serializeQuery(this.state.query),
+ id: this.props.component && this.props.component.key,
+ myIssues: this.state.myIssues ? 'true' : undefined,
+ open: undefined
+ }
+ });
+ }
+ };
+
+ openSelectedIssue = () => {
+ const { selected } = this.state;
+ if (selected) {
+ this.openIssue(selected);
+ }
+ };
+
+ scrollToSelectedIssue = () => {
+ const { selected } = this.state;
+ if (selected) {
+ const element = document.querySelector(`[data-issue="${selected}"]`);
+ if (element) {
+ scrollToElement(element, 150, 100);
+ }
+ }
+ };
+
+ fetchIssues = (additional?: {}, requestFacets?: boolean = false): Promise<*> => {
+ const { component } = this.props;
+ const { myIssues, query } = this.state;
+
+ const parameters = {
+ componentKeys: component && component.key,
+ ...serializeQuery(query),
+ s: 'FILE_LINE',
+ ps: 25,
+ facets: requestFacets
+ ? [
+ 'assignees',
+ 'authors',
+ 'createdAt',
+ 'directories',
+ 'fileUuids',
+ 'languages',
+ 'moduleUuids',
+ 'projectUuids',
+ 'resolutions',
+ 'rules',
+ 'severities',
+ 'statuses',
+ 'tags',
+ 'types'
+ ].join()
+ : undefined,
+ ...additional
+ };
+
+ if (myIssues) {
+ Object.assign(parameters, { assignees: '__me__' });
+ }
+
+ return this.props.fetchIssues(parameters);
+ };
+
+ fetchFirstIssues() {
+ this.setState({ loading: true });
+ this.fetchIssues({}, true).then(({ facets, issues, paging, ...other }) => {
+ if (this.mounted) {
+ const open = getOpen(this.props.location.query);
+ this.setState({
+ facets: parseFacets(facets),
+ loading: false,
+ issues,
+ paging,
+ referencedComponents: keyBy(other.components, 'uuid'),
+ referencedLanguages: keyBy(other.languages, 'key'),
+ referencedRules: keyBy(other.rules, 'key'),
+ referencedUsers: keyBy(other.users, 'login'),
+ selected: issues.length > 0
+ ? issues.find(issue => issue.key === open) != null ? open : issues[0].key
+ : undefined
+ });
+ }
+ });
+ }
+
+ fetchIssuesPage = (p: number): Promise<*> => {
+ return this.fetchIssues({ p });
+ };
+
+ fetchIssuesUntil = (p: number, done: (Array<Issue>, Paging) => boolean) => {
+ return this.fetchIssuesPage(p).then(response => {
+ const { issues, paging } = response;
+
+ return done(issues, paging)
+ ? { issues, paging }
+ : this.fetchIssuesUntil(p + 1, done).then(nextResponse => {
+ return {
+ issues: [...issues, ...nextResponse.issues],
+ paging: nextResponse.paging
+ };
+ });
+ });
+ };
+
+ fetchMoreIssues = () => {
+ const { paging } = this.state;
+
+ if (!paging) {
+ return;
+ }
+
+ const p = paging.pageIndex + 1;
+
+ this.setState({ loading: true });
+ this.fetchIssuesPage(p).then(response => {
+ if (this.mounted) {
+ this.setState(state => ({
+ loading: false,
+ issues: [...state.issues, ...response.issues],
+ paging: response.paging
+ }));
+ }
+ });
+ };
+
+ fetchIssuesForComponent = (): Promise<Array<Issue>> => {
+ const { issues, paging } = this.state;
+
+ const open = getOpen(this.props.location.query);
+ const openIssue = issues.find(issue => issue.key === open);
+
+ if (!openIssue || !paging) {
+ return Promise.reject();
+ }
+
+ const isSameComponent = (issue: Issue): boolean => issue.component === openIssue.component;
+
+ const done = (issues: Array<Issue>, paging: Paging): boolean =>
+ paging.total <= paging.pageIndex * paging.pageSize ||
+ issues[issues.length - 1].component !== openIssue.component;
+
+ if (done(issues, paging)) {
+ return Promise.resolve(issues.filter(isSameComponent));
+ }
+
+ this.setState({ loading: true });
+ return this.fetchIssuesUntil(paging.pageIndex + 1, done).then(response => {
+ const nextIssues = [...issues, ...response.issues];
+
+ this.setState({
+ issues: nextIssues,
+ loading: false,
+ paging: response.paging
+ });
+ return nextIssues.filter(isSameComponent);
+ });
+ };
+
+ isFiltered = () => {
+ const serialized = serializeQuery(this.state.query);
+ return !areQueriesEqual(serialized, DEFAULT_QUERY);
+ };
+
+ getCheckedIssues = () => {
+ const issues = this.state.checked.map(checked =>
+ this.state.issues.find(issue => issue.key === checked));
+ const paging = { pageIndex: 1, pageSize: issues.length, total: issues.length };
+ return Promise.resolve({ issues, paging });
+ };
+
+ handleFilterChange = (changes: {}) => {
+ this.props.router.push({
+ pathname: this.props.location.pathname,
+ query: {
+ ...serializeQuery({ ...this.state.query, ...changes }),
+ id: this.props.component && this.props.component.key,
+ myIssues: this.state.myIssues ? 'true' : undefined
+ }
+ });
+ };
+
+ handleMyIssuesChange = (myIssues: boolean) => {
+ this.closeFacet('assignees');
+ this.props.router.push({
+ pathname: this.props.location.pathname,
+ query: {
+ ...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }),
+ id: this.props.component && this.props.component.key,
+ myIssues: myIssues ? 'true' : undefined
+ }
+ });
+ };
+
+ closeFacet = (property: string) => {
+ this.setState(state => ({
+ openFacets: { ...state.openFacets, [property]: false }
+ }));
+ };
+
+ handleFacetToggle = (property: string) => {
+ this.setState(state => ({
+ openFacets: { ...state.openFacets, [property]: !state.openFacets[property] }
+ }));
+ };
+
+ handleReset = () => {
+ this.props.router.push({
+ pathname: this.props.location.pathname,
+ query: {
+ ...DEFAULT_QUERY,
+ id: this.props.component && this.props.component.key,
+ myIssues: this.state.myIssues ? 'true' : undefined
+ }
+ });
+ };
+
+ handleIssueCheck = (issue: string) => {
+ this.setState(state => ({
+ checked: state.checked.includes(issue)
+ ? without(state.checked, issue)
+ : [...state.checked, issue]
+ }));
+ };
+
+ handleIssueChange = (issue: Issue) => {
+ this.setState(state => ({
+ issues: state.issues.map(candidate => candidate.key === issue.key ? issue : candidate)
+ }));
+ };
+
+ openBulkChange = (mode: 'all' | 'selected') => {
+ this.setState({ bulkChange: mode });
+ key.setScope('issues-bulk-change');
+ };
+
+ closeBulkChange = () => {
+ key.setScope('issues');
+ this.setState({ bulkChange: null });
+ };
+
+ handleBulkChangeClick = (e: Event & { target: HTMLElement }) => {
+ e.preventDefault();
+ e.target.blur();
+ this.openBulkChange('all');
+ };
+
+ handleBulkChangeSelectedClick = (e: Event & { target: HTMLElement }) => {
+ e.preventDefault();
+ e.target.blur();
+ this.openBulkChange('selected');
+ };
+
+ handleBulkChangeDone = () => {
+ this.fetchFirstIssues();
+ this.closeBulkChange();
+ };
+
+ renderBulkChange(openIssue?: Issue) {
+ const { component, currentUser } = this.props;
+ const { bulkChange, checked, paging } = this.state;
+
+ if (!currentUser.isLoggedIn || openIssue != null) {
+ return null;
+ }
+
+ return (
+ <div className="pull-left">
+ {checked.length > 0
+ ? <div className="dropdown">
+ <button id="issues-bulk-change" data-toggle="dropdown">
+ {translate('bulk_change')}
+ <i className="icon-dropdown little-spacer-left" />
+ </button>
+ <ul className="dropdown-menu">
+ <li>
+ <a href="#" onClick={this.handleBulkChangeClick}>
+ {translateWithParameters('issues.bulk_change', paging ? paging.total : 0)}
+ </a>
+ </li>
+ <li>
+ <a href="#" onClick={this.handleBulkChangeSelectedClick}>
+ {translateWithParameters('issues.bulk_change_selected', checked.length)}
+ </a>
+ </li>
+ </ul>
+ </div>
+ : <button id="issues-bulk-change" onClick={this.handleBulkChangeClick}>
+ {translate('bulk_change')}
+ </button>}
+ {bulkChange != null &&
+ <BulkChangeModal
+ component={component}
+ currentUser={currentUser}
+ fetchIssues={bulkChange === 'all' ? this.fetchIssues : this.getCheckedIssues}
+ onClose={this.closeBulkChange}
+ onDone={this.handleBulkChangeDone}
+ onRequestFail={this.props.onRequestFail}
+ />}
+ </div>
+ );
+ }
+
+ renderList(openIssue?: Issue) {
+ const { component, currentUser } = this.props;
+ const { issues, paging } = this.state;
+ const selectedIndex = this.getSelectedIndex();
+ const selectedIssue = selectedIndex != null ? issues[selectedIndex] : null;
+
+ if (paging == null) {
+ return null;
+ }
+
+ return (
+ <div className={openIssue != null ? 'hidden' : undefined}>
+ {paging.total > 0 &&
+ <IssuesList
+ checked={this.state.checked}
+ component={component}
+ issues={issues}
+ onFilterChange={this.handleFilterChange}
+ onIssueChange={this.handleIssueChange}
+ onIssueCheck={currentUser.isLoggedIn ? this.handleIssueCheck : undefined}
+ onIssueClick={this.openIssue}
+ selectedIssue={selectedIssue}
+ />}
+
+ {paging.total > 0 &&
+ <ListFooter total={paging.total} count={issues.length} loadMore={this.fetchMoreIssues} />}
+
+ {paging.total === 0 && <EmptySearch />}
+ </div>
+ );
+ }
+
+ render() {
+ const { component, currentUser } = this.props;
+ const { issues, paging, query } = this.state;
+
+ const open = getOpen(this.props.location.query);
+ const openIssue = issues.find(issue => issue.key === open);
+
+ const selectedIndex = this.getSelectedIndex();
+
+ const top = component ? 95 : 30;
+
+ return (
+ <Page className="issues" id="issues-page">
+ <Helmet title={translate('issues.page')} titleTemplate="%s - SonarQube" />
+
+ <PageSide top={top}>
+ <PageFilters>
+ {currentUser.isLoggedIn &&
+ <MyIssuesFilter
+ myIssues={this.state.myIssues}
+ onMyIssuesChange={this.handleMyIssuesChange}
+ />}
+ <FiltersHeader displayReset={this.isFiltered()} onReset={this.handleReset} />
+ <Sidebar
+ component={component}
+ facets={this.state.facets}
+ myIssues={this.state.myIssues}
+ onFacetToggle={this.handleFacetToggle}
+ onFilterChange={this.handleFilterChange}
+ openFacets={this.state.openFacets}
+ query={query}
+ referencedComponents={this.state.referencedComponents}
+ referencedLanguages={this.state.referencedLanguages}
+ referencedRules={this.state.referencedRules}
+ referencedUsers={this.state.referencedUsers}
+ />
+ </PageFilters>
+ </PageSide>
+
+ <PageMain>
+ <HeaderPanel border={true} top={top}>
+ <PageMainInner>
+ {this.renderBulkChange(openIssue)}
+ {openIssue != null &&
+ <div className="pull-left">
+ <ComponentBreadcrumbs component={component} issue={openIssue} />
+ </div>}
+ <PageActions
+ loading={this.state.loading}
+ openIssue={openIssue}
+ paging={paging}
+ selectedIndex={selectedIndex}
+ />
+ </PageMainInner>
+ </HeaderPanel>
+
+ <PageMainInner>
+ <div>
+ {openIssue != null &&
+ <IssuesSourceViewer
+ openIssue={openIssue}
+ loadIssues={this.fetchIssuesForComponent}
+ onIssueChange={this.handleIssueChange}
+ onIssueSelect={this.openIssue}
+ />}
+
+ {this.renderList(openIssue)}
+ </div>
+ </PageMainInner>
+ </PageMain>
+ </Page>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js
index 061b3e8f8c6..c605961dcad 100644
--- a/server/sonar-web/src/main/js/apps/component-issues/components/ComponentIssuesAppContainer.js
+++ b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js
@@ -17,36 +17,38 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
+// @flow
import { connect } from 'react-redux';
-import init from '../init';
+import { withRouter } from 'react-router';
+import type { Dispatch } from 'redux';
+import App from './App';
+import { onFail } from '../../../store/rootActions';
import { getComponent, getCurrentUser } from '../../../store/rootReducer';
+import { searchIssues } from '../../../api/issues';
+import { parseIssueFromResponse } from '../../../helpers/issues';
-class ComponentIssuesAppContainer extends React.Component {
- componentDidMount() {
- this.stop = init(this.refs.container, this.props.component, this.props.currentUser);
- }
-
- componentWillUnmount() {
- this.stop();
- }
-
- render() {
- // placing container inside div is required,
- // because when backbone.marionette's layout is destroyed,
- // it also destroys the root element,
- // but react wants it to be there to unmount it
- return (
- <div>
- <div ref="container" />
- </div>
- );
- }
-}
+type Query = { [string]: string };
const mapStateToProps = (state, ownProps) => ({
- component: getComponent(state, ownProps.location.query.id),
+ component: ownProps.location.query.id
+ ? getComponent(state, ownProps.location.query.id)
+ : undefined,
currentUser: getCurrentUser(state)
});
-export default connect(mapStateToProps)(ComponentIssuesAppContainer);
+const fetchIssues = (query: Query) =>
+ (dispatch: Dispatch<*>) =>
+ searchIssues({ ...query, additionalFields: '_all' }).then(
+ response => {
+ const parsedIssues = response.issues.map(issue =>
+ parseIssueFromResponse(issue, response.components, response.users, response.rules));
+ return { ...response, issues: parsedIssues };
+ },
+ onFail(dispatch)
+ );
+
+const onRequestFail = (error: Error) => (dispatch: Dispatch<*>) => onFail(dispatch)(error);
+
+const mapDispatchToProps = { fetchIssues, onRequestFail };
+
+export default connect(mapStateToProps, mapDispatchToProps)(withRouter(App));
diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js
new file mode 100644
index 00000000000..3b7d454aa72
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js
@@ -0,0 +1,522 @@
+/*
+ * 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 Modal from 'react-modal';
+import Select from 'react-select';
+import { css } from 'glamor';
+import { pickBy, sortBy } from 'lodash';
+import SearchSelect from './SearchSelect';
+import Checkbox from '../../../components/controls/Checkbox';
+import Tooltip from '../../../components/controls/Tooltip';
+import MarkdownTips from '../../../components/common/MarkdownTips';
+import SeverityHelper from '../../../components/shared/SeverityHelper';
+import Avatar from '../../../components/ui/Avatar';
+import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
+import { searchIssueTags, bulkChangeIssues } from '../../../api/issues';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { searchAssignees } from '../utils';
+import type { Paging, Component, CurrentUser } from '../utils';
+import type { Issue } from '../../../components/issue/types';
+
+type Props = {|
+ component?: Component,
+ currentUser: CurrentUser,
+ fetchIssues: ({}) => Promise<*>,
+ onClose: () => void,
+ onDone: () => void,
+ onRequestFail: (Error) => void
+|};
+
+type State = {|
+ issues: Array<Issue>,
+ // used for initial loading of issues
+ loading: boolean,
+ paging?: Paging,
+ // used when submitting a form
+ submitting: boolean,
+ tags?: Array<string>,
+
+ // form fields
+ addTags?: Array<string>,
+ assignee?: string,
+ comment?: string,
+ notifications?: boolean,
+ removeTags?: Array<string>,
+ severity?: string,
+ transition?: string,
+ type?: string
+|};
+
+const hasAction = (action: string) =>
+ (issue: Issue): boolean => issue.actions && issue.actions.includes(action);
+
+export default class BulkChangeModal extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = { issues: [], loading: true, submitting: false };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ Promise.all([this.loadIssues(), searchIssueTags()]).then(([issues, tags]) => {
+ if (this.mounted) {
+ this.setState({
+ issues: issues.issues,
+ loading: false,
+ paging: issues.paging,
+ tags
+ });
+ }
+ });
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleCloseClick = (e: Event & { target: HTMLElement }) => {
+ e.preventDefault();
+ e.target.blur();
+ this.props.onClose();
+ };
+
+ loadIssues = () => {
+ return this.props.fetchIssues({ additionalFields: 'actions,transitions', ps: 250 });
+ };
+
+ handleAssigneeSearch = (query: string) => {
+ if (query.length > 1) {
+ return searchAssignees(query, this.props.component);
+ } else {
+ const { currentUser } = this.props;
+ const { issues } = this.state;
+ const options = [];
+
+ if (currentUser.isLoggedIn) {
+ const canBeAssignedToMe = issues.filter(
+ issue => issue.assignee !== currentUser.login
+ ).length > 0;
+ if (canBeAssignedToMe) {
+ options.push({
+ email: currentUser.email,
+ label: currentUser.name,
+ value: currentUser.login
+ });
+ }
+ }
+
+ const canBeUnassigned = issues.filter(issue => issue.assignee).length > 0;
+ if (canBeUnassigned) {
+ options.push({ label: translate('unassigned'), value: '' });
+ }
+
+ return Promise.resolve(options);
+ }
+ };
+
+ handleAssigneeSelect = (assignee: string) => {
+ this.setState({ assignee });
+ };
+
+ handleFieldCheck = (field: string) =>
+ (checked: boolean) => {
+ if (!checked) {
+ this.setState({ [field]: undefined });
+ } else if (field === 'notifications') {
+ this.setState({ [field]: true });
+ }
+ };
+
+ handleFieldChange = (field: string) =>
+ (event: { target: HTMLInputElement }) => {
+ this.setState({ [field]: event.target.value });
+ };
+
+ handleSelectFieldChange = (field: string) =>
+ ({ value }: { value: string }) => {
+ this.setState({ [field]: value });
+ };
+
+ handleMultiSelectFieldChange = (field: string) =>
+ (options: Array<{ value: string }>) => {
+ this.setState({ [field]: options.map(option => option.value) });
+ };
+
+ handleSubmit = (e: Event) => {
+ e.preventDefault();
+ const query = pickBy({
+ assign: this.state.assignee,
+ set_type: this.state.type,
+ set_severity: this.state.severity,
+ add_tags: this.state.addTags && this.state.addTags.join(),
+ remove_tags: this.state.removeTags && this.state.removeTags.join(),
+ do_transition: this.state.transition,
+ comment: this.state.comment,
+ sendNotifications: this.state.notifications
+ });
+ const issueKeys = this.state.issues.map(issue => issue.key);
+
+ this.setState({ submitting: true });
+ bulkChangeIssues(issueKeys, query).then(
+ () => {
+ this.setState({ submitting: false });
+ this.props.onDone();
+ },
+ (error: Error) => {
+ this.setState({ submitting: false });
+ this.props.onRequestFail(error);
+ }
+ );
+ };
+
+ getAvailableTransitions(issues: Array<Issue>): Array<{ transition: string, count: number }> {
+ const transitions = {};
+ issues.forEach(issue => {
+ if (issue.transitions) {
+ issue.transitions.forEach(t => {
+ if (transitions[t] != null) {
+ transitions[t]++;
+ } else {
+ transitions[t] = 1;
+ }
+ });
+ }
+ });
+ return sortBy(Object.keys(transitions)).map(transition => ({
+ transition,
+ count: transitions[transition]
+ }));
+ }
+
+ renderCancelButton = () => (
+ <a id="bulk-change-cancel" href="#" onClick={this.handleCloseClick}>
+ {translate('cancel')}
+ </a>
+ );
+
+ renderLoading = () => (
+ <div>
+ <div className="modal-head">
+ <h2>{translate('bulk_change')}</h2>
+ </div>
+ <div className="modal-body">
+ <div className="text-center">
+ <i className="spinner spinner-margin" />
+ </div>
+ </div>
+ <div className="modal-foot">
+ {this.renderCancelButton()}
+ </div>
+ </div>
+ );
+
+ renderCheckbox = (field: string) => (
+ <Checkbox
+ className={css({ paddingTop: 6, paddingRight: 8 })}
+ checked={this.state[field] != null}
+ onCheck={this.handleFieldCheck(field)}
+ />
+ );
+
+ renderAffected = (affected: number) => (
+ <div className="pull-right note">
+ ({translateWithParameters('issue_bulk_change.x_issues', affected)})
+ </div>
+ );
+
+ renderField = (field: string, label: string, affected: ?number, input: Object) => (
+ <div className="modal-field" id={`issues-bulk-change-${field}`}>
+ <label htmlFor={field}>{translate(label)}</label>
+ {this.renderCheckbox(field)}
+ {input}
+ {affected != null && this.renderAffected(affected)}
+ </div>
+ );
+
+ renderAssigneeOption = (option: { avatar?: string, email?: string, label: string }) => (
+ <span>
+ {(option.avatar != null || option.email != null) &&
+ <Avatar
+ className="little-spacer-right"
+ email={option.email}
+ hash={option.avatar}
+ size={16}
+ />}
+ {option.label}
+ </span>
+ );
+
+ renderAssigneeField = () => {
+ const affected: number = this.state.issues.filter(hasAction('assign')).length;
+
+ if (affected === 0) {
+ return null;
+ }
+
+ const input = (
+ <SearchSelect
+ onSearch={this.handleAssigneeSearch}
+ onSelect={this.handleAssigneeSelect}
+ minimumQueryLength={0}
+ renderOption={this.renderAssigneeOption}
+ resetOnBlur={false}
+ value={this.state.assignee}
+ />
+ );
+
+ return this.renderField('assignee', 'issue.assign.formlink', affected, input);
+ };
+
+ renderTypeField = () => {
+ const affected: number = this.state.issues.filter(hasAction('set_type')).length;
+
+ if (affected === 0) {
+ return null;
+ }
+
+ const types = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
+ const options = types.map(type => ({ label: translate('issue.type', type), value: type }));
+
+ const optionRenderer = (option: { label: string, value: string }) => (
+ <span>
+ <IssueTypeIcon className="little-spacer-right" query={option.value} />
+ {option.label}
+ </span>
+ );
+
+ const input = (
+ <Select
+ clearable={false}
+ id="type"
+ onChange={this.handleSelectFieldChange('type')}
+ options={options}
+ optionRenderer={optionRenderer}
+ searchable={false}
+ value={this.state.type}
+ valueRenderer={optionRenderer}
+ />
+ );
+
+ return this.renderField('type', 'issue.set_type', affected, input);
+ };
+
+ renderSeverityField = () => {
+ const affected: number = this.state.issues.filter(hasAction('set_severity')).length;
+
+ if (affected === 0) {
+ return null;
+ }
+
+ const severities = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];
+ const options = severities.map(severity => ({
+ label: translate('severity', severity),
+ value: severity
+ }));
+
+ const input = (
+ <Select
+ clearable={false}
+ id="severity"
+ onChange={this.handleSelectFieldChange('severity')}
+ options={options}
+ optionRenderer={option => <SeverityHelper severity={option.value} />}
+ searchable={false}
+ value={this.state.severity}
+ valueRenderer={option => <SeverityHelper severity={option.value} />}
+ />
+ );
+
+ return this.renderField('severity', 'issue.set_severity', affected, input);
+ };
+
+ renderAddTagsField = () => {
+ const affected: number = this.state.issues.filter(hasAction('set_tags')).length;
+
+ if (this.state.tags == null || affected === 0) {
+ return null;
+ }
+
+ const options = this.state.tags.map(tag => ({ label: tag, value: tag }));
+
+ const input = (
+ <Select
+ clearable={false}
+ id="add_tags"
+ multi={true}
+ onChange={this.handleMultiSelectFieldChange('addTags')}
+ options={options}
+ searchable={true}
+ value={this.state.addTags}
+ />
+ );
+
+ return this.renderField('addTags', 'issue.add_tags', affected, input);
+ };
+
+ renderRemoveTagsField = () => {
+ const affected: number = this.state.issues.filter(hasAction('set_tags')).length;
+
+ if (this.state.tags == null || affected === 0) {
+ return null;
+ }
+
+ const options = this.state.tags.map(tag => ({ label: tag, value: tag }));
+
+ const input = (
+ <Select
+ clearable={false}
+ id="remove_tags"
+ multi={true}
+ onChange={this.handleMultiSelectFieldChange('removeTags')}
+ options={options}
+ searchable={true}
+ value={this.state.removeTags}
+ />
+ );
+
+ return this.renderField('removeTags', 'issue.remove_tags', affected, input);
+ };
+
+ renderTransitionsField = () => {
+ const transitions = this.getAvailableTransitions(this.state.issues);
+
+ if (transitions.length === 0) {
+ return null;
+ }
+
+ return (
+ <div className="modal-field">
+ <label>{translate('issue.transition')}</label>
+ {transitions.map(transition => (
+ <span key={transition.transition}>
+ <input
+ checked={this.state.transition === transition.transition}
+ id={`transition-${transition.transition}`}
+ name="do_transition.transition"
+ onChange={this.handleFieldChange('transition')}
+ type="radio"
+ value={transition.transition}
+ />
+ <label
+ htmlFor={`transition-${transition.transition}`}
+ style={{ float: 'none', display: 'inline', left: 0, cursor: 'pointer' }}>
+ {translate('issue.transition', transition.transition)}
+ </label>
+ {this.renderAffected(transition.count)}
+ <br />
+ </span>
+ ))}
+ </div>
+ );
+ };
+
+ renderCommentField = () => {
+ const affected: number = this.state.issues.filter(hasAction('comment')).length;
+
+ if (affected === 0) {
+ return null;
+ }
+
+ return (
+ <div className="modal-field">
+ <label htmlFor="comment">
+ {translate('issue.comment.formlink')}
+ <Tooltip overlay={translate('issue_bulk_change.comment.help')}>
+ <i className="icon-help little-spacer-left" />
+ </Tooltip>
+ </label>
+ <div>
+ <textarea
+ id="comment"
+ onChange={this.handleFieldChange('comment')}
+ rows="4"
+ style={{ width: '100%' }}
+ value={this.state.comment || ''}
+ />
+ </div>
+ <div className="pull-right">
+ <MarkdownTips />
+ </div>
+ </div>
+ );
+ };
+
+ renderNotificationsField = () => (
+ <div className="modal-field">
+ <label htmlFor="send-notifications">{translate('issue.send_notifications')}</label>
+ {this.renderCheckbox('notifications')}
+ </div>
+ );
+
+ renderForm = () => {
+ const { issues, paging, submitting } = this.state;
+
+ const limitReached: boolean = paging != null &&
+ paging.total > paging.pageIndex * paging.pageSize;
+
+ return (
+ <form id="bulk-change-form" onSubmit={this.handleSubmit}>
+ <div className="modal-head">
+ <h2>{translateWithParameters('issue_bulk_change.form.title', issues.length)}</h2>
+ </div>
+
+ <div className="modal-body">
+ {limitReached &&
+ <div className="alert alert-warning">
+ {translateWithParameters('issue_bulk_change.max_issues_reached', issues.length)}
+ </div>}
+
+ {this.renderAssigneeField()}
+ {this.renderTypeField()}
+ {this.renderSeverityField()}
+ {this.renderAddTagsField()}
+ {this.renderRemoveTagsField()}
+ {this.renderTransitionsField()}
+ {this.renderCommentField()}
+ {this.renderNotificationsField()}
+ </div>
+
+ <div className="modal-foot">
+ {submitting && <i className="spinner spacer-right" />}
+ <button disabled={submitting} id="bulk-change-submit">{translate('apply')}</button>
+ {this.renderCancelButton()}
+ </div>
+ </form>
+ );
+ };
+
+ render() {
+ return (
+ <Modal
+ isOpen={true}
+ contentLabel="modal"
+ className="modal"
+ overlayClassName="modal-overlay"
+ onRequestClose={this.props.onClose}>
+ {this.state.loading ? this.renderLoading() : this.renderForm()}
+ </Modal>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js
new file mode 100644
index 00000000000..bc12da43bf1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { Link } from 'react-router';
+import Organization from '../../../components/shared/Organization';
+import { collapsePath, limitComponentName } from '../../../helpers/path';
+import { getProjectUrl } from '../../../helpers/urls';
+import type { Component } from '../utils';
+
+type Props = {
+ component?: Component,
+ issue: Object
+};
+
+export default class ComponentBreadcrumbs extends React.PureComponent {
+ props: Props;
+
+ render() {
+ const { component, issue } = this.props;
+
+ const displayOrganization = component == null || ['VW', 'SVW'].includes(component.qualifier);
+ const displayProject = component == null ||
+ !['TRK', 'BRC', 'DIR'].includes(component.qualifier);
+ const displaySubProject = component == null || !['BRC', 'DIR'].includes(component.qualifier);
+
+ return (
+ <div className="component-name">
+ {displayOrganization &&
+ <Organization linkClassName="link-no-underline" organizationKey={issue.organization} />}
+
+ {displayProject &&
+ <span>
+ <Link to={getProjectUrl(issue.project)} className="link-no-underline">
+ {limitComponentName(issue.projectName)}
+ </Link>
+ <span className="slash-separator" />
+ </span>}
+
+ {displaySubProject &&
+ issue.subProject != null &&
+ <span>
+ <Link to={getProjectUrl(issue.subProject)} className="link-no-underline">
+ {limitComponentName(issue.subProjectName)}
+ </Link>
+ <span className="slash-separator" />
+ </span>}
+
+ <Link to={getProjectUrl(issue.component)} className="link-no-underline">
+ {collapsePath(issue.componentLongName)}
+ </Link>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js b/server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js
index b1f3e70f3f0..9740f63e718 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssuesAppContainer.js
+++ b/server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js
@@ -17,39 +17,39 @@
* 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 { connect } from 'react-redux';
-import init from '../init';
-import { getCurrentUser } from '../../../store/rootReducer';
+import { css } from 'glamor';
+import { translate } from '../../../helpers/l10n';
-class IssuesAppContainer extends React.Component {
- static propTypes = {
- currentUser: React.PropTypes.any.isRequired
- };
+type Props = {
+ displayReset: boolean,
+ onReset: () => void
+};
- componentDidMount() {
- this.stop = init(this.refs.container, this.props.currentUser);
- }
+const styles = css({ marginBottom: 12, paddingBottom: 11, borderBottom: '1px solid #e6e6e6' });
- componentWillUnmount() {
- this.stop();
- }
+export default class FiltersHeader extends React.PureComponent {
+ props: Props;
+
+ handleResetClick = (e: Event & { currentTarget: HTMLElement }) => {
+ e.preventDefault();
+ e.currentTarget.blur();
+ this.props.onReset();
+ };
render() {
- // placing container inside div is required,
- // because when backbone.marionette's layout is destroyed,
- // it also destroys the root element,
- // but react wants it to be there to unmount it
return (
- <div>
- <div ref="container" />
+ <div className={styles}>
+ {this.props.displayReset &&
+ <div className={css({ float: 'right' })}>
+ <button className="button-red" onClick={this.handleResetClick}>
+ {translate('clear_all_filters')}
+ </button>
+ </div>}
+
+ <h3>{translate('filters')}</h3>
</div>
);
}
}
-
-const mapStateToProps = state => ({
- currentUser: getCurrentUser(state)
-});
-
-export default connect(mapStateToProps)(IssuesAppContainer);
diff --git a/server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js b/server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js
new file mode 100644
index 00000000000..2d8530dab51
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/HeaderPanel.js
@@ -0,0 +1,106 @@
+/*
+ * 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';
+import { clearfix } from 'glamor/utils';
+import { throttle } from 'lodash';
+
+type Props = {|
+ border: boolean,
+ children?: React.Element<*>,
+ top?: number
+|};
+
+type State = {
+ scrolled: boolean
+};
+
+export default class HeaderPanel extends React.PureComponent {
+ props: Props;
+ state: State;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = { scrolled: this.isScrolled() };
+ this.handleScroll = throttle(this.handleScroll, 50);
+ }
+
+ componentDidMount() {
+ if (this.props.top != null) {
+ window.addEventListener('scroll', this.handleScroll);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.props.top != null) {
+ window.removeEventListener('scroll', this.handleScroll);
+ }
+ }
+
+ isScrolled = () => window.scrollY > 10;
+
+ handleScroll = () => {
+ this.setState({ scrolled: this.isScrolled() });
+ };
+
+ render() {
+ const commonStyles = {
+ height: 56,
+ lineHeight: '24px',
+ padding: '16px 20px',
+ boxSizing: 'border-box',
+ borderBottom: this.props.border ? '1px solid #e6e6e6' : undefined,
+ backgroundColor: '#f3f3f3'
+ };
+
+ const inner = this.props.top
+ ? <div
+ className={css(
+ commonStyles,
+ {
+ position: 'fixed',
+ zIndex: 30,
+ top: this.props.top,
+ left: 'calc(50vw - 360px + 1px)',
+ right: 0,
+ boxShadow: this.state.scrolled ? '0 2px 4px rgba(0, 0, 0, .125)' : 'none',
+ transition: 'box-shadow 0.3s ease'
+ },
+ media('(max-width: 1320px)', { left: 301 })
+ )}>
+ {this.props.children}
+ </div>
+ : this.props.children;
+
+ return (
+ <div
+ className={css(clearfix(), commonStyles, {
+ marginTop: -20,
+ marginBottom: 20,
+ marginLeft: -20,
+ marginRight: -20,
+ '& .component-name': { lineHeight: '24px' }
+ })}>
+ {inner}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js
new file mode 100644
index 00000000000..4bd9cf945d6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.js
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import ListItem from './ListItem';
+import type { Issue } from '../../../components/issue/types';
+import type { Component } from '../utils';
+
+type Props = {|
+ checked: Array<string>,
+ component?: Component,
+ issues: Array<Issue>,
+ onFilterChange: (changes: {}) => void,
+ onIssueChange: (Issue) => void,
+ onIssueCheck?: (string) => void,
+ onIssueClick: (string) => void,
+ selectedIssue: ?Issue
+|};
+
+export default class IssuesList extends React.PureComponent {
+ props: Props;
+
+ render() {
+ const { checked, component, issues, selectedIssue } = this.props;
+
+ return (
+ <div>
+ {issues.map((issue, index) => (
+ <ListItem
+ checked={checked.includes(issue.key)}
+ component={component}
+ key={issue.key}
+ issue={issue}
+ onChange={this.props.onIssueChange}
+ onCheck={this.props.onIssueCheck}
+ onClick={this.props.onIssueClick}
+ onFilterChange={this.props.onFilterChange}
+ previousIssue={index > 0 ? issues[index - 1] : null}
+ selected={selectedIssue != null && selectedIssue.key === issue.key}
+ />
+ ))}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js
new file mode 100644
index 00000000000..ff090ed8287
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js
@@ -0,0 +1,68 @@
+/*
+ * 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 SourceViewer from '../../../components/SourceViewer/SourceViewer';
+import { scrollToElement } from '../../../helpers/scrolling';
+import type { Issue } from '../../../components/issue/types';
+
+type Props = {|
+ loadIssues: () => Promise<*>,
+ onIssueChange: (Issue) => void,
+ onIssueSelect: (string) => void,
+ openIssue: Issue
+|};
+
+export default class IssuesSourceViewer extends React.PureComponent {
+ node: HTMLElement;
+ props: Props;
+
+ componentDidUpdate(prevProps: Props) {
+ if (prevProps.openIssue.component === this.props.openIssue.component) {
+ this.scrollToIssue();
+ }
+ }
+
+ scrollToIssue = () => {
+ const element = this.node.querySelector(`[data-issue="${this.props.openIssue.key}"]`);
+ if (element) {
+ scrollToElement(element, 100, 100);
+ }
+ };
+
+ render() {
+ const { openIssue } = this.props;
+
+ return (
+ <div ref={node => this.node = node}>
+ <SourceViewer
+ aroundLine={openIssue.line}
+ component={openIssue.component}
+ displayAllIssues={true}
+ loadIssues={this.props.loadIssues}
+ onLoaded={this.scrollToIssue}
+ onIssueChange={this.props.onIssueChange}
+ onIssueSelect={this.props.onIssueSelect}
+ selectedIssue={openIssue.key}
+ />
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/ListItem.js b/server/sonar-web/src/main/js/apps/issues/components/ListItem.js
new file mode 100644
index 00000000000..f8a0bdda14a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/ListItem.js
@@ -0,0 +1,106 @@
+/*
+ * 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 ComponentBreadcrumbs from './ComponentBreadcrumbs';
+import Issue from '../../../components/issue/Issue';
+import type { Issue as IssueType } from '../../../components/issue/types';
+import type { Component } from '../utils';
+
+type Props = {|
+ checked: boolean,
+ component?: Component,
+ issue: IssueType,
+ onChange: (IssueType) => void,
+ onCheck?: (string) => void,
+ onClick: (string) => void,
+ onFilterChange: (changes: {}) => void,
+ previousIssue: ?Object,
+ selected: boolean
+|};
+
+type State = {
+ similarIssues: boolean
+};
+
+export default class ListItem extends React.PureComponent {
+ props: Props;
+ state: State = { similarIssues: false };
+
+ handleFilter = (property: string, issue: IssueType) => {
+ const { onFilterChange } = this.props;
+
+ const issuesReset = { issues: [] };
+
+ if (property.startsWith('tag###')) {
+ const tag = property.substr(6);
+ return onFilterChange({ ...issuesReset, tags: [tag] });
+ }
+
+ switch (property) {
+ case 'type':
+ return onFilterChange({ ...issuesReset, types: [issue.type] });
+ case 'severity':
+ return onFilterChange({ ...issuesReset, severities: [issue.severity] });
+ case 'status':
+ return onFilterChange({ ...issuesReset, statuses: [issue.status] });
+ case 'resolution':
+ return issue.resolution != null
+ ? onFilterChange({ ...issuesReset, resolved: true, resolutions: [issue.resolution] })
+ : onFilterChange({ ...issuesReset, resolved: false, resolutions: [] });
+ case 'assignee':
+ return issue.assignee != null
+ ? onFilterChange({ ...issuesReset, assigned: true, assignees: [issue.assignee] })
+ : onFilterChange({ ...issuesReset, assigned: false, assignees: [] });
+ case 'rule':
+ return onFilterChange({ ...issuesReset, rules: [issue.rule] });
+ case 'project':
+ return onFilterChange({ ...issuesReset, projects: [issue.projectUuid] });
+ case 'module':
+ return onFilterChange({ ...issuesReset, modules: [issue.subProjectUuid] });
+ case 'file':
+ return onFilterChange({ ...issuesReset, files: [issue.componentUuid] });
+ }
+ };
+
+ render() {
+ const { component, issue, previousIssue } = this.props;
+
+ const displayComponent = previousIssue == null || previousIssue.component !== issue.component;
+
+ return (
+ <div className="issues-workspace-list-item">
+ {displayComponent &&
+ <div className="issues-workspace-list-component">
+ <ComponentBreadcrumbs component={component} issue={this.props.issue} />
+ </div>}
+ <Issue
+ checked={this.props.checked}
+ issue={issue}
+ onChange={this.props.onChange}
+ onCheck={this.props.onCheck}
+ onClick={this.props.onClick}
+ onFilter={this.handleFilter}
+ selected={this.props.selected}
+ />
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js b/server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js
new file mode 100644
index 00000000000..a8f6441b1fd
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js
@@ -0,0 +1,60 @@
+/*
+ * 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';
+
+type Props = {|
+ myIssues: boolean,
+ onMyIssuesChange: (boolean) => void
+|};
+
+export default class MyIssuesFilter extends React.Component {
+ props: Props;
+
+ handleClick = (myIssues: boolean) =>
+ (e: Event & { currentTarget: HTMLElement }) => {
+ e.preventDefault();
+ e.currentTarget.blur();
+ this.props.onMyIssuesChange(myIssues);
+ };
+
+ render() {
+ const { myIssues } = this.props;
+
+ return (
+ <div className={css({ marginBottom: 24, textAlign: 'center' })}>
+ <div className="button-group">
+ <button
+ className={myIssues ? 'button-active' : undefined}
+ onClick={this.handleClick(true)}>
+ {translate('issues.my_issues')}
+ </button>
+ <button
+ className={myIssues ? undefined : 'button-active'}
+ onClick={this.handleClick(false)}>
+ {translate('all')}
+ </button>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/PageActions.js b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js
new file mode 100644
index 00000000000..262c3ad8699
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { css } from 'glamor';
+import type { Paging } from '../utils';
+import { translate } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+
+type Props = {|
+ loading: boolean,
+ openIssue: ?{},
+ paging: ?Paging,
+ selectedIndex: ?number
+|};
+
+export default class PageActions extends React.Component {
+ props: Props;
+
+ renderShortcuts() {
+ return (
+ <span className="note big-spacer-right">
+ <span className="big-spacer-right">
+ <span className="shortcut-button little-spacer-right">↑</span>
+ <span className="shortcut-button little-spacer-right">↓</span>
+ {translate('issues.to_select_issues')}
+ </span>
+
+ <span>
+ <span className="shortcut-button little-spacer-right">←</span>
+ <span className="shortcut-button little-spacer-right">→</span>
+ {translate('issues.to_navigate')}
+ </span>
+ </span>
+ );
+ }
+
+ render() {
+ const { openIssue, paging, selectedIndex } = this.props;
+
+ return (
+ <div className={css({ float: 'right' })}>
+ {openIssue == null && this.renderShortcuts()}
+
+ <div className={css({ display: 'inline-block', minWidth: 80, textAlign: 'right' })}>
+ {this.props.loading && <i className="spinner spacer-right" />}
+ {paging != null &&
+ <span>
+ <strong>
+ {selectedIndex != null && <span>{selectedIndex + 1} / </span>}
+ {formatMeasure(paging.total, 'INT')}
+ </strong>
+ {' '}
+ {translate('issues.issues')}
+ </span>}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/SearchSelect.js b/server/sonar-web/src/main/js/apps/issues/components/SearchSelect.js
new file mode 100644
index 00000000000..eddea6983ae
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/SearchSelect.js
@@ -0,0 +1,122 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import Select from 'react-select';
+import { debounce } from 'lodash';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+type Option = { label: string, value: string };
+
+type Props = {|
+ minimumQueryLength: number,
+ onSearch: (query: string) => Promise<Array<Option>>,
+ onSelect: (value: string) => void,
+ renderOption?: (option: Object) => React.Element<*>,
+ resetOnBlur: boolean,
+ value?: string
+|};
+
+type State = {
+ loading: boolean,
+ options: Array<Option>,
+ query: string
+};
+
+export default class SearchSelect extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State;
+
+ static defaultProps = {
+ minimumQueryLength: 2,
+ resetOnBlur: true
+ };
+
+ constructor(props: Props) {
+ super(props);
+ this.state = { loading: false, options: [], query: '' };
+ this.search = debounce(this.search, 250);
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ search = (query: string) => {
+ this.props.onSearch(query).then(options => {
+ if (this.mounted) {
+ this.setState({ loading: false, options });
+ }
+ });
+ };
+
+ handleBlur = () => {
+ this.setState({ options: [], query: '' });
+ };
+
+ handleChange = (option: Option) => {
+ this.props.onSelect(option.value);
+ };
+
+ handleInputChange = (query: string = '') => {
+ if (query.length >= this.props.minimumQueryLength) {
+ this.setState({ loading: true, query });
+ this.search(query);
+ } else {
+ this.setState({ options: [], query });
+ }
+ };
+
+ // disable internal filtering
+ handleFilterOption = () => true;
+
+ render() {
+ return (
+ <Select
+ autofocus={true}
+ cache={false}
+ className="input-super-large"
+ clearable={false}
+ filterOption={this.handleFilterOption}
+ isLoading={this.state.loading}
+ noResultsText={
+ this.state.query.length < this.props.minimumQueryLength
+ ? translateWithParameters('select2.tooShort', this.props.minimumQueryLength)
+ : translate('select2.noMatches')
+ }
+ onBlur={this.props.resetOnBlur ? this.handleBlur : undefined}
+ onChange={this.handleChange}
+ onInputChange={this.handleInputChange}
+ onOpen={this.props.minimumQueryLength === 0 ? this.handleInputChange : undefined}
+ optionRenderer={this.props.renderOption}
+ options={this.state.options}
+ placeholder={translate('search_verb')}
+ searchable={true}
+ value={this.props.value}
+ valueRenderer={this.props.renderOption}
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/SearchSelect-test.js b/server/sonar-web/src/main/js/apps/issues/components/__tests__/SearchSelect-test.js
new file mode 100644
index 00000000000..f4d46d4f8a8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/SearchSelect-test.js
@@ -0,0 +1,49 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import SearchSelect from '../SearchSelect';
+
+jest.mock('lodash', () => ({
+ debounce: fn => fn
+}));
+
+it('should render Select', () => {
+ expect(shallow(<SearchSelect onSearch={jest.fn()} onSelect={jest.fn()} />)).toMatchSnapshot();
+});
+
+it('should call onSelect', () => {
+ const onSelect = jest.fn();
+ const wrapper = shallow(<SearchSelect onSearch={jest.fn()} onSelect={onSelect} />);
+ wrapper.prop('onChange')({ value: 'foo' });
+ expect(onSelect).lastCalledWith('foo');
+});
+
+it('should call onSearch', () => {
+ const onSearch = jest.fn().mockReturnValue(Promise.resolve([]));
+ const wrapper = shallow(
+ <SearchSelect minimumQueryLength={2} onSearch={onSearch} onSelect={jest.fn()} />
+ );
+ wrapper.prop('onInputChange')('f');
+ expect(onSearch).not.toHaveBeenCalled();
+ wrapper.prop('onInputChange')('foo');
+ expect(onSearch).lastCalledWith('foo');
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/SearchSelect-test.js.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/SearchSelect-test.js.snap
new file mode 100644
index 00000000000..4f2fe37cc62
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/SearchSelect-test.js.snap
@@ -0,0 +1,48 @@
+exports[`test should render Select 1`] = `
+<Select
+ addLabelText="Add \"{label}\"?"
+ arrowRenderer={[Function]}
+ autofocus={true}
+ autosize={true}
+ backspaceRemoves={true}
+ backspaceToRemoveMessage="Press backspace to remove {label}"
+ cache={false}
+ className="input-super-large"
+ clearAllText="Clear all"
+ clearValueText="Clear value"
+ clearable={false}
+ delimiter=","
+ disabled={false}
+ escapeClearsValue={true}
+ filterOption={[Function]}
+ filterOptions={[Function]}
+ ignoreAccents={true}
+ ignoreCase={true}
+ inputProps={Object {}}
+ isLoading={false}
+ joinValues={false}
+ labelKey="label"
+ matchPos="any"
+ matchProp="any"
+ menuBuffer={0}
+ menuRenderer={[Function]}
+ multi={false}
+ noResultsText="select2.tooShort.2"
+ onBlur={[Function]}
+ onBlurResetsInput={true}
+ onChange={[Function]}
+ onCloseResetsInput={true}
+ onInputChange={[Function]}
+ openAfterFocus={false}
+ optionComponent={[Function]}
+ options={Array []}
+ pageSize={5}
+ placeholder="search_verb"
+ required={false}
+ scrollMenuIntoView={true}
+ searchable={true}
+ simpleValue={false}
+ tabSelectsValue={true}
+ valueComponent={[Function]}
+ valueKey="value" />
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/controller.js b/server/sonar-web/src/main/js/apps/issues/controller.js
deleted file mode 100644
index f98f292d4bd..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/controller.js
+++ /dev/null
@@ -1,217 +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 Controller from '../../components/navigator/controller';
-import ComponentViewer from './component-viewer/main';
-import getStore from '../../app/utils/getStore';
-import { receiveIssues } from '../../store/issues/duck';
-
-const FACET_DATA_FIELDS = ['components', 'users', 'rules', 'languages'];
-
-export default Controller.extend({
- _issuesParameters() {
- return {
- p: this.options.app.state.get('page'),
- ps: this.pageSize,
- asc: true,
- additionalFields: '_all',
- facets: this._facetsFromServer().join()
- };
- },
-
- receiveIssues(issues) {
- const store = getStore();
- store.dispatch(receiveIssues(issues));
- },
-
- fetchList(firstPage) {
- const that = this;
- if (firstPage == null) {
- firstPage = true;
- }
- if (firstPage) {
- this.options.app.state.set({ selectedIndex: 0, page: 1 }, { silent: true });
- this.closeComponentViewer();
- }
- const data = this._issuesParameters();
- Object.assign(data, this.options.app.state.get('query'));
- if (this.options.app.state.get('query').assigned_to_me) {
- Object.assign(data, { assignees: '__me__' });
- }
- if (this.options.app.state.get('isContext')) {
- Object.assign(data, this.options.app.state.get('contextQuery'));
- }
- return $.get(window.baseUrl + '/api/issues/search', data).done(r => {
- const issues = that.options.app.list.parseIssues(r);
- this.receiveIssues(issues);
- if (firstPage) {
- const issues = that.options.app.list.parseIssues(r);
- that.options.app.list.reset(issues);
- } else {
- const issues = that.options.app.list.parseIssues(r, that.options.app.list.length);
- that.options.app.list.add(issues);
- }
- that.options.app.list.setIndex();
- FACET_DATA_FIELDS.forEach(field => {
- that.options.app.facets[field] = r[field];
- });
- that.options.app.facets.reset(that._allFacets());
- that.options.app.facets.add(r.facets, { merge: true });
- that.enableFacets(that._enabledFacets());
- if (firstPage) {
- that.options.app.state.set({
- page: r.p,
- pageSize: r.ps,
- total: r.total,
- maxResultsReached: r.p * r.ps >= r.total
- });
- } else {
- that.options.app.state.set({
- page: r.p,
- maxResultsReached: r.p * r.ps >= r.total
- });
- }
- if (firstPage && that.isIssuePermalink()) {
- that.showComponentViewer(that.options.app.list.first());
- }
- });
- },
-
- isIssuePermalink() {
- const query = this.options.app.state.get('query');
- return query.issues != null && this.options.app.list.length === 1;
- },
-
- _mergeCollections(a, b) {
- const collection = new Backbone.Collection(a);
- collection.add(b, { merge: true });
- return collection.toJSON();
- },
-
- requestFacet(id) {
- const that = this;
- const facet = this.options.app.facets.get(id);
- const data = {
- facets: id,
- ps: 1,
- additionalFields: '_all',
- ...this.options.app.state.get('query')
- };
- if (this.options.app.state.get('query').assigned_to_me) {
- Object.assign(data, { assignees: '__me__' });
- }
- if (this.options.app.state.get('isContext')) {
- Object.assign(data, this.options.app.state.get('contextQuery'));
- }
- return $.get(window.baseUrl + '/api/issues/search', data, r => {
- FACET_DATA_FIELDS.forEach(field => {
- that.options.app.facets[field] = that._mergeCollections(
- that.options.app.facets[field],
- r[field]
- );
- });
- const facetData = r.facets.find(facet => facet.property === id);
- if (facetData != null) {
- facet.set(facetData);
- }
- });
- },
-
- newSearch() {
- this.options.app.state.unset('filter');
- return this.options.app.state.setQuery({ resolved: 'false' });
- },
-
- parseQuery() {
- const q = Controller.prototype.parseQuery.apply(this, arguments);
- delete q.asc;
- delete q.s;
- delete q.id;
- return q;
- },
-
- getQueryAsObject() {
- const state = this.options.app.state;
- const query = state.get('query');
- if (query.assigned_to_me) {
- Object.assign(query, { assignees: '__me__' });
- }
- if (state.get('isContext')) {
- Object.assign(query, state.get('contextQuery'));
- }
- return query;
- },
-
- getQuery(separator, addContext, handleMyIssues = false) {
- if (separator == null) {
- separator = '|';
- }
- if (addContext == null) {
- addContext = false;
- }
- const filter = this.options.app.state.get('query');
- if (addContext && this.options.app.state.get('isContext')) {
- Object.assign(filter, this.options.app.state.get('contextQuery'));
- }
- if (handleMyIssues && this.options.app.state.get('query').assigned_to_me) {
- Object.assign(filter, { assignees: '__me__' });
- }
- const route = [];
- Object.keys(filter).forEach(property => {
- route.push(`${property}=${encodeURIComponent(filter[property])}`);
- });
- return route.join(separator);
- },
-
- _prepareComponent(issue) {
- return {
- key: issue.get('component'),
- name: issue.get('componentLongName'),
- qualifier: issue.get('componentQualifier'),
- subProject: issue.get('subProject'),
- subProjectName: issue.get('subProjectLongName'),
- project: issue.get('project'),
- projectName: issue.get('projectLongName'),
- projectOrganization: issue.get('projectOrganization')
- };
- },
-
- showComponentViewer(issue) {
- this.options.app.layout.workspaceComponentViewerRegion.reset();
- key.setScope('componentViewer');
- this.options.app.issuesView.unbindScrollEvents();
- this.options.app.state.set('component', this._prepareComponent(issue));
- this.options.app.componentViewer = new ComponentViewer({ app: this.options.app });
- this.options.app.layout.workspaceComponentViewerRegion.show(this.options.app.componentViewer);
- this.options.app.layout.showComponentViewer();
- return this.options.app.componentViewer.openFileByIssue(issue);
- },
-
- closeComponentViewer() {
- key.setScope('list');
- $('body').click();
- this.options.app.state.unset('component');
- this.options.app.layout.workspaceComponentViewerRegion.reset();
- this.options.app.layout.hideComponentViewer();
- this.options.app.issuesView.bindScrollEvents();
- return this.options.app.issuesView.scrollTo();
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets-view.js b/server/sonar-web/src/main/js/apps/issues/facets-view.js
deleted file mode 100644
index f706ef53a39..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets-view.js
+++ /dev/null
@@ -1,63 +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 FacetsView from '../../components/navigator/facets-view';
-import BaseFacet from './facets/base-facet';
-import TypeFacet from './facets/type-facet';
-import SeverityFacet from './facets/severity-facet';
-import StatusFacet from './facets/status-facet';
-import ProjectFacet from './facets/project-facet';
-import ModuleFacet from './facets/module-facet';
-import AssigneeFacet from './facets/assignee-facet';
-import RuleFacet from './facets/rule-facet';
-import TagFacet from './facets/tag-facet';
-import ResolutionFacet from './facets/resolution-facet';
-import CreationDateFacet from './facets/creation-date-facet';
-import FileFacet from './facets/file-facet';
-import LanguageFacet from './facets/language-facet';
-import AuthorFacet from './facets/author-facet';
-import IssueKeyFacet from './facets/issue-key-facet';
-import ContextFacet from './facets/context-facet';
-import ModeFacet from './facets/mode-facet';
-
-const viewsMapping = {
- types: TypeFacet,
- severities: SeverityFacet,
- statuses: StatusFacet,
- assignees: AssigneeFacet,
- resolutions: ResolutionFacet,
- createdAt: CreationDateFacet,
- projectUuids: ProjectFacet,
- moduleUuids: ModuleFacet,
- rules: RuleFacet,
- tags: TagFacet,
- fileUuids: FileFacet,
- languages: LanguageFacet,
- authors: AuthorFacet,
- issues: IssueKeyFacet,
- context: ContextFacet,
- facetMode: ModeFacet
-};
-
-export default FacetsView.extend({
- getChildView(model) {
- const view = viewsMapping[model.get('property')];
- return view ? view : BaseFacet;
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js
deleted file mode 100644
index 597afcaee47..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js
+++ /dev/null
@@ -1,135 +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 { sortBy } from 'lodash';
-import CustomValuesFacet from './custom-values-facet';
-import Template from '../templates/facets/issues-assignee-facet.hbs';
-
-export default CustomValuesFacet.extend({
- template: Template,
-
- initialize() {
- this.context = {
- isContext: this.options.app.state.get('isContext'),
- organization: this.options.app.state.get('contextOrganization')
- };
- },
-
- getUrl() {
- return window.baseUrl +
- (this.context.isContext ? '/api/organizations/search_members' : '/api/users/search');
- },
-
- prepareAjaxSearch() {
- return {
- quietMillis: 300,
- url: this.getUrl(),
- data: (term, page) => {
- if (this.context.isContext && this.context.organization) {
- return { q: term, p: page, organization: this.context.organization };
- } else {
- return { q: term, p: page };
- }
- },
- results: window.usersToSelect2
- };
- },
-
- onRender() {
- CustomValuesFacet.prototype.onRender.apply(this, arguments);
-
- const myIssuesSelected = !!this.options.app.state.get('query').assigned_to_me;
- this.$el.toggleClass('hidden', myIssuesSelected);
-
- const value = this.options.app.state.get('query').assigned;
- if (value != null && (!value || value === 'false')) {
- this.$('.js-facet').filter('[data-unassigned]').addClass('active');
- }
- },
-
- toggleFacet(e) {
- const unassigned = $(e.currentTarget).is('[data-unassigned]');
- $(e.currentTarget).toggleClass('active');
- if (unassigned) {
- const checked = $(e.currentTarget).is('.active');
- const value = checked ? 'false' : null;
- return this.options.app.state.updateFilter({
- assigned: value,
- assignees: null,
- assigned_to_me: null
- });
- } else {
- return this.options.app.state.updateFilter({
- assigned: null,
- assignees: this.getValue(),
- assigned_to_me: null
- });
- }
- },
-
- getValuesWithLabels() {
- const values = this.model.getValues();
- const users = this.options.app.facets.users;
- values.forEach(v => {
- const login = v.val;
- let name = '';
- if (login) {
- const user = users.find(user => user.login === login);
- if (user != null) {
- name = user.name;
- }
- }
- v.label = name;
- });
- return values;
- },
-
- disable() {
- return this.options.app.state.updateFilter({
- assigned: null,
- assignees: null
- });
- },
-
- addCustomValue() {
- const property = this.model.get('property');
- const customValue = this.$('.js-custom-value').select2('val');
- let value = this.getValue();
- if (value.length > 0) {
- value += ',';
- }
- value += customValue;
- const obj = {};
- obj[property] = value;
- obj.assigned = null;
- return this.options.app.state.updateFilter(obj);
- },
-
- sortValues(values) {
- return sortBy(values, v => v.val === '' ? -999999 : -v.count);
- },
-
- serializeData() {
- return {
- ...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/author-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/author-facet.js
deleted file mode 100644
index c652eba0abd..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/author-facet.js
+++ /dev/null
@@ -1,60 +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 CustomValuesFacet from './custom-values-facet';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-
-export default CustomValuesFacet.extend({
- getUrl() {
- return window.baseUrl + '/api/issues/authors';
- },
-
- prepareSearch() {
- return this.$('.js-custom-value').select2({
- placeholder: translate('search_verb'),
- minimumInputLength: 2,
- allowClear: false,
- formatNoMatches() {
- return translate('select2.noMatches');
- },
- formatSearching() {
- return translate('select2.searching');
- },
- formatInputTooShort() {
- return translateWithParameters('select2.tooShort', 2);
- },
- width: '100%',
- ajax: {
- quietMillis: 300,
- url: this.getUrl(),
- data(term) {
- return { q: term, ps: 25 };
- },
- results(data) {
- return {
- more: false,
- results: data.authors.map(author => {
- return { id: author, text: author };
- })
- };
- }
- }
- });
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js
deleted file mode 100644
index 5d05caaeb2e..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js
+++ /dev/null
@@ -1,41 +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 BaseFacet from '../../../components/navigator/facets/base-facet';
-import Template from '../templates/facets/issues-base-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- onRender() {
- BaseFacet.prototype.onRender.apply(this, arguments);
- return this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
- },
-
- onDestroy() {
- return this.$('[data-toggle="tooltip"]').tooltip('destroy');
- },
-
- serializeData() {
- return {
- ...BaseFacet.prototype.serializeData.apply(this, arguments),
- state: this.options.app.state.toJSON()
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js
deleted file mode 100644
index 81c31973b79..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js
+++ /dev/null
@@ -1,176 +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 moment from 'moment';
-import { times } from 'lodash';
-import BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-creation-date-facet.hbs';
-import '../../../components/widgets/barchart';
-import { formatMeasure } from '../../../helpers/measures';
-
-export default BaseFacet.extend({
- template: Template,
-
- events() {
- return {
- ...BaseFacet.prototype.events.apply(this, arguments),
- 'change input': 'applyFacet',
- 'click .js-select-period-start': 'selectPeriodStart',
- 'click .js-select-period-end': 'selectPeriodEnd',
- 'click .sonar-d3 rect': 'selectBar',
- 'click .js-all': 'onAllClick',
- 'click .js-last-week': 'onLastWeekClick',
- 'click .js-last-month': 'onLastMonthClick',
- 'click .js-last-year': 'onLastYearClick',
- 'click .js-leak': 'onLeakClick'
- };
- },
-
- onRender() {
- const that = this;
- this.$el.toggleClass('search-navigator-facet-box-collapsed', !this.model.get('enabled'));
- this.$('input').datepicker({
- dateFormat: 'yy-mm-dd',
- changeMonth: true,
- changeYear: true
- });
- const props = ['createdAfter', 'createdBefore', 'createdAt'];
- const query = this.options.app.state.get('query');
- props.forEach(prop => {
- const value = query[prop];
- if (value != null) {
- that.$(`input[name=${prop}]`).val(value);
- }
- });
- let values = this.model.getValues();
- if (!(Array.isArray(values) && values.length > 0)) {
- let date = moment();
- values = [];
- times(10, () => {
- values.push({ count: 0, val: date.toDate().toString() });
- date = date.subtract(1, 'days');
- });
- values.reverse();
- }
- values = values.map(v => {
- const format = that.options.app.state.getFacetMode() === 'count'
- ? 'SHORT_INT'
- : 'SHORT_WORK_DUR';
- const text = formatMeasure(v.count, format);
- return { ...v, text };
- });
- return this.$('.js-barchart').barchart(values);
- },
-
- selectPeriodStart() {
- return this.$('.js-period-start').datepicker('show');
- },
-
- selectPeriodEnd() {
- return this.$('.js-period-end').datepicker('show');
- },
-
- applyFacet() {
- const obj = { createdAt: null, createdInLast: null };
- this.$('input').each(function() {
- const property = $(this).prop('name');
- const value = $(this).val();
- obj[property] = value;
- });
- return this.options.app.state.updateFilter(obj);
- },
-
- disable() {
- return this.options.app.state.updateFilter({
- createdAfter: null,
- createdBefore: null,
- createdAt: null,
- sinceLeakPeriod: null,
- createdInLast: null
- });
- },
-
- selectBar(e) {
- const periodStart = $(e.currentTarget).data('period-start');
- const periodEnd = $(e.currentTarget).data('period-end');
- return this.options.app.state.updateFilter({
- createdAfter: periodStart,
- createdBefore: periodEnd,
- createdAt: null,
- sinceLeakPeriod: null,
- createdInLast: null
- });
- },
-
- selectPeriod(period) {
- return this.options.app.state.updateFilter({
- createdAfter: null,
- createdBefore: null,
- createdAt: null,
- sinceLeakPeriod: null,
- createdInLast: period
- });
- },
-
- onAllClick(e) {
- e.preventDefault();
- return this.disable();
- },
-
- onLastWeekClick(e) {
- e.preventDefault();
- return this.selectPeriod('1w');
- },
-
- onLastMonthClick(e) {
- e.preventDefault();
- return this.selectPeriod('1m');
- },
-
- onLastYearClick(e) {
- e.preventDefault();
- return this.selectPeriod('1y');
- },
-
- onLeakClick(e) {
- e.preventDefault();
- this.options.app.state.updateFilter({
- createdAfter: null,
- createdBefore: null,
- createdAt: null,
- createdInLast: null,
- sinceLeakPeriod: 'true'
- });
- },
-
- serializeData() {
- const hasLeak = this.options.app.state.get('contextComponentQualifier') === 'TRK';
-
- return {
- ...BaseFacet.prototype.serializeData.apply(this, arguments),
- hasLeak,
- periodStart: this.options.app.state.get('query').createdAfter,
- periodEnd: this.options.app.state.get('query').createdBefore,
- createdAt: this.options.app.state.get('query').createdAt,
- sinceLeakPeriod: this.options.app.state.get('query').sinceLeakPeriod,
- createdInLast: this.options.app.state.get('query').createdInLast
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js
deleted file mode 100644
index a6168f350f1..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js
+++ /dev/null
@@ -1,85 +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 BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-custom-values-facet.hbs';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-
-export default BaseFacet.extend({
- template: Template,
-
- events() {
- return {
- ...BaseFacet.prototype.events.apply(this, arguments),
- 'change .js-custom-value': 'addCustomValue'
- };
- },
-
- getUrl() {},
-
- onRender() {
- BaseFacet.prototype.onRender.apply(this, arguments);
- return this.prepareSearch();
- },
-
- prepareSearch() {
- return this.$('.js-custom-value').select2({
- placeholder: translate('search_verb'),
- minimumInputLength: 2,
- allowClear: false,
- formatNoMatches() {
- return translate('select2.noMatches');
- },
- formatSearching() {
- return translate('select2.searching');
- },
- formatInputTooShort() {
- return translateWithParameters('select2.tooShort', 2);
- },
- width: '100%',
- ajax: this.prepareAjaxSearch()
- });
- },
-
- prepareAjaxSearch() {
- return {
- quietMillis: 300,
- url: this.getUrl(),
- data(term, page) {
- return { s: term, p: page };
- },
- results(data) {
- return { more: data.more, results: data.results };
- }
- };
- },
-
- addCustomValue() {
- const property = this.model.get('property');
- const customValue = this.$('.js-custom-value').select2('val');
- let value = this.getValue();
- if (value.length > 0) {
- value += ',';
- }
- value += customValue;
- const obj = {};
- obj[property] = value;
- return this.options.app.state.updateFilter(obj);
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/file-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/file-facet.js
deleted file mode 100644
index 11b9a69a46d..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/file-facet.js
+++ /dev/null
@@ -1,61 +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 BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-file-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- onRender() {
- BaseFacet.prototype.onRender.apply(this, arguments);
- const widths = this.$('.facet-stat')
- .map(function() {
- return $(this).outerWidth();
- })
- .get();
- const maxValueWidth = Math.max(...widths);
- return this.$('.facet-name').css('padding-right', maxValueWidth);
- },
-
- getValuesWithLabels() {
- const values = this.model.getValues();
- const source = this.options.app.facets.components;
- values.forEach(v => {
- const key = v.val;
- let label = null;
- if (key) {
- const item = source.find(file => file.uuid === key);
- if (item != null) {
- label = item.longName;
- }
- }
- v.label = label;
- });
- return values;
- },
-
- serializeData() {
- return {
- ...BaseFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js
deleted file mode 100644
index d57d9c79cce..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.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 BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-issue-key-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- onRender() {
- return this.$el.toggleClass('hidden', !this.options.app.state.get('query').issues);
- },
-
- disable() {
- return this.options.app.state.updateFilter({ issues: null });
- },
-
- serializeData() {
- return {
- ...BaseFacet.prototype.serializeData.apply(this, arguments),
- issues: this.options.app.state.get('query').issues
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/language-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/language-facet.js
deleted file mode 100644
index 789ae16ca93..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/language-facet.js
+++ /dev/null
@@ -1,84 +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 CustomValuesFacet from './custom-values-facet';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-
-export default CustomValuesFacet.extend({
- getUrl() {
- return window.baseUrl + '/api/languages/list';
- },
-
- prepareSearch() {
- return this.$('.js-custom-value').select2({
- placeholder: translate('search_verb'),
- minimumInputLength: 2,
- allowClear: false,
- formatNoMatches() {
- return translate('select2.noMatches');
- },
- formatSearching() {
- return translate('select2.searching');
- },
- formatInputTooShort() {
- return translateWithParameters('select2.tooShort', 2);
- },
- width: '100%',
- ajax: {
- quietMillis: 300,
- url: this.getUrl(),
- data(term) {
- return { q: term, ps: 0 };
- },
- results(data) {
- return {
- more: false,
- results: data.languages.map(lang => {
- return { id: lang.key, text: lang.name };
- })
- };
- }
- }
- });
- },
-
- getValuesWithLabels() {
- const values = this.model.getValues();
- const source = this.options.app.facets.languages;
- values.forEach(v => {
- const key = v.val;
- let label = null;
- if (key) {
- const item = source.find(lang => lang.key === key);
- if (item != null) {
- label = item.name;
- }
- }
- v.label = label;
- });
- return values;
- },
-
- serializeData() {
- return {
- ...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/mode-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/mode-facet.js
deleted file mode 100644
index dd54e82c8d2..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/mode-facet.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 BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-mode-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- toggleFacet(e) {
- const isCount = $(e.currentTarget).is('[data-value="count"]');
- return this.options.app.state.updateFilter({
- facetMode: isCount ? 'count' : 'effort'
- });
- },
-
- serializeData() {
- return {
- ...BaseFacet.prototype.serializeData.apply(this, arguments),
- mode: this.options.app.state.getFacetMode()
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/module-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/module-facet.js
deleted file mode 100644
index b6e87598bff..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/module-facet.js
+++ /dev/null
@@ -1,46 +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 BaseFacet from './base-facet';
-
-export default BaseFacet.extend({
- getValuesWithLabels() {
- const values = this.model.getValues();
- const components = this.options.app.facets.components;
- values.forEach(v => {
- const uuid = v.val;
- let label = uuid;
- if (uuid) {
- const component = components.find(c => c.uuid === uuid);
- if (component != null) {
- label = component.longName;
- }
- }
- v.label = label;
- });
- return values;
- },
-
- serializeData() {
- return {
- ...BaseFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/project-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/project-facet.js
deleted file mode 100644
index 9bd37e64b03..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/project-facet.js
+++ /dev/null
@@ -1,112 +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 CustomValuesFacet from './custom-values-facet';
-import Template from '../templates/facets/issues-projects-facet.hbs';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { areThereCustomOrganizations, getOrganization } from '../../../store/organizations/utils';
-
-export default CustomValuesFacet.extend({
- template: Template,
-
- getUrl() {
- return window.baseUrl + '/api/components/search';
- },
-
- prepareSearchForViews() {
- const contextId = this.options.app.state.get('contextComponentUuid');
- return {
- url: window.baseUrl + '/api/components/tree',
- data(term, page) {
- return { q: term, p: page, qualifiers: 'TRK', baseComponentId: contextId };
- }
- };
- },
-
- prepareAjaxSearch() {
- const options = {
- quietMillis: 300,
- url: this.getUrl(),
- data(term, page) {
- return { q: term, p: page, qualifiers: 'TRK' };
- },
- results: r => ({
- more: r.paging.total > r.paging.pageIndex * r.paging.pageSize,
- results: r.components.map(component => ({
- id: component.id,
- text: component.name
- }))
- })
- };
- const contextQualifier = this.options.app.state.get('contextComponentQualifier');
- if (contextQualifier === 'VW' || contextQualifier === 'SVW') {
- Object.assign(options, this.prepareSearchForViews());
- }
- return options;
- },
-
- prepareSearch() {
- return this.$('.js-custom-value').select2({
- placeholder: translate('search_verb'),
- minimumInputLength: 3,
- allowClear: false,
- formatNoMatches() {
- return translate('select2.noMatches');
- },
- formatSearching() {
- return translate('select2.searching');
- },
- formatInputTooShort() {
- return translateWithParameters('select2.tooShort', 3);
- },
- width: '100%',
- ajax: this.prepareAjaxSearch()
- });
- },
-
- getValuesWithLabels() {
- const values = this.model.getValues();
- const projects = this.options.app.facets.components;
- const displayOrganizations = areThereCustomOrganizations();
- values.forEach(v => {
- const uuid = v.val;
- let label = '';
- let organization = null;
- if (uuid) {
- const project = projects.find(p => p.uuid === uuid);
- if (project != null) {
- label = project.longName;
- organization = displayOrganizations && project.organization
- ? getOrganization(project.organization)
- : null;
- }
- }
- v.label = label;
- v.organization = organization;
- });
- return values;
- },
-
- serializeData() {
- return {
- ...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js
deleted file mode 100644
index 3e691aea9be..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js
+++ /dev/null
@@ -1,61 +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 CustomValuesFacet from './custom-values-facet';
-
-export default CustomValuesFacet.extend({
- getUrl() {
- return window.baseUrl + '/api/users/search';
- },
-
- prepareAjaxSearch() {
- return {
- quietMillis: 300,
- url: this.getUrl(),
- data(term, page) {
- return { q: term, p: page };
- },
- results: window.usersToSelect2
- };
- },
-
- getValuesWithLabels() {
- const values = this.model.getValues();
- const source = this.options.app.facets.users;
- values.forEach(v => {
- const key = v.val;
- let label = null;
- if (key) {
- const item = source.find(user => user.login === key);
- if (item != null) {
- label = item.name;
- }
- }
- v.label = label;
- });
- return values;
- },
-
- serializeData() {
- return {
- ...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js
deleted file mode 100644
index d4e01b1c0f4..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js
+++ /dev/null
@@ -1,65 +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 { sortBy } from 'lodash';
-import BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-resolution-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- onRender() {
- BaseFacet.prototype.onRender.apply(this, arguments);
- const value = this.options.app.state.get('query').resolved;
- if (value != null && (!value || value === 'false')) {
- this.$('.js-facet').filter('[data-unresolved]').addClass('active');
- }
- },
-
- toggleFacet(e) {
- const unresolved = $(e.currentTarget).is('[data-unresolved]');
- $(e.currentTarget).toggleClass('active');
- if (unresolved) {
- const checked = $(e.currentTarget).is('.active');
- const value = checked ? 'false' : null;
- return this.options.app.state.updateFilter({
- resolved: value,
- resolutions: null
- });
- } else {
- return this.options.app.state.updateFilter({
- resolved: null,
- resolutions: this.getValue()
- });
- }
- },
-
- disable() {
- return this.options.app.state.updateFilter({
- resolved: null,
- resolutions: null
- });
- },
-
- sortValues(values) {
- const order = ['', 'FIXED', 'FALSE-POSITIVE', 'WONTFIX', 'REMOVED'];
- return sortBy(values, v => order.indexOf(v.val));
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js
deleted file mode 100644
index 75445570098..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js
+++ /dev/null
@@ -1,95 +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 CustomValuesFacet from './custom-values-facet';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-
-export default CustomValuesFacet.extend({
- prepareSearch() {
- let url = window.baseUrl + '/api/rules/search?f=name,langName';
- const languages = this.options.app.state.get('query').languages;
- if (languages != null) {
- url += '&languages=' + languages;
- }
- return this.$('.js-custom-value').select2({
- placeholder: translate('search_verb'),
- minimumInputLength: 2,
- allowClear: false,
- formatNoMatches() {
- return translate('select2.noMatches');
- },
- formatSearching() {
- return translate('select2.searching');
- },
- formatInputTooShort() {
- return translateWithParameters('select2.tooShort', 2);
- },
- width: '100%',
- ajax: {
- url,
- quietMillis: 300,
- data(term, page) {
- return { q: term, p: page };
- },
- results(data) {
- const results = data.rules.map(rule => {
- const lang = rule.langName || translate('manual');
- return {
- id: rule.key,
- text: '(' + lang + ') ' + rule.name
- };
- });
- return {
- more: data.p * data.ps < data.total,
- results
- };
- }
- }
- });
- },
-
- getValuesWithLabels() {
- const values = this.model.getValues();
- const rules = this.options.app.facets.rules;
- values.forEach(v => {
- const key = v.val;
- let label = '';
- let extra = '';
- if (key) {
- const rule = rules.find(r => r.key === key);
- if (rule != null) {
- label = rule.name;
- }
- if (rule != null) {
- extra = rule.langName;
- }
- }
- v.label = label;
- v.extra = extra;
- });
- return values;
- },
-
- serializeData() {
- return {
- ...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js
deleted file mode 100644
index db5cd337838..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js
+++ /dev/null
@@ -1,31 +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 { sortBy } from 'lodash';
-import BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-severity-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- sortValues(values) {
- const order = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR'];
- return sortBy(values, v => order.indexOf(v.val));
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/status-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/status-facet.js
deleted file mode 100644
index cb4e88a5f49..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/status-facet.js
+++ /dev/null
@@ -1,31 +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 { sortBy } from 'lodash';
-import BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-status-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- sortValues(values) {
- const order = ['OPEN', 'RESOLVED', 'REOPENED', 'CLOSED', 'CONFIRMED'];
- return sortBy(values, v => order.indexOf(v.val));
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js
deleted file mode 100644
index 8bb629ff3d4..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js
+++ /dev/null
@@ -1,72 +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 CustomValuesFacet from './custom-values-facet';
-import { translate } from '../../../helpers/l10n';
-
-export default CustomValuesFacet.extend({
- prepareSearch() {
- let url = window.baseUrl + '/api/issues/tags?ps=10';
- const tags = this.options.app.state.get('query').tags;
- if (tags != null) {
- url += '&tags=' + tags;
- }
- return this.$('.js-custom-value').select2({
- placeholder: translate('search_verb'),
- minimumInputLength: 0,
- allowClear: false,
- formatNoMatches() {
- return translate('select2.noMatches');
- },
- formatSearching() {
- return translate('select2.searching');
- },
- width: '100%',
- ajax: {
- url,
- quietMillis: 300,
- data(term) {
- return { q: term, ps: 10 };
- },
- results(data) {
- const results = data.tags.map(tag => {
- return { id: tag, text: tag };
- });
- return { more: false, results };
- }
- }
- });
- },
-
- getValuesWithLabels() {
- const values = this.model.getValues();
- values.forEach(v => {
- v.label = v.val;
- v.extra = '';
- });
- return values;
- },
-
- serializeData() {
- return {
- ...CustomValuesFacet.prototype.serializeData.apply(this, arguments),
- values: this.sortValues(this.getValuesWithLabels())
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/type-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/type-facet.js
deleted file mode 100644
index efac17ea9f4..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/facets/type-facet.js
+++ /dev/null
@@ -1,31 +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 { sortBy } from 'lodash';
-import BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-type-facet.hbs';
-
-export default BaseFacet.extend({
- template: Template,
-
- sortValues(values) {
- const order = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
- return sortBy(values, v => order.indexOf(v.val));
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/init.js b/server/sonar-web/src/main/js/apps/issues/init.js
deleted file mode 100644
index 0fb32e839ab..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/init.js
+++ /dev/null
@@ -1,87 +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 State from './models/state';
-import Layout from './layout';
-import Issues from './models/issues';
-import Facets from '../../components/navigator/models/facets';
-import Controller from './controller';
-import Router from './router';
-import WorkspaceListView from './workspace-list-view';
-import WorkspaceHeaderView from './workspace-header-view';
-import FacetsView from './facets-view';
-import HeaderView from './HeaderView';
-
-const App = new Marionette.Application();
-const init = function({ el, user }) {
- this.state = new State({ user, canBulkChange: user.isLoggedIn });
- this.list = new Issues();
- this.facets = new Facets();
-
- this.layout = new Layout({ app: this, el });
- this.layout.render();
- $('#footer').addClass('search-navigator-footer');
-
- this.controller = new Controller({ app: this });
-
- this.issuesView = new WorkspaceListView({
- app: this,
- collection: this.list
- });
- this.layout.workspaceListRegion.show(this.issuesView);
- this.issuesView.bindScrollEvents();
-
- this.workspaceHeaderView = new WorkspaceHeaderView({
- app: this,
- collection: this.list
- });
- this.layout.workspaceHeaderRegion.show(this.workspaceHeaderView);
-
- this.facetsView = new FacetsView({
- app: this,
- collection: this.facets
- });
- this.layout.facetsRegion.show(this.facetsView);
-
- this.headerView = new HeaderView({
- app: this
- });
- this.layout.filtersRegion.show(this.headerView);
-
- key.setScope('list');
- App.router = new Router({ app: App });
- Backbone.history.start();
-};
-
-App.on('start', el => {
- init.call(App, el);
-});
-
-export default function(el, user) {
- App.start({ el, user });
-
- return () => {
- Backbone.history.stop();
- App.layout.destroy();
- $('#footer').removeClass('search-navigator-footer');
- };
-}
diff --git a/server/sonar-web/src/main/js/apps/issues/issue-filter-view.js b/server/sonar-web/src/main/js/apps/issues/issue-filter-view.js
deleted file mode 100644
index c3f45efbfe9..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/issue-filter-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 '../../components/common/action-options-view';
-import Template from './templates/issues-issue-filter-form.hbs';
-
-export default ActionOptionsView.extend({
- template: Template,
-
- selectOption(e) {
- const property = $(e.currentTarget).data('property');
- const value = $(e.currentTarget).data('value');
- this.trigger('select', property, value);
- ActionOptionsView.prototype.selectOption.apply(this, arguments);
- },
-
- serializeData() {
- return {
- ...ActionOptionsView.prototype.serializeData.apply(this, arguments),
- s: this.model.get('severity')
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/layout.js b/server/sonar-web/src/main/js/apps/issues/layout.js
deleted file mode 100644
index c796baccb9c..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/layout.js
+++ /dev/null
@@ -1,62 +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 Marionette from 'backbone.marionette';
-import Template from './templates/issues-layout.hbs';
-import './styles.css';
-
-export default Marionette.LayoutView.extend({
- template: Template,
-
- regions: {
- filtersRegion: '.issues-header',
- facetsRegion: '.search-navigator-facets',
- workspaceHeaderRegion: '.search-navigator-workspace-header',
- workspaceListRegion: '.search-navigator-workspace-list',
- workspaceComponentViewerRegion: '.issues-workspace-component-viewer'
- },
-
- onRender() {
- this.$('.search-navigator').addClass('sticky');
- const top = this.$('.search-navigator').offset().top;
- this.$('.search-navigator-workspace-header').css({ top });
- this.$('.search-navigator-side').css({ top }).isolatedScroll();
- },
-
- showSpinner(region) {
- return this[region].show(
- new Marionette.ItemView({
- template: () => '<i class="spinner"></i>'
- })
- );
- },
-
- showComponentViewer() {
- this.scroll = $(window).scrollTop();
- this.$('.issues').addClass('issues-extended-view');
- },
-
- hideComponentViewer() {
- this.$('.issues').removeClass('issues-extended-view');
- if (this.scroll != null) {
- $(window).scrollTop(this.scroll);
- }
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/models/issues.js b/server/sonar-web/src/main/js/apps/issues/models/issues.js
deleted file mode 100644
index 9ea6454a484..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/models/issues.js
+++ /dev/null
@@ -1,108 +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 './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;
- },
-
- parseIssues(r, startIndex = 0) {
- const that = this;
- return r.issues.map((issue, index) => {
- Object.assign(issue, { index: startIndex + index });
- 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;
- });
- },
-
- setIndex() {
- return this.forEach((issue, index) => issue.set({ index }));
- },
-
- selectByKeys(keys) {
- const that = this;
- keys.forEach(key => {
- const issue = that.get(key);
- if (issue) {
- issue.set({ selected: true });
- }
- });
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/models/state.js b/server/sonar-web/src/main/js/apps/issues/models/state.js
deleted file mode 100644
index b367cc670b0..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/models/state.js
+++ /dev/null
@@ -1,81 +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 State from '../../../components/navigator/models/state';
-
-export default State.extend({
- defaults: {
- page: 1,
- maxResultsReached: false,
- query: {},
- facets: ['facetMode', 'types', 'resolutions'],
- isContext: false,
- allFacets: [
- 'facetMode',
- 'issues',
- 'types',
- 'resolutions',
- 'severities',
- 'statuses',
- 'createdAt',
- 'rules',
- 'tags',
- 'projectUuids',
- 'moduleUuids',
- 'directories',
- 'fileUuids',
- 'assignees',
- 'authors',
- 'languages'
- ],
- facetsFromServer: [
- 'types',
- 'severities',
- 'statuses',
- 'resolutions',
- 'projectUuids',
- 'directories',
- 'rules',
- 'moduleUuids',
- 'tags',
- 'assignees',
- 'authors',
- 'fileUuids',
- 'languages',
- 'createdAt'
- ],
- transform: {
- resolved: 'resolutions',
- assigned: 'assignees',
- createdBefore: 'createdAt',
- createdAfter: 'createdAt',
- sinceLeakPeriod: 'createdAt',
- createdInLast: 'createdAt'
- }
- },
-
- getFacetMode() {
- const query = this.get('query');
- return query.facetMode || 'count';
- },
-
- toJSON() {
- return { facetMode: this.getFacetMode(), ...this.attributes };
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/redirects.js b/server/sonar-web/src/main/js/apps/issues/redirects.js
new file mode 100644
index 00000000000..3efc1af64be
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/redirects.js
@@ -0,0 +1,55 @@
+/*
+ * 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 { parseQuery, areMyIssuesSelected, serializeQuery } from './utils';
+import type { RawQuery } from './utils';
+
+const parseHash = (hash: string): RawQuery => {
+ const query: RawQuery = {};
+ const parts = hash.split('|');
+ parts.forEach(part => {
+ const tokens = part.split('=');
+ if (tokens.length === 2) {
+ const property = decodeURIComponent(tokens[0]);
+ const value = decodeURIComponent(tokens[1]);
+ if (property === 'assigned_to_me' && value === 'true') {
+ query.myIssues = 'true';
+ } else {
+ query[property] = value;
+ }
+ }
+ });
+ return query;
+};
+
+export const onEnter = (state: Object, replace: Function) => {
+ const { hash } = window.location;
+ if (hash.length > 1) {
+ const query = parseHash(hash.substr(1));
+ const normalizedQuery = {
+ ...serializeQuery(parseQuery(query)),
+ myIssues: areMyIssuesSelected(query) ? 'true' : undefined
+ };
+ replace({
+ pathname: state.location.pathname,
+ query: normalizedQuery
+ });
+ }
+};
diff --git a/server/sonar-web/src/main/js/apps/issues/router.js b/server/sonar-web/src/main/js/apps/issues/router.js
deleted file mode 100644
index ac35322a355..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/router.js
+++ /dev/null
@@ -1,35 +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 Router from '../../components/navigator/router';
-
-export default Router.extend({
- routes: {
- '': 'home',
- ':query': 'index'
- },
-
- home() {
- return this.navigate('resolved=false', { trigger: true, replace: true });
- },
-
- index(query) {
- this.options.app.state.setQuery(this.options.app.controller.parseQuery(query));
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/routes.js b/server/sonar-web/src/main/js/apps/issues/routes.js
index 5f3a27c905c..61893ce7fec 100644
--- a/server/sonar-web/src/main/js/apps/issues/routes.js
+++ b/server/sonar-web/src/main/js/apps/issues/routes.js
@@ -17,13 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { onEnter } from './redirects';
+
const routes = [
{
- indexRoute: {
- getComponent(_, callback) {
- require.ensure([], require =>
- callback(null, require('./components/IssuesAppContainer').default));
- }
+ getIndexRoute(_, callback) {
+ require.ensure([], require =>
+ callback(null, {
+ component: require('./components/AppContainer').default,
+ onEnter
+ }));
}
}
];
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js
new file mode 100644
index 00000000000..cf7bb4be8f2
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js
@@ -0,0 +1,168 @@
+/*
+ * 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 { sortBy, uniq, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import FacetFooter from './components/FacetFooter';
+import { searchAssignees } from '../utils';
+import type { ReferencedUser, Component } from '../utils';
+import Avatar from '../../../components/ui/Avatar';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ assigned: boolean,
+ assignees: Array<string>,
+ component?: Component,
+ facetMode: string,
+ onChange: (changes: {}) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ referencedUsers: { [string]: ReferencedUser }
+|};
+
+export default class AssigneeFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'assignees';
+
+ handleItemClick = (itemValue: string) => {
+ if (itemValue === '') {
+ // unassigned
+ this.props.onChange({ assigned: !this.props.assigned, assignees: [] });
+ } else {
+ // defined assignee
+ const { assignees } = this.props;
+ const newValue = sortBy(
+ assignees.includes(itemValue) ? without(assignees, itemValue) : [...assignees, itemValue]
+ );
+ this.props.onChange({ assigned: true, assignees: newValue });
+ }
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ handleSearch = (query: string) => searchAssignees(query, this.props.component);
+
+ handleSelect = (assignee: string) => {
+ const { assignees } = this.props;
+ this.props.onChange({ assigned: true, [this.property]: uniq([...assignees, assignee]) });
+ };
+
+ isAssigneeActive(assignee: string) {
+ return assignee === '' ? !this.props.assigned : this.props.assignees.includes(assignee);
+ }
+
+ getAssigneeName(assignee: string): React.Element<*> | string {
+ if (assignee === '') {
+ return translate('unassigned');
+ } else {
+ const { referencedUsers } = this.props;
+ if (referencedUsers[assignee]) {
+ return (
+ <span>
+ <Avatar
+ className="little-spacer-right"
+ hash={referencedUsers[assignee].avatar}
+ size={16}
+ />
+ {referencedUsers[assignee].name}
+ </span>
+ );
+ } else {
+ return assignee;
+ }
+ }
+ }
+
+ getStat(assignee: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[assignee] : null;
+ }
+
+ renderOption = (option: { avatar: string, label: string }) => {
+ return (
+ <span>
+ {option.avatar != null &&
+ <Avatar className="little-spacer-right" hash={option.avatar} size={16} />}
+ {option.label}
+ </span>
+ );
+ };
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const assignees = sortBy(
+ Object.keys(stats),
+ // put unassigned first
+ key => key === '' ? 0 : 1,
+ // the sort by number
+ key => -stats[key]
+ );
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={!this.props.assigned || this.props.assignees.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {assignees.map(assignee => (
+ <FacetItem
+ active={this.isAssigneeActive(assignee)}
+ facetMode={this.props.facetMode}
+ key={assignee}
+ name={this.getAssigneeName(assignee)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(assignee)}
+ value={assignee}
+ />
+ ))}
+ </FacetItemsList>}
+
+ {this.props.open &&
+ <FacetFooter
+ onSearch={this.handleSearch}
+ onSelect={this.handleSelect}
+ renderOption={this.renderOption}
+ />}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js
new file mode 100644
index 00000000000..d539229ff78
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js
@@ -0,0 +1,99 @@
+/*
+ * 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 { sortBy, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: {}) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ authors: Array<string>
+|};
+
+export default class AuthorFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'authors';
+
+ handleItemClick = (itemValue: string) => {
+ const { authors } = this.props;
+ const newValue = sortBy(
+ authors.includes(itemValue) ? without(authors, itemValue) : [...authors, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ getStat(author: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[author] : null;
+ }
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const authors = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.authors.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {authors.map(author => (
+ <FacetItem
+ active={this.props.authors.includes(author)}
+ facetMode={this.props.facetMode}
+ key={author}
+ name={author}
+ onClick={this.handleItemClick}
+ stat={this.getStat(author)}
+ value={author}
+ />
+ ))}
+ </FacetItemsList>}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js
new file mode 100644
index 00000000000..dd28ccfddea
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js
@@ -0,0 +1,276 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import moment from 'moment';
+import { max } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import { BarChart } from '../../../components/charts/bar-chart';
+import DateInput from '../../../components/controls/DateInput';
+import { translate } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+import type { Component } from '../utils';
+
+type Props = {|
+ component?: Component,
+ createdAfter: string,
+ createdAt: string,
+ createdBefore: string,
+ createdInLast: string,
+ facetMode: string,
+ onChange: (changes: {}) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ sinceLeakPeriod: boolean,
+ stats?: { [string]: number }
+|};
+
+const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZZ';
+
+export default class CreationDateFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'createdAt';
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ resetTo = (changes: {}) => {
+ this.props.onChange({
+ createdAfter: undefined,
+ createdAt: undefined,
+ createdBefore: undefined,
+ createdInLast: undefined,
+ sinceLeakPeriod: undefined,
+ ...changes
+ });
+ };
+
+ handleBarClick = (
+ { createdAfter, createdBefore }: { createdAfter: Object, createdBefore?: Object }
+ ) => {
+ this.resetTo({
+ createdAfter: createdAfter.format(DATE_FORMAT),
+ createdBefore: createdBefore && createdBefore.format(DATE_FORMAT)
+ });
+ };
+
+ handlePeriodChange = (property: string) =>
+ (value: string) => {
+ this.props.onChange({
+ createdAt: undefined,
+ createdInLast: undefined,
+ sinceLeakPeriod: undefined,
+ [property]: value
+ });
+ };
+
+ handlePeriodClick = (period?: string) =>
+ (e: Event & { target: HTMLElement }) => {
+ e.preventDefault();
+ e.target.blur;
+ this.resetTo({ createdInLast: period });
+ };
+
+ handleLeakPeriodClick = () =>
+ (e: Event & { target: HTMLElement }) => {
+ e.preventDefault();
+ e.target.blur;
+ this.resetTo({ sinceLeakPeriod: true });
+ };
+
+ renderBarChart() {
+ const { createdBefore, stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const periods = Object.keys(stats);
+
+ if (periods.length < 2) {
+ return null;
+ }
+
+ const data = periods.map((startDate, index) => {
+ const startMoment = moment(startDate);
+ const nextStartMoment = index < periods.length - 1
+ ? moment(periods[index + 1])
+ : createdBefore ? moment(createdBefore) : undefined;
+ const endMoment = nextStartMoment && nextStartMoment.clone().subtract(1, 'days');
+
+ let tooltip = formatMeasure(stats[startDate], 'SHORT_INT') +
+ '<br>' +
+ startMoment.format('LL');
+
+ if (endMoment) {
+ const isSameDay = endMoment.diff(startMoment, 'days') <= 1;
+ if (!isSameDay) {
+ tooltip += ' – ' + endMoment.format('LL');
+ }
+ }
+
+ return {
+ createdAfter: startMoment,
+ createdBefore: nextStartMoment,
+ startMoment,
+ tooltip,
+ x: index,
+ y: stats[startDate]
+ };
+ });
+
+ const barsWidth = Math.floor(240 / data.length);
+ const width = barsWidth * data.length - 1 + 20;
+
+ const maxValue = max(data.map(d => d.y));
+ const format = this.props.facetMode === 'count' ? 'SHORT_INT' : 'SHORT_WORK_DUR';
+ const xValues = data.map(d => d.y === maxValue ? formatMeasure(maxValue, format) : '');
+
+ return (
+ <BarChart
+ barsWidth={barsWidth - 1}
+ data={data}
+ height={75}
+ onBarClick={this.handleBarClick}
+ padding={[25, 10, 5, 10]}
+ width={width}
+ xValues={xValues}
+ />
+ );
+ }
+
+ renderExactDate() {
+ const m = moment(this.props.createdAt);
+ return (
+ <div className="search-navigator-facet-container">
+ {m.format('LLL')}
+ <br />
+ <span className="note">({m.fromNow()})</span>
+ </div>
+ );
+ }
+
+ renderPeriodSelectors() {
+ const { createdAfter, createdBefore } = this.props;
+
+ return (
+ <div className="search-navigator-date-facet-selection">
+ <DateInput
+ className="search-navigator-date-facet-selection-dropdown-left"
+ onChange={this.handlePeriodChange('createdAfter')}
+ placeholder={translate('from')}
+ value={createdAfter ? moment(createdAfter).format('YYYY-MM-DD') : undefined}
+ />
+ <DateInput
+ className="search-navigator-date-facet-selection-dropdown-right"
+ onChange={this.handlePeriodChange('createdBefore')}
+ placeholder={translate('to')}
+ value={createdBefore ? moment(createdBefore).format('YYYY-MM-DD') : undefined}
+ />
+ </div>
+ );
+ }
+
+ renderPrefefinedPeriods() {
+ const { component, createdInLast, sinceLeakPeriod } = this.props;
+ return (
+ <div className="spacer-top">
+ <span className="spacer-right">{translate('issues.facet.createdAt.or')}</span>
+ <a className="spacer-right" href="#" onClick={this.handlePeriodClick()}>
+ {translate('issues.facet.createdAt.all')}
+ </a>
+ {component == null &&
+ <a
+ className={classNames('spacer-right', { 'active-link': createdInLast === '1w' })}
+ href="#"
+ onClick={this.handlePeriodClick('1w')}>
+ {translate('issues.facet.createdAt.last_week')}
+ </a>}
+ {component == null &&
+ <a
+ className={classNames('spacer-right', { 'active-link': createdInLast === '1m' })}
+ href="#"
+ onClick={this.handlePeriodClick('1m')}>
+ {translate('issues.facet.createdAt.last_month')}
+ </a>}
+ {component == null &&
+ <a
+ className={classNames('spacer-right', { 'active-link': createdInLast === '1y' })}
+ href="#"
+ onClick={this.handlePeriodClick('1y')}>
+ {translate('issues.facet.createdAt.last_year')}
+ </a>}
+ {component != null &&
+ <a
+ className={classNames('spacer-right', { 'active-link': sinceLeakPeriod })}
+ href="#"
+ onClick={this.handleLeakPeriodClick()}>
+ {translate('issues.leak_period')}
+ </a>}
+ </div>
+ );
+ }
+
+ renderInner() {
+ const { createdAt } = this.props;
+ return createdAt
+ ? this.renderExactDate()
+ : <div>
+ {this.renderBarChart()}
+ {this.renderPeriodSelectors()}
+ {this.renderPrefefinedPeriods()}
+ </div>;
+ }
+
+ render() {
+ const hasValue = this.props.createdAfter.length > 0 ||
+ this.props.createdAt.length > 0 ||
+ this.props.createdBefore.length > 0 ||
+ this.props.createdInLast.length > 0 ||
+ this.props.sinceLeakPeriod;
+
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={hasValue}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open && this.renderInner()}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js
new file mode 100644
index 00000000000..ddd82db1eab
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js
@@ -0,0 +1,120 @@
+/*
+ * 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 { sortBy, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import type { ReferencedComponent } from '../utils';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ referencedComponents: { [string]: ReferencedComponent },
+ directories: Array<string>
+|};
+
+export default class DirectoryFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'directories';
+
+ handleItemClick = (itemValue: string) => {
+ const { directories } = this.props;
+ const newValue = sortBy(
+ directories.includes(itemValue)
+ ? without(directories, itemValue)
+ : [...directories, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ getStat(directory: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[directory] : null;
+ }
+
+ renderName(directory: string): React.Element<*> | string {
+ // `referencedComponents` are indexed by uuid
+ // so we have to browse them all to find a matching one
+ const { referencedComponents } = this.props;
+ const uuid = Object.keys(referencedComponents).find(
+ uuid => referencedComponents[uuid].key === directory
+ );
+ const name = uuid ? referencedComponents[uuid].name : directory;
+ return (
+ <span>
+ <QualifierIcon className="little-spacer-right" qualifier="DIR" />
+ {name}
+ </span>
+ );
+ }
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const directories = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.directories.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {directories.map(directory => (
+ <FacetItem
+ active={this.props.directories.includes(directory)}
+ facetMode={this.props.facetMode}
+ key={directory}
+ name={this.renderName(directory)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(directory)}
+ value={directory}
+ />
+ ))}
+ </FacetItemsList>}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js b/server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js
new file mode 100644
index 00000000000..dcfb16202e9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FacetMode.js
@@ -0,0 +1,67 @@
+/*
+ * 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 FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: {}) => void
+|};
+
+export default class FacetMode extends React.PureComponent {
+ props: Props;
+
+ property = 'facetMode';
+
+ handleItemClick = (itemValue: string) => {
+ this.props.onChange({ [this.property]: itemValue });
+ };
+
+ render() {
+ const { facetMode } = this.props;
+ const modes = ['count', 'effort'];
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader name={translate('issues.facet.mode')} />
+
+ <FacetItemsList>
+ {modes.map(mode => (
+ <FacetItem
+ active={facetMode === mode}
+ facetMode={this.props.facetMode}
+ halfWidth={true}
+ key={mode}
+ name={translate('issues.facet.mode', mode)}
+ onClick={this.handleItemClick}
+ stat={null}
+ value={mode}
+ />
+ ))}
+ </FacetItemsList>
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js
new file mode 100644
index 00000000000..5d914f8380d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js
@@ -0,0 +1,116 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { sortBy, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import type { ReferencedComponent } from '../utils';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
+import { translate } from '../../../helpers/l10n';
+import { collapsePath } from '../../../helpers/path';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ referencedComponents: { [string]: ReferencedComponent },
+ files: Array<string>
+|};
+
+export default class FileFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'files';
+
+ handleItemClick = (itemValue: string) => {
+ const { files } = this.props;
+ const newValue = sortBy(
+ files.includes(itemValue) ? without(files, itemValue) : [...files, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ getStat(file: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[file] : null;
+ }
+
+ renderName(file: string): React.Element<*> | string {
+ const { referencedComponents } = this.props;
+ const name = referencedComponents[file]
+ ? collapsePath(referencedComponents[file].path, 15)
+ : file;
+ return (
+ <span>
+ <QualifierIcon className="little-spacer-right" qualifier="FIL" />
+ {name}
+ </span>
+ );
+ }
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const files = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.files.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {files.map(file => (
+ <FacetItem
+ active={this.props.files.includes(file)}
+ facetMode={this.props.facetMode}
+ key={file}
+ name={this.renderName(file)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(file)}
+ value={file}
+ />
+ ))}
+ </FacetItemsList>}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js
new file mode 100644
index 00000000000..411c9b74d2e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js
@@ -0,0 +1,114 @@
+/*
+ * 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 { sortBy, uniq, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import LanguageFacetFooter from './LanguageFacetFooter';
+import type { ReferencedLanguage } from '../utils';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ referencedLanguages: { [string]: ReferencedLanguage },
+ languages: Array<string>
+|};
+
+export default class LanguageFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'languages';
+
+ handleItemClick = (itemValue: string) => {
+ const { languages } = this.props;
+ const newValue = sortBy(
+ languages.includes(itemValue) ? without(languages, itemValue) : [...languages, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ getLanguageName(language: string): string {
+ const { referencedLanguages } = this.props;
+ return referencedLanguages[language] ? referencedLanguages[language].name : language;
+ }
+
+ getStat(language: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[language] : null;
+ }
+
+ handleSelect = (language: string) => {
+ const { languages } = this.props;
+ this.props.onChange({ [this.property]: uniq([...languages, language]) });
+ };
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const languages = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.languages.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {languages.map(language => (
+ <FacetItem
+ active={this.props.languages.includes(language)}
+ facetMode={this.props.facetMode}
+ key={language}
+ name={this.getLanguageName(language)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(language)}
+ value={language}
+ />
+ ))}
+ </FacetItemsList>}
+
+ {this.props.open && <LanguageFacetFooter onSelect={this.handleSelect} />}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js
new file mode 100644
index 00000000000..4ad3bc32825
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js
@@ -0,0 +1,68 @@
+/*
+ * 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 Select from 'react-select';
+import { connect } from 'react-redux';
+import { translate } from '../../../helpers/l10n';
+import { getLanguages } from '../../../store/rootReducer';
+
+type Option = { label: string, value: string };
+
+type Props = {|
+ languages: Array<{ key: string, name: string }>,
+ onSelect: (value: string) => void
+|};
+
+class LanguageFacetFooter extends React.PureComponent {
+ props: Props;
+
+ handleChange = (option: Option) => {
+ this.props.onSelect(option.value);
+ };
+
+ render() {
+ const options = this.props.languages.map(language => ({
+ label: language.name,
+ value: language.key
+ }));
+
+ return (
+ <div className="search-navigator-facet-footer">
+ <Select
+ autofocus={true}
+ className="input-super-large"
+ clearable={false}
+ noResultsText={translate('select2.noMatches')}
+ onChange={this.handleChange}
+ options={options}
+ placeholder={translate('search_verb')}
+ searchable={true}
+ />
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ languages: Object.values(getLanguages(state))
+});
+
+export default connect(mapStateToProps)(LanguageFacetFooter);
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js
new file mode 100644
index 00000000000..8711e017462
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { sortBy, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import type { ReferencedComponent } from '../utils';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ referencedComponents: { [string]: ReferencedComponent },
+ modules: Array<string>
+|};
+
+export default class ModuleFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'modules';
+
+ handleItemClick = (itemValue: string) => {
+ const { modules } = this.props;
+ const newValue = sortBy(
+ modules.includes(itemValue) ? without(modules, itemValue) : [...modules, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ getStat(module: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[module] : null;
+ }
+
+ renderName(module: string): React.Element<*> | string {
+ const { referencedComponents } = this.props;
+ const name = referencedComponents[module] ? referencedComponents[module].name : module;
+ return (
+ <span>
+ <QualifierIcon className="little-spacer-right" qualifier="BRC" />
+ {name}
+ </span>
+ );
+ }
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const modules = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.modules.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {modules.map(module => (
+ <FacetItem
+ active={this.props.modules.includes(module)}
+ facetMode={this.props.facetMode}
+ key={module}
+ name={this.renderName(module)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(module)}
+ value={module}
+ />
+ ))}
+ </FacetItemsList>}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js
new file mode 100644
index 00000000000..2b7046f69ef
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js
@@ -0,0 +1,160 @@
+/*
+ * 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 { sortBy, uniq, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import FacetFooter from './components/FacetFooter';
+import type { ReferencedComponent, Component } from '../utils';
+import Organization from '../../../components/shared/Organization';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
+import { searchComponents, getTree } from '../../../api/components';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ component?: Component,
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ referencedComponents: { [string]: ReferencedComponent },
+ projects: Array<string>
+|};
+
+export default class ProjectFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'projects';
+
+ handleItemClick = (itemValue: string) => {
+ const { projects } = this.props;
+ const newValue = sortBy(
+ projects.includes(itemValue) ? without(projects, itemValue) : [...projects, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ handleSearch = (query: string) => {
+ const { component } = this.props;
+
+ return component != null && ['VW', 'SVW'].includes(component.qualifier)
+ ? getTree(component.key, { ps: 50, q: query, qualifiers: 'TRK' }).then(response =>
+ response.components.map(component => ({
+ label: component.name,
+ organization: component.organization,
+ value: component.refId
+ })))
+ : searchComponents({ ps: 50, q: query, qualifiers: 'TRK' }).then(response =>
+ response.components.map(component => ({
+ label: component.name,
+ organization: component.organization,
+ value: component.id
+ })));
+ };
+
+ handleSelect = (rule: string) => {
+ const { projects } = this.props;
+ this.props.onChange({ [this.property]: uniq([...projects, rule]) });
+ };
+
+ getStat(project: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[project] : null;
+ }
+
+ renderName(project: string): React.Element<*> | string {
+ const { referencedComponents } = this.props;
+ return referencedComponents[project]
+ ? <span>
+ <QualifierIcon className="little-spacer-right" qualifier="TRK" />
+ <Organization link={false} organizationKey={referencedComponents[project].organization} />
+ {referencedComponents[project].name}
+ </span>
+ : <span>
+ <QualifierIcon className="little-spacer-right" qualifier="TRK" />
+ {project}
+ </span>;
+ }
+
+ renderOption = (option: { label: string, organization: string }) => {
+ return (
+ <span>
+ <Organization link={false} organizationKey={option.organization} />
+ {option.label}
+ </span>
+ );
+ };
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const projects = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.projects.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {projects.map(project => (
+ <FacetItem
+ active={this.props.projects.includes(project)}
+ facetMode={this.props.facetMode}
+ key={project}
+ name={this.renderName(project)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(project)}
+ value={project}
+ />
+ ))}
+ </FacetItemsList>}
+
+ {this.props.open &&
+ <FacetFooter
+ minimumQueryLength={3}
+ onSearch={this.handleSearch}
+ onSelect={this.handleSelect}
+ renderOption={this.renderOption}
+ />}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js
new file mode 100644
index 00000000000..d83c56cd917
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { orderBy, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: {}) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ resolved: boolean,
+ resolutions: Array<string>,
+ stats?: { [string]: number }
+|};
+
+export default class ResolutionFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'resolutions';
+
+ handleItemClick = (itemValue: string) => {
+ if (itemValue === '') {
+ // unresolved
+ this.props.onChange({ resolved: !this.props.resolved, resolutions: [] });
+ } else {
+ // defined resolution
+ const { resolutions } = this.props;
+ const newValue = orderBy(
+ resolutions.includes(itemValue)
+ ? without(resolutions, itemValue)
+ : [...resolutions, itemValue]
+ );
+ this.props.onChange({ resolved: true, resolutions: newValue });
+ }
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ isFacetItemActive(resolution: string) {
+ return resolution === '' ? !this.props.resolved : this.props.resolutions.includes(resolution);
+ }
+
+ getFacetItemName(resolution: string) {
+ return resolution === '' ? translate('unresolved') : translate('issue.resolution', resolution);
+ }
+
+ getStat(resolution: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[resolution] : null;
+ }
+
+ renderItem = (resolution: string) => {
+ const active = this.isFacetItemActive(resolution);
+ const stat = this.getStat(resolution);
+
+ return (
+ <FacetItem
+ active={active}
+ disabled={stat === 0 && !active}
+ facetMode={this.props.facetMode}
+ key={resolution}
+ halfWidth={true}
+ name={this.getFacetItemName(resolution)}
+ onClick={this.handleItemClick}
+ stat={stat}
+ value={resolution}
+ />
+ );
+ };
+
+ render() {
+ const resolutions = ['', 'FIXED', 'FALSE-POSITIVE', 'WONTFIX', 'REMOVED'];
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={!this.props.resolved || this.props.resolutions.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {resolutions.map(this.renderItem)}
+ </FacetItemsList>}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js
new file mode 100644
index 00000000000..ce5d5d49468
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js
@@ -0,0 +1,126 @@
+/*
+ * 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 { sortBy, uniq, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import FacetFooter from './components/FacetFooter';
+import { searchRules } from '../../../api/rules';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ languages: Array<string>,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ referencedRules: { [string]: { name: string } },
+ rules: Array<string>
+|};
+
+export default class RuleFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'rules';
+
+ handleItemClick = (itemValue: string) => {
+ const { rules } = this.props;
+ const newValue = sortBy(
+ rules.includes(itemValue) ? without(rules, itemValue) : [...rules, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ handleSearch = (query: string) => {
+ const { languages } = this.props;
+ return searchRules({
+ f: 'name,langName',
+ languages: languages.length ? languages.join() : undefined,
+ q: query
+ }).then(response =>
+ response.rules.map(rule => ({ label: `(${rule.langName}) ${rule.name}`, value: rule.key })));
+ };
+
+ handleSelect = (rule: string) => {
+ const { rules } = this.props;
+ this.props.onChange({ [this.property]: uniq([...rules, rule]) });
+ };
+
+ getRuleName(rule: string): string {
+ const { referencedRules } = this.props;
+ return referencedRules[rule] ? referencedRules[rule].name : rule;
+ }
+
+ getStat(rule: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[rule] : null;
+ }
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const rules = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.rules.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {rules.map(rule => (
+ <FacetItem
+ active={this.props.rules.includes(rule)}
+ facetMode={this.props.facetMode}
+ key={rule}
+ name={this.getRuleName(rule)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(rule)}
+ value={rule}
+ />
+ ))}
+ </FacetItemsList>}
+
+ {this.props.open &&
+ <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js
index cadec2974e3..e95f44008d0 100644
--- a/server/sonar-web/src/main/js/apps/issues2/sidebar/SeverityFacet.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js
@@ -19,7 +19,7 @@
*/
// @flow
import React from 'react';
-import { orderBy, uniq, without } from 'lodash';
+import { orderBy, without } from 'lodash';
import FacetBox from './components/FacetBox';
import FacetHeader from './components/FacetHeader';
import FacetItem from './components/FacetItem';
@@ -28,6 +28,7 @@ import SeverityHelper from '../../../components/shared/SeverityHelper';
import { translate } from '../../../helpers/l10n';
type Props = {|
+ facetMode: string,
onChange: (changes: { [string]: Array<string> }) => void,
onToggle: (property: string) => void,
open: boolean,
@@ -47,9 +48,7 @@ export default class SeverityFacet extends React.PureComponent {
handleItemClick = (itemValue: string) => {
const { severities } = this.props;
const newValue = orderBy(
- severities.includes(itemValue)
- ? without(severities, itemValue)
- : uniq([...severities, itemValue])
+ severities.includes(itemValue) ? without(severities, itemValue) : [...severities, itemValue]
);
this.props.onChange({ [this.property]: newValue });
};
@@ -63,6 +62,25 @@ export default class SeverityFacet extends React.PureComponent {
return stats ? stats[severity] : null;
}
+ renderItem = (severity: string) => {
+ const active = this.props.severities.includes(severity);
+ const stat = this.getStat(severity);
+
+ return (
+ <FacetItem
+ active={active}
+ disabled={stat === 0 && !active}
+ facetMode={this.props.facetMode}
+ halfWidth={true}
+ key={severity}
+ name={<SeverityHelper severity={severity} />}
+ onClick={this.handleItemClick}
+ stat={stat}
+ value={severity}
+ />
+ );
+ };
+
render() {
const severities = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR'];
@@ -75,19 +93,10 @@ export default class SeverityFacet extends React.PureComponent {
open={this.props.open}
/>
- <FacetItemsList open={this.props.open}>
- {severities.map(severity => (
- <FacetItem
- active={this.props.severities.includes(severity)}
- halfWidth={true}
- key={severity}
- name={<SeverityHelper severity={severity} />}
- onClick={this.handleItemClick}
- stat={this.getStat(severity)}
- value={severity}
- />
- ))}
- </FacetItemsList>
+ {this.props.open &&
+ <FacetItemsList>
+ {severities.map(this.renderItem)}
+ </FacetItemsList>}
</FacetBox>
);
}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js
new file mode 100644
index 00000000000..fd4c2c3f91c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js
@@ -0,0 +1,212 @@
+/*
+ * 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 AssigneeFacet from './AssigneeFacet';
+import AuthorFacet from './AuthorFacet';
+import CreationDateFacet from './CreationDateFacet';
+import DirectoryFacet from './DirectoryFacet';
+import FacetMode from './FacetMode';
+import FileFacet from './FileFacet';
+import LanguageFacet from './LanguageFacet';
+import ModuleFacet from './ModuleFacet';
+import ProjectFacet from './ProjectFacet';
+import ResolutionFacet from './ResolutionFacet';
+import RuleFacet from './RuleFacet';
+import SeverityFacet from './SeverityFacet';
+import StatusFacet from './StatusFacet';
+import TagFacet from './TagFacet';
+import TypeFacet from './TypeFacet';
+import type {
+ Query,
+ Facet,
+ ReferencedComponent,
+ ReferencedUser,
+ ReferencedLanguage,
+ Component
+} from '../utils';
+
+type Props = {|
+ component?: Component,
+ facets: { [string]: Facet },
+ myIssues: boolean,
+ onFacetToggle: (property: string) => void,
+ onFilterChange: (changes: { [string]: Array<string> }) => void,
+ openFacets: { [string]: boolean },
+ query: Query,
+ referencedComponents: { [string]: ReferencedComponent },
+ referencedLanguages: { [string]: ReferencedLanguage },
+ referencedRules: { [string]: { name: string } },
+ referencedUsers: { [string]: ReferencedUser }
+|};
+
+export default class Sidebar extends React.PureComponent {
+ props: Props;
+
+ render() {
+ const { component, facets, openFacets, query } = this.props;
+
+ const displayProjectsFacet: boolean = component == null ||
+ !['TRK', 'BRC', 'DIR', 'DEV_PRJ'].includes(component.qualifier);
+ const displayModulesFacet = component == null || component.qualifier !== 'DIR';
+ const displayDirectoriesFacet = component == null || component.qualifier !== 'DIR';
+ const displayAuthorFacet = component == null || component.qualifier !== 'DEV';
+
+ return (
+ <div className="search-navigator-facets-list">
+ <FacetMode facetMode={query.facetMode} onChange={this.props.onFilterChange} />
+ <TypeFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.types}
+ stats={facets.types}
+ types={query.types}
+ />
+ <ResolutionFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.resolutions}
+ resolved={query.resolved}
+ resolutions={query.resolutions}
+ stats={facets.resolutions}
+ />
+ <SeverityFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.severities}
+ severities={query.severities}
+ stats={facets.severities}
+ />
+ <StatusFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.statuses}
+ stats={facets.statuses}
+ statuses={query.statuses}
+ />
+ <CreationDateFacet
+ component={component}
+ createdAfter={query.createdAfter}
+ createdAt={query.createdAt}
+ createdBefore={query.createdBefore}
+ createdInLast={query.createdInLast}
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.createdAt}
+ sinceLeakPeriod={query.sinceLeakPeriod}
+ stats={facets.createdAt}
+ />
+ <RuleFacet
+ facetMode={query.facetMode}
+ languages={query.languages}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.rules}
+ stats={facets.rules}
+ referencedRules={this.props.referencedRules}
+ rules={query.rules}
+ />
+ <TagFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.tags}
+ stats={facets.tags}
+ tags={query.tags}
+ />
+ {displayProjectsFacet &&
+ <ProjectFacet
+ component={component}
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.projects}
+ projects={query.projects}
+ referencedComponents={this.props.referencedComponents}
+ stats={facets.projects}
+ />}
+ {displayModulesFacet &&
+ <ModuleFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.modules}
+ modules={query.modules}
+ referencedComponents={this.props.referencedComponents}
+ stats={facets.modules}
+ />}
+ {displayDirectoriesFacet &&
+ <DirectoryFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.directories}
+ directories={query.directories}
+ referencedComponents={this.props.referencedComponents}
+ stats={facets.directories}
+ />}
+ <FileFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.files}
+ files={query.files}
+ referencedComponents={this.props.referencedComponents}
+ stats={facets.files}
+ />
+ {!this.props.myIssues &&
+ <AssigneeFacet
+ component={component}
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.assignees}
+ assigned={query.assigned}
+ assignees={query.assignees}
+ referencedUsers={this.props.referencedUsers}
+ stats={facets.assignees}
+ />}
+ {displayAuthorFacet &&
+ <AuthorFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.authors}
+ authors={query.authors}
+ stats={facets.authors}
+ />}
+ <LanguageFacet
+ facetMode={query.facetMode}
+ onChange={this.props.onFilterChange}
+ onToggle={this.props.onFacetToggle}
+ open={!!openFacets.languages}
+ languages={query.languages}
+ referencedLanguages={this.props.referencedLanguages}
+ stats={facets.languages}
+ />
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js
new file mode 100644
index 00000000000..40bfb251141
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js
@@ -0,0 +1,112 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { orderBy, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ statuses: Array<string>
+|};
+
+export default class StatusFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'statuses';
+
+ handleItemClick = (itemValue: string) => {
+ const { statuses } = this.props;
+ const newValue = orderBy(
+ statuses.includes(itemValue) ? without(statuses, itemValue) : [...statuses, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ getStat(status: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[status] : null;
+ }
+
+ renderStatus(status: string) {
+ return (
+ <span>
+ <i className={`icon-status-${status.toLowerCase()}`} />
+ {' '}
+ {translate('issue.status', status)}
+ </span>
+ );
+ }
+
+ renderItem = (status: string) => {
+ const active = this.props.statuses.includes(status);
+ const stat = this.getStat(status);
+
+ return (
+ <FacetItem
+ active={active}
+ disabled={stat === 0 && !active}
+ facetMode={this.props.facetMode}
+ halfWidth={true}
+ key={status}
+ name={this.renderStatus(status)}
+ onClick={this.handleItemClick}
+ stat={stat}
+ value={status}
+ />
+ );
+ };
+
+ render() {
+ const statuses = ['OPEN', 'RESOLVED', 'REOPENED', 'CLOSED', 'CONFIRMED'];
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.statuses.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {statuses.map(this.renderItem)}
+ </FacetItemsList>}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js
new file mode 100644
index 00000000000..cd6ab46e679
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js
@@ -0,0 +1,123 @@
+/*
+ * 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 { sortBy, uniq, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import FacetFooter from './components/FacetFooter';
+import { searchIssueTags } from '../../../api/issues';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ tags: Array<string>
+|};
+
+export default class TagFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'tags';
+
+ handleItemClick = (itemValue: string) => {
+ const { tags } = this.props;
+ const newValue = sortBy(
+ tags.includes(itemValue) ? without(tags, itemValue) : [...tags, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ handleSearch = (query: string) => {
+ return searchIssueTags({ ps: 50, q: query }).then(tags =>
+ tags.map(tag => ({ label: tag, value: tag })));
+ };
+
+ handleSelect = (tag: string) => {
+ const { tags } = this.props;
+ this.props.onChange({ [this.property]: uniq([...tags, tag]) });
+ };
+
+ getStat(tag: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[tag] : null;
+ }
+
+ renderTag(tag: string) {
+ return (
+ <span>
+ <i className="icon-tags icon-gray little-spacer-right" />
+ {tag}
+ </span>
+ );
+ }
+
+ render() {
+ const { stats } = this.props;
+
+ if (!stats) {
+ return null;
+ }
+
+ const tags = sortBy(Object.keys(stats), key => -stats[key]);
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.tags.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {tags.map(tag => (
+ <FacetItem
+ active={this.props.tags.includes(tag)}
+ facetMode={this.props.facetMode}
+ key={tag}
+ name={this.renderTag(tag)}
+ onClick={this.handleItemClick}
+ stat={this.getStat(tag)}
+ value={tag}
+ />
+ ))}
+ </FacetItemsList>}
+
+ {this.props.open &&
+ <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js
new file mode 100644
index 00000000000..c0eb0271058
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js
@@ -0,0 +1,102 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { orderBy, without } from 'lodash';
+import FacetBox from './components/FacetBox';
+import FacetHeader from './components/FacetHeader';
+import FacetItem from './components/FacetItem';
+import FacetItemsList from './components/FacetItemsList';
+import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ facetMode: string,
+ onChange: (changes: { [string]: Array<string> }) => void,
+ onToggle: (property: string) => void,
+ open: boolean,
+ stats?: { [string]: number },
+ types: Array<string>
+|};
+
+export default class TypeFacet extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ open: true
+ };
+
+ property = 'types';
+
+ handleItemClick = (itemValue: string) => {
+ const { types } = this.props;
+ const newValue = orderBy(
+ types.includes(itemValue) ? without(types, itemValue) : [...types, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ };
+
+ handleHeaderClick = () => {
+ this.props.onToggle(this.property);
+ };
+
+ getStat(type: string): ?number {
+ const { stats } = this.props;
+ return stats ? stats[type] : null;
+ }
+
+ renderItem = (type: string) => {
+ const active = this.props.types.includes(type);
+ const stat = this.getStat(type);
+
+ return (
+ <FacetItem
+ active={active}
+ disabled={stat === 0 && !active}
+ facetMode={this.props.facetMode}
+ key={type}
+ name={<span><IssueTypeIcon query={type} /> {translate('issue.type', type)}</span>}
+ onClick={this.handleItemClick}
+ stat={stat}
+ value={type}
+ />
+ );
+ };
+
+ render() {
+ const types = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
+
+ return (
+ <FacetBox property={this.property}>
+ <FacetHeader
+ hasValue={this.props.types.length > 0}
+ name={translate('issues.facet', this.property)}
+ onClick={this.handleHeaderClick}
+ open={this.props.open}
+ />
+
+ {this.props.open &&
+ <FacetItemsList>
+ {types.map(this.renderItem)}
+ </FacetItemsList>}
+ </FacetBox>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js
new file mode 100644
index 00000000000..5dc2230f4e3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js
@@ -0,0 +1,95 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import AssigneeFacet from '../AssigneeFacet';
+
+jest.mock('../../../../store/rootReducer', () => ({}));
+
+const renderAssigneeFacet = (props?: {}) =>
+ shallow(
+ <AssigneeFacet
+ assigned={true}
+ assignees={[]}
+ facetMode="count"
+ onChange={jest.fn()}
+ onToggle={jest.fn()}
+ open={true}
+ referencedUsers={{ foo: { avatar: 'avatart-foo', name: 'name-foo' } }}
+ stats={{ '': 5, foo: 13, bar: 7 }}
+ {...props}
+ />
+ );
+
+it('should render', () => {
+ expect(renderAssigneeFacet()).toMatchSnapshot();
+});
+
+it('should not render without stats', () => {
+ expect(renderAssigneeFacet({ stats: null })).toMatchSnapshot();
+});
+
+it('should select unassigned', () => {
+ expect(renderAssigneeFacet({ assigned: false })).toMatchSnapshot();
+});
+
+it('should select user', () => {
+ expect(renderAssigneeFacet({ assignees: ['foo'] })).toMatchSnapshot();
+});
+
+it('should render footer select option', () => {
+ const wrapper = renderAssigneeFacet();
+ expect(
+ wrapper.instance().renderOption({ avatar: 'avatar-foo', label: 'name-foo' })
+ ).toMatchSnapshot();
+});
+
+it('should call onChange', () => {
+ const onChange = jest.fn();
+ const wrapper = renderAssigneeFacet({ assignees: ['foo'], onChange });
+ const itemOnClick = wrapper.find('FacetItem').first().prop('onClick');
+
+ itemOnClick('');
+ expect(onChange).lastCalledWith({ assigned: false, assignees: [] });
+
+ itemOnClick('bar');
+ expect(onChange).lastCalledWith({ assigned: true, assignees: ['bar', 'foo'] });
+
+ itemOnClick('foo');
+ expect(onChange).lastCalledWith({ assigned: true, assignees: [] });
+});
+
+it('should call onToggle', () => {
+ const onToggle = jest.fn();
+ const wrapper = renderAssigneeFacet({ onToggle });
+ const headerOnClick = wrapper.find('FacetHeader').prop('onClick');
+
+ headerOnClick();
+ expect(onToggle).lastCalledWith('assignees');
+});
+
+it('should handle footer callbacks', () => {
+ const onChange = jest.fn();
+ const wrapper = renderAssigneeFacet({ assignees: ['foo'], onChange });
+ const onSelect = wrapper.find('FacetFooter').prop('onSelect');
+
+ onSelect('qux');
+ expect(onChange).lastCalledWith({ assigned: true, assignees: ['foo', 'qux'] });
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js
new file mode 100644
index 00000000000..a55c903b90c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import Sidebar from '../Sidebar';
+
+jest.mock('../../../../store/rootReducer', () => ({}));
+
+const renderSidebar = props =>
+ shallow(<Sidebar facets={{}} myIssues={false} openFacets={{}} query={{}} {...props} />)
+ .children()
+ .map(node => node.name());
+
+it('should render all facets', () => {
+ expect(renderSidebar()).toMatchSnapshot();
+});
+
+it('should render facets for project', () => {
+ expect(renderSidebar({ component: { qualifier: 'TRK' } })).toMatchSnapshot();
+});
+
+it('should render facets for module', () => {
+ expect(renderSidebar({ component: { qualifier: 'BRC' } })).toMatchSnapshot();
+});
+
+it('should render facets for directory', () => {
+ expect(renderSidebar({ component: { qualifier: 'DIR' } })).toMatchSnapshot();
+});
+
+it('should render facets for developer', () => {
+ expect(renderSidebar({ component: { qualifier: 'DEV' } })).toMatchSnapshot();
+});
+
+it('should render facets when my issues are selected', () => {
+ expect(renderSidebar({ myIssues: true })).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap
new file mode 100644
index 00000000000..80726a28b0b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap
@@ -0,0 +1,167 @@
+exports[`test should not render without stats 1`] = `null`;
+
+exports[`test should render 1`] = `
+<FacetBox
+ property="assignees">
+ <FacetHeader
+ hasValue={false}
+ name="issues.facet.assignees"
+ onClick={[Function]}
+ open={true} />
+ <FacetItemsList>
+ <FacetItem
+ active={false}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name="unassigned"
+ onClick={[Function]}
+ stat={5}
+ value="" />
+ <FacetItem
+ active={false}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name={
+ <span>
+ <Connect(Avatar)
+ className="little-spacer-right"
+ hash="avatart-foo"
+ size={16} />
+ name-foo
+ </span>
+ }
+ onClick={[Function]}
+ stat={13}
+ value="foo" />
+ <FacetItem
+ active={false}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name="bar"
+ onClick={[Function]}
+ stat={7}
+ value="bar" />
+ </FacetItemsList>
+ <FacetFooter
+ onSearch={[Function]}
+ onSelect={[Function]}
+ renderOption={[Function]} />
+</FacetBox>
+`;
+
+exports[`test should render footer select option 1`] = `
+<span>
+ <Connect(Avatar)
+ className="little-spacer-right"
+ hash="avatar-foo"
+ size={16} />
+ name-foo
+</span>
+`;
+
+exports[`test should select unassigned 1`] = `
+<FacetBox
+ property="assignees">
+ <FacetHeader
+ hasValue={true}
+ name="issues.facet.assignees"
+ onClick={[Function]}
+ open={true} />
+ <FacetItemsList>
+ <FacetItem
+ active={true}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name="unassigned"
+ onClick={[Function]}
+ stat={5}
+ value="" />
+ <FacetItem
+ active={false}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name={
+ <span>
+ <Connect(Avatar)
+ className="little-spacer-right"
+ hash="avatart-foo"
+ size={16} />
+ name-foo
+ </span>
+ }
+ onClick={[Function]}
+ stat={13}
+ value="foo" />
+ <FacetItem
+ active={false}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name="bar"
+ onClick={[Function]}
+ stat={7}
+ value="bar" />
+ </FacetItemsList>
+ <FacetFooter
+ onSearch={[Function]}
+ onSelect={[Function]}
+ renderOption={[Function]} />
+</FacetBox>
+`;
+
+exports[`test should select user 1`] = `
+<FacetBox
+ property="assignees">
+ <FacetHeader
+ hasValue={true}
+ name="issues.facet.assignees"
+ onClick={[Function]}
+ open={true} />
+ <FacetItemsList>
+ <FacetItem
+ active={false}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name="unassigned"
+ onClick={[Function]}
+ stat={5}
+ value="" />
+ <FacetItem
+ active={true}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name={
+ <span>
+ <Connect(Avatar)
+ className="little-spacer-right"
+ hash="avatart-foo"
+ size={16} />
+ name-foo
+ </span>
+ }
+ onClick={[Function]}
+ stat={13}
+ value="foo" />
+ <FacetItem
+ active={false}
+ disabled={false}
+ facetMode="count"
+ halfWidth={false}
+ name="bar"
+ onClick={[Function]}
+ stat={7}
+ value="bar" />
+ </FacetItemsList>
+ <FacetFooter
+ onSearch={[Function]}
+ onSelect={[Function]}
+ renderOption={[Function]} />
+</FacetBox>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap
new file mode 100644
index 00000000000..81d6ce875fc
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap
@@ -0,0 +1,112 @@
+exports[`test should render all facets 1`] = `
+Array [
+ "FacetMode",
+ "TypeFacet",
+ "ResolutionFacet",
+ "SeverityFacet",
+ "StatusFacet",
+ "CreationDateFacet",
+ "RuleFacet",
+ "TagFacet",
+ "ProjectFacet",
+ "ModuleFacet",
+ "DirectoryFacet",
+ "FileFacet",
+ "AssigneeFacet",
+ "AuthorFacet",
+ "LanguageFacet",
+]
+`;
+
+exports[`test should render facets for developer 1`] = `
+Array [
+ "FacetMode",
+ "TypeFacet",
+ "ResolutionFacet",
+ "SeverityFacet",
+ "StatusFacet",
+ "CreationDateFacet",
+ "RuleFacet",
+ "TagFacet",
+ "ProjectFacet",
+ "ModuleFacet",
+ "DirectoryFacet",
+ "FileFacet",
+ "AssigneeFacet",
+ "LanguageFacet",
+]
+`;
+
+exports[`test should render facets for directory 1`] = `
+Array [
+ "FacetMode",
+ "TypeFacet",
+ "ResolutionFacet",
+ "SeverityFacet",
+ "StatusFacet",
+ "CreationDateFacet",
+ "RuleFacet",
+ "TagFacet",
+ "FileFacet",
+ "AssigneeFacet",
+ "AuthorFacet",
+ "LanguageFacet",
+]
+`;
+
+exports[`test should render facets for module 1`] = `
+Array [
+ "FacetMode",
+ "TypeFacet",
+ "ResolutionFacet",
+ "SeverityFacet",
+ "StatusFacet",
+ "CreationDateFacet",
+ "RuleFacet",
+ "TagFacet",
+ "ModuleFacet",
+ "DirectoryFacet",
+ "FileFacet",
+ "AssigneeFacet",
+ "AuthorFacet",
+ "LanguageFacet",
+]
+`;
+
+exports[`test should render facets for project 1`] = `
+Array [
+ "FacetMode",
+ "TypeFacet",
+ "ResolutionFacet",
+ "SeverityFacet",
+ "StatusFacet",
+ "CreationDateFacet",
+ "RuleFacet",
+ "TagFacet",
+ "ModuleFacet",
+ "DirectoryFacet",
+ "FileFacet",
+ "AssigneeFacet",
+ "AuthorFacet",
+ "LanguageFacet",
+]
+`;
+
+exports[`test should render facets when my issues are selected 1`] = `
+Array [
+ "FacetMode",
+ "TypeFacet",
+ "ResolutionFacet",
+ "SeverityFacet",
+ "StatusFacet",
+ "CreationDateFacet",
+ "RuleFacet",
+ "TagFacet",
+ "ProjectFacet",
+ "ModuleFacet",
+ "DirectoryFacet",
+ "FileFacet",
+ "AuthorFacet",
+ "LanguageFacet",
+]
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/context-facet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetBox.js
index a2f31cb9c37..358f6ee3a19 100644
--- a/server/sonar-web/src/main/js/apps/issues/facets/context-facet.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetBox.js
@@ -17,16 +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 BaseFacet from './base-facet';
-import Template from '../templates/facets/issues-context-facet.hbs';
+// @flow
+import React from 'react';
-export default BaseFacet.extend({
- template: Template,
+type Props = {|
+ children?: React.Element<*>,
+ property: string
+|};
- serializeData() {
- return {
- ...BaseFacet.prototype.serializeData.apply(this, arguments),
- state: this.options.app.state.toJSON()
- };
- }
-});
+const FacetBox = (props: Props) => (
+ <div className="search-navigator-facet-box" data-property={props.property}>
+ {props.children}
+ </div>
+);
+
+export default FacetBox;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/NoProjects.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetFooter.js
index c2194ad5409..8f0fca20fdc 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/NoProjects.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetFooter.js
@@ -17,15 +17,26 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+// @flow
import React from 'react';
-import { translate } from '../../../helpers/l10n';
+import SearchSelect from '../../components/SearchSelect';
+
+type Option = { label: string, value: string };
+
+type Props = {|
+ minimumQueryLength?: number,
+ onSearch: (query: string) => Promise<Array<Option>>,
+ onSelect: (value: string) => void,
+ renderOption?: (option: Object) => React.Element<*>
+|};
+
+export default class FacetFooter extends React.PureComponent {
+ props: Props;
-export default class NoProjects extends React.Component {
render() {
return (
- <div className="projects-empty-list">
- <h3>{translate('projects.no_projects.1')}</h3>
- <p className="big-spacer-top">{translate('projects.no_projects.2')}</p>
+ <div className="search-navigator-facet-footer">
+ <SearchSelect {...this.props} />
</div>
);
}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js
new file mode 100644
index 00000000000..ff6ef8387c9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js
@@ -0,0 +1,83 @@
+/*
+ * 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
+/* eslint-disable max-len */
+import React from 'react';
+
+type Props = {
+ hasValue: boolean,
+ name: string,
+ onClick?: () => void,
+ open: boolean
+};
+
+export default class FacetHeader extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ hasValue: false,
+ open: true
+ };
+
+ handleClick = (e: Event & { currentTarget: HTMLElement }) => {
+ e.preventDefault();
+ e.currentTarget.blur();
+ if (this.props.onClick) {
+ this.props.onClick();
+ }
+ };
+
+ renderCheckbox() {
+ return (
+ <svg viewBox="0 0 1792 1792" width="10" height="10" style={{ paddingTop: 3 }}>
+ {this.props.open
+ ? <path
+ style={{ fill: 'currentColor ' }}
+ d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z"
+ />
+ : <path
+ style={{ fill: 'currentColor ' }}
+ d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z"
+ />}
+ </svg>
+ );
+ }
+
+ renderValueIndicator() {
+ return this.props.hasValue && !this.props.open
+ ? <svg viewBox="0 0 1792 1792" width="8" height="8" style={{ paddingTop: 5, paddingLeft: 8 }}>
+ <path
+ d="M1664 896q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"
+ fill="#4b9fd5"
+ />
+ </svg>
+ : null;
+ }
+
+ render() {
+ return this.props.onClick
+ ? <a className="search-navigator-facet-header" href="#" onClick={this.handleClick}>
+ {this.renderCheckbox()}{' '}{this.props.name}{' '}{this.renderValueIndicator()}
+ </a>
+ : <span className="search-navigator-facet-header">
+ {this.props.name}
+ </span>;
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js
new file mode 100644
index 00000000000..94d512d195b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItem.js
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import { formatMeasure } from '../../../../helpers/measures';
+
+type Props = {|
+ active: boolean,
+ disabled: boolean,
+ facetMode: string,
+ halfWidth: boolean,
+ name: string | React.Element<*>,
+ onClick: (string) => void,
+ stat: ?number,
+ value: string
+|};
+
+export default class FacetItem extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ disabled: false,
+ halfWidth: false
+ };
+
+ handleClick = (event: Event & { currentTarget: HTMLElement }) => {
+ event.preventDefault();
+ const value = event.currentTarget.dataset.value;
+ this.props.onClick(value);
+ };
+
+ render() {
+ const { stat } = this.props;
+
+ const className = classNames('facet', 'search-navigator-facet', {
+ active: this.props.active,
+ 'search-navigator-facet-half': this.props.halfWidth
+ });
+
+ const formattedStat = stat &&
+ formatMeasure(stat, this.props.facetMode === 'effort' ? 'SHORT_WORK_DUR' : 'SHORT_INT');
+
+ return this.props.disabled
+ ? <span className={className}>
+ <span className="facet-name">{this.props.name}</span>
+ <span className="facet-stat">{formattedStat}</span>
+ </span>
+ : <a className={className} data-value={this.props.value} href="#" onClick={this.handleClick}>
+ <span className="facet-name">{this.props.name}</span>
+ <span className="facet-stat">{formattedStat}</span>
+ </a>;
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItemsList.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItemsList.js
new file mode 100644
index 00000000000..4a203f0c071
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetItemsList.js
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+
+type Props = {|
+ children?: Array<React.Element<*>>
+|};
+
+const FacetItemsList = (props: Props) => (
+ <div className="search-navigator-facet-list">
+ {props.children}
+ </div>
+);
+
+export default FacetItemsList;
diff --git a/server/sonar-web/src/main/js/apps/component-issues/routes.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetBox-test.js
index 954ba1487bb..0eb453e8066 100644
--- a/server/sonar-web/src/main/js/apps/component-issues/routes.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetBox-test.js
@@ -17,15 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-const routes = [
- {
- indexRoute: {
- getComponent(_, callback) {
- require.ensure([], require =>
- callback(null, require('./components/ComponentIssuesAppContainer').default));
- }
- }
- }
-];
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import FacetBox from '../FacetBox';
-export default routes;
+it('should render', () => {
+ expect(shallow(<FacetBox property="foo"><div /></FacetBox>)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetFooter-test.js
index eea72dc3e28..4dbf1cc3ece 100644
--- a/server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetFooter-test.js
@@ -17,13 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import Marionette from 'backbone.marionette';
-import { translate } from '../../helpers/l10n';
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import FacetFooter from '../FacetFooter';
-export default Marionette.ItemView.extend({
- className: 'search-navigator-no-results',
-
- template() {
- return translate('issue_filter.no_issues');
- }
+it('should render', () => {
+ expect(shallow(<FacetFooter onSearch={jest.fn()} onSelect={jest.fn()} />)).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js
new file mode 100644
index 00000000000..5aa00c4a41e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js
@@ -0,0 +1,61 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import { click } from '../../../../../helpers/testUtils';
+import FacetHeader from '../FacetHeader';
+
+it('should render open facet with value', () => {
+ expect(
+ shallow(<FacetHeader hasValue={true} name="foo" onClick={jest.fn()} open={true} />)
+ ).toMatchSnapshot();
+});
+
+it('should render open facet without value', () => {
+ expect(
+ shallow(<FacetHeader hasValue={false} name="foo" onClick={jest.fn()} open={true} />)
+ ).toMatchSnapshot();
+});
+
+it('should render closed facet with value', () => {
+ expect(
+ shallow(<FacetHeader hasValue={true} name="foo" onClick={jest.fn()} open={false} />)
+ ).toMatchSnapshot();
+});
+
+it('should render closed facet without value', () => {
+ expect(
+ shallow(<FacetHeader hasValue={false} name="foo" onClick={jest.fn()} open={false} />)
+ ).toMatchSnapshot();
+});
+
+it('should render without link', () => {
+ expect(shallow(<FacetHeader hasValue={false} name="foo" open={false} />)).toMatchSnapshot();
+});
+
+it('should call onClick', () => {
+ const onClick = jest.fn();
+ const wrapper = shallow(
+ <FacetHeader hasValue={false} name="foo" onClick={onClick} open={false} />
+ );
+ click(wrapper);
+ expect(onClick).toHaveBeenCalled();
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItem-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItem-test.js
new file mode 100644
index 00000000000..ddc84eca458
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItem-test.js
@@ -0,0 +1,68 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import { click } from '../../../../../helpers/testUtils';
+import FacetItem from '../FacetItem';
+
+const renderFacetItem = (props: {}) =>
+ shallow(
+ <FacetItem
+ active={false}
+ facetMode="count"
+ name="foo"
+ onClick={jest.fn()}
+ stat={null}
+ value="bar"
+ {...props}
+ />
+ );
+
+it('should render active', () => {
+ expect(renderFacetItem({ active: true })).toMatchSnapshot();
+});
+
+it('should render inactive', () => {
+ expect(renderFacetItem({ active: false })).toMatchSnapshot();
+});
+
+it('should render stat', () => {
+ expect(renderFacetItem({ stat: 13 })).toMatchSnapshot();
+});
+
+it('should render disabled', () => {
+ expect(renderFacetItem({ disabled: true })).toMatchSnapshot();
+});
+
+it('should render half width', () => {
+ expect(renderFacetItem({ halfWidth: true })).toMatchSnapshot();
+});
+
+it('should render effort stat', () => {
+ expect(renderFacetItem({ facetMode: 'effort', stat: 1234 })).toMatchSnapshot();
+});
+
+it('should call onClick', () => {
+ const onClick = jest.fn();
+ const wrapper = renderFacetItem({ onClick });
+ click(wrapper, { currentTarget: { dataset: { value: 'bar' } } });
+ expect(onClick).toHaveBeenCalled();
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/models/issue.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItemsList-test.js
index 218e27810ad..883e62b88d4 100644
--- a/server/sonar-web/src/main/js/apps/issues/models/issue.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetItemsList-test.js
@@ -17,14 +17,11 @@
* 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 '../../../components/issue/models/issue';
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import FacetItemsList from '../FacetItemsList';
-export default Issue.extend({
- reset(attrs, options) {
- const keepFields = ['index', 'selected', 'comments'];
- keepFields.forEach(field => {
- attrs[field] = this.get(field);
- });
- return Issue.prototype.reset.call(this, attrs, options);
- }
+it('should render', () => {
+ expect(shallow(<FacetItemsList><div /></FacetItemsList>)).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetBox-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetBox-test.js.snap
new file mode 100644
index 00000000000..2722912ca7d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetBox-test.js.snap
@@ -0,0 +1,7 @@
+exports[`test should render 1`] = `
+<div
+ className="search-navigator-facet-box"
+ data-property="foo">
+ <div />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetFooter-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetFooter-test.js.snap
new file mode 100644
index 00000000000..21f8096c8e8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetFooter-test.js.snap
@@ -0,0 +1,10 @@
+exports[`test should render 1`] = `
+<div
+ className="search-navigator-facet-footer">
+ <SearchSelect
+ minimumQueryLength={2}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ resetOnBlur={true} />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap
new file mode 100644
index 00000000000..3333ae8944d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap
@@ -0,0 +1,132 @@
+exports[`test should render closed facet with value 1`] = `
+<a
+ className="search-navigator-facet-header"
+ href="#"
+ onClick={[Function]}>
+ <svg
+ height="10"
+ style={
+ Object {
+ "paddingTop": 3,
+ }
+ }
+ viewBox="0 0 1792 1792"
+ width="10">
+ <path
+ d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z"
+ style={
+ Object {
+ "fill": "currentColor ",
+ }
+ } />
+ </svg>
+
+ foo
+
+ <svg
+ height="8"
+ style={
+ Object {
+ "paddingLeft": 8,
+ "paddingTop": 5,
+ }
+ }
+ viewBox="0 0 1792 1792"
+ width="8">
+ <path
+ d="M1664 896q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"
+ fill="#4b9fd5" />
+ </svg>
+</a>
+`;
+
+exports[`test should render closed facet without value 1`] = `
+<a
+ className="search-navigator-facet-header"
+ href="#"
+ onClick={[Function]}>
+ <svg
+ height="10"
+ style={
+ Object {
+ "paddingTop": 3,
+ }
+ }
+ viewBox="0 0 1792 1792"
+ width="10">
+ <path
+ d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z"
+ style={
+ Object {
+ "fill": "currentColor ",
+ }
+ } />
+ </svg>
+
+ foo
+
+</a>
+`;
+
+exports[`test should render open facet with value 1`] = `
+<a
+ className="search-navigator-facet-header"
+ href="#"
+ onClick={[Function]}>
+ <svg
+ height="10"
+ style={
+ Object {
+ "paddingTop": 3,
+ }
+ }
+ viewBox="0 0 1792 1792"
+ width="10">
+ <path
+ d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z"
+ style={
+ Object {
+ "fill": "currentColor ",
+ }
+ } />
+ </svg>
+
+ foo
+
+</a>
+`;
+
+exports[`test should render open facet without value 1`] = `
+<a
+ className="search-navigator-facet-header"
+ href="#"
+ onClick={[Function]}>
+ <svg
+ height="10"
+ style={
+ Object {
+ "paddingTop": 3,
+ }
+ }
+ viewBox="0 0 1792 1792"
+ width="10">
+ <path
+ d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z"
+ style={
+ Object {
+ "fill": "currentColor ",
+ }
+ } />
+ </svg>
+
+ foo
+
+</a>
+`;
+
+exports[`test should render without link 1`] = `
+<span
+ className="search-navigator-facet-header">
+ foo
+</span>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap
new file mode 100644
index 00000000000..0a22b710499
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItem-test.js.snap
@@ -0,0 +1,90 @@
+exports[`test should render active 1`] = `
+<a
+ className="facet search-navigator-facet active"
+ data-value="bar"
+ href="#"
+ onClick={[Function]}>
+ <span
+ className="facet-name">
+ foo
+ </span>
+ <span
+ className="facet-stat" />
+</a>
+`;
+
+exports[`test should render disabled 1`] = `
+<span
+ className="facet search-navigator-facet">
+ <span
+ className="facet-name">
+ foo
+ </span>
+ <span
+ className="facet-stat" />
+</span>
+`;
+
+exports[`test should render effort stat 1`] = `
+<a
+ className="facet search-navigator-facet"
+ data-value="bar"
+ href="#"
+ onClick={[Function]}>
+ <span
+ className="facet-name">
+ foo
+ </span>
+ <span
+ className="facet-stat">
+ work_duration.x_days.3
+ </span>
+</a>
+`;
+
+exports[`test should render half width 1`] = `
+<a
+ className="facet search-navigator-facet search-navigator-facet-half"
+ data-value="bar"
+ href="#"
+ onClick={[Function]}>
+ <span
+ className="facet-name">
+ foo
+ </span>
+ <span
+ className="facet-stat" />
+</a>
+`;
+
+exports[`test should render inactive 1`] = `
+<a
+ className="facet search-navigator-facet"
+ data-value="bar"
+ href="#"
+ onClick={[Function]}>
+ <span
+ className="facet-name">
+ foo
+ </span>
+ <span
+ className="facet-stat" />
+</a>
+`;
+
+exports[`test should render stat 1`] = `
+<a
+ className="facet search-navigator-facet"
+ data-value="bar"
+ href="#"
+ onClick={[Function]}>
+ <span
+ className="facet-name">
+ foo
+ </span>
+ <span
+ className="facet-stat">
+ 13
+ </span>
+</a>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItemsList-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItemsList-test.js.snap
new file mode 100644
index 00000000000..15686ac81d6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetItemsList-test.js.snap
@@ -0,0 +1,6 @@
+exports[`test should render 1`] = `
+<div
+ className="search-navigator-facet-list">
+ <div />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/styles.css b/server/sonar-web/src/main/js/apps/issues/styles.css
deleted file mode 100644
index 46942bb2796..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/styles.css
+++ /dev/null
@@ -1,23 +0,0 @@
-.issues-header-inner {
- padding: 32px 10px 15px;
- background-color: #f3f3f3;
- text-align: center;
-}
-
-.issues-header-inner:empty {
- display: none;
-}
-
-.issues-header-order {
- display: inline-block;
- vertical-align: top;
- margin-right: 20px;
- font-size: 12px;
-}
-
-.issues-header-order {
- display: inline-block;
- vertical-align: top;
- margin-right: 20px;
- font-size: 12px;
-}
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs b/server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs
deleted file mode 100644
index fda291ab2b3..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/BulkChangeForm.hbs
+++ /dev/null
@@ -1,145 +0,0 @@
-{{#if isLoaded}}
- <form id="bulk-change-form">
- <div class="modal-head">
- <h2>{{tp 'issue_bulk_change.form.title' issues.length}}</h2>
- </div>
- <div class="modal-body">
- <div class="js-modal-messages"></div>
-
- {{#if limitReached}}
- <div class="alert alert-warning">
- {{tp 'issue_bulk_change.max_issues_reached' issues.length}}
- </div>
- {{/if}}
-
- {{! assign }}
- {{#if canBeAssigned}}
- <div class="modal-field">
- <label for="assignee">{{t 'issue.assign.formlink'}}</label>
- <input id="assign-action" name="actions[]" type="checkbox" value="assign">
- <input id="assignee" type="hidden">
- <div class="pull-right note">
- ({{tp 'issue_bulk_change.x_issues' canBeAssigned}})
- </div>
- </div>
- {{/if}}
-
- {{! type }}
- {{#if canChangeType}}
- <div class="modal-field">
- <label for="type">{{t 'issue.set_type'}}</label>
- <input id="set-type-action" name="actions[]" type="checkbox" value="set_type">
- <select id="type" name="set_type.type">
- <option value="BUG">{{t 'issue.type.BUG'}}</option>
- <option value="VULNERABILITY">{{t 'issue.type.VULNERABILITY'}}</option>
- <option value="CODE_SMELL">{{t 'issue.type.CODE_SMELL'}}</option>
- </select>
- <div class="pull-right note">
- ({{tp 'issue_bulk_change.x_issues' canChangeType}})
- </div>
- </div>
- {{/if}}
-
- {{! severity }}
- {{#if canChangeSeverity}}
- <div class="modal-field">
- <label for="severity">{{t 'issue.set_severity'}}</label>
- <input id="set-severity-action" name="actions[]" type="checkbox" value="set_severity">
- <select id="severity" name="set_severity.severity">
- <option value="BLOCKER">{{t 'severity.BLOCKER'}}</option>
- <option value="CRITICAL">{{t 'severity.CRITICAL'}}</option>
- <option value="MAJOR">{{t 'severity.MAJOR'}}</option>
- <option value="MINOR">{{t 'severity.MINOR'}}</option>
- <option value="INFO">{{t 'severity.INFO'}}</option>
- </select>
- <div class="pull-right note">
- ({{tp 'issue_bulk_change.x_issues' canChangeSeverity}})
- </div>
- </div>
- {{/if}}
-
- {{! add tags }}
- {{#if canChangeTags}}
- <div class="modal-field">
- <label for="add_tags">{{t 'issue.add_tags'}}</label>
- <input id="add-tags-action" name="actions[]" type="checkbox" value="add_tags">
- <input id="add_tags" name="add_tags.tags" type="text">
- <div class="pull-right note">
- ({{tp 'issue_bulk_change.x_issues' canChangeTags}})
- </div>
- </div>
- {{/if}}
-
- {{! remove tags }}
- {{#if canChangeTags}}
- <div class="modal-field">
- <label for="remove_tags">{{t 'issue.remove_tags'}}</label>
- <input id="remove-tags-action" name="actions[]" type="checkbox" value="remove_tags">
- <input id="remove_tags" name="remove_tags.tags" type="text">
- <div class="pull-right note">
- ({{tp 'issue_bulk_change.x_issues' canChangeTags}})
- </div>
- </div>
- {{/if}}
-
- {{! transitions }}
- {{#notEmpty availableTransitions}}
- <div class="modal-field">
- <label>{{t 'issue.transition'}}</label>
- {{#each availableTransitions}}
- <input type="radio" id="transition-{{transition}}" name="do_transition.transition"
- value="{{transition}}">
- <label for="transition-{{transition}}" style="float: none; display: inline; left: 0; cursor: pointer;">
- {{t 'issue.transition' transition}}
- </label>
- <div class="pull-right note">
- ({{tp 'issue_bulk_change.x_issues' count}})
- </div>
- <br>
- {{/each}}
- </div>
- {{/notEmpty}}
-
- {{! comment }}
- {{#if canBeCommented}}
- <div class="modal-field">
- <label for="comment">
- {{t 'issue.comment.formlink'}}
- <i class="icon-help" title="{{t 'issue_bulk_change.comment.help'}}"></i>
- </label>
- <div>
- <textarea rows="4" name="comment" id="comment" style="width: 100%"></textarea>
- </div>
- <div class="pull-right">
- {{> ../../../components/common/templates/_markdown-tips}}
- </div>
- </div>
- {{/if}}
-
- {{! notifications }}
- <div class="modal-field">
- <label for="send-notifications">{{t 'issue.send_notifications'}}</label>
- <input id="send-notifications" name="sendNotifications" type="checkbox" value="true">
- </div>
-
- </div>
- <div class="modal-foot">
- <i class="js-modal-spinner spinner spacer-right hidden"></i>
- <button id="bulk-change-submit">{{t 'apply'}}</button>
- <a id="bulk-change-cancel" href="#" class="js-modal-close">{{t 'cancel'}}</a>
- </div>
- </form>
-{{else}}
- <div class="modal-head">
- <h2>{{t 'bulk_change'}}</h2>
- </div>
- <div class="modal-body">
- <div class="js-modal-messages"></div>
- <div class="text-center">
- <i class="spinner spinner-margin"></i>
- </div>
- </div>
- <div class="modal-foot">
- <a id="bulk-change-cancel" href="#" class="js-modal-close">{{t 'cancel'}}</a>
- </div>
-{{/if}}
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs
deleted file mode 100644
index 64237bc0e79..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs
+++ /dev/null
@@ -1,4 +0,0 @@
-<a class="search-navigator-facet-header js-facet-toggle">
- <i class="icon-checkbox {{#if enabled}}icon-checkbox-checked{{/if}}"></i>
- {{t "issues.facet" property}}
-</a>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs
deleted file mode 100644
index 9fb23f78ab8..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs
+++ /dev/null
@@ -1,26 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list">
- {{#each values}}
- {{#eq val ""}}
- {{! unassigned }}
- <a class="facet search-navigator-facet js-facet" data-unassigned title="{{t "unassigned"}}">
- <span class="facet-name">{{t "unassigned"}}</span>
- <span class="facet-stat">
- {{formatFacetValue count ../../state.facetMode}}
- </span>
- </a>
- {{else}}
- <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{label}}">
- <span class="facet-name">{{label}}</span>
- <span class="facet-stat">
- {{formatFacetValue count ../../state.facetMode}}
- </span>
- </a>
- {{/eq}}
- {{/each}}
-
- <div class="search-navigator-facet-custom-value">
- <input type="hidden" class="js-custom-value">
- </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs
deleted file mode 100644
index 83442f81b37..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs
+++ /dev/null
@@ -1,11 +0,0 @@
-{{> "_issues-facet-header"}}
-<div class="search-navigator-facet-list">
- {{#each values}}
- <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{default label val}}">
- <span class="facet-name">{{default label val}}</span>
- <span class="facet-stat">
- {{formatFacetValue count ../state.facetMode}}
- </span>
- </a>
- {{/each}}
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs
deleted file mode 100644
index 9f981c07c1a..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs
+++ /dev/null
@@ -1,3 +0,0 @@
-<div class="search-navigator-facet-query">
- Issues of &nbsp;&nbsp; {{qualifierIcon state.contextComponentQualifier}}&nbsp;{{state.contextComponentName}}
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs
deleted file mode 100644
index bf5410833c9..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs
+++ /dev/null
@@ -1,42 +0,0 @@
-{{> "_issues-facet-header"}}
-
-{{#if createdAt}}
- <input type="hidden" name="createdAt">
- <div class="search-navigator-facet-container">
- {{dt createdAt}} ({{fromNow createdAt}})
- </div>
-{{else}}
- <div class="search-navigator-facet-container">
- <div class="js-barchart" data-height="75" {{#if periodEnd}}data-end-date="{{periodEnd}}"{{/if}}></div>
- <div class="search-navigator-date-facet-selection">
- <a class="js-select-period-start search-navigator-date-facet-selection-dropdown-left">
- {{#if periodStart}}{{d periodStart}}{{else}}Past{{/if}}&nbsp;<i class="icon-dropdown"></i>
- </a>
- <a class="js-select-period-end search-navigator-date-facet-selection-dropdown-right">
- {{#if periodEnd}}{{d periodEnd}}{{else}}Now{{/if}}&nbsp;<i class="icon-dropdown"></i>
- </a>
- <input class="js-period-start search-navigator-date-facet-selection-input-left"
- type="text" value="{{#if periodStart}}{{ds periodStart}}{{/if}}" name="createdAfter">
- <input class="js-period-end search-navigator-date-facet-selection-input-right"
- type="text" value="{{#if periodEnd}}{{ds periodEnd}}{{/if}}" name="createdBefore">
- </div>
-
- <div class="spacer-top">
- <span class="spacer-right">{{t "issues.facet.createdAt.or"}}</span>
- <a class="js-all spacer-right" href="#">{{t "issues.facet.createdAt.all"}}</a>
- {{#if hasLeak}}
- <a class="js-leak spacer-right {{#eq sinceLeakPeriod "true"}}active-link{{/eq}}" href="#">Leak Period</a>
- {{else}}
- <a class="js-last-week spacer-right {{#eq createdInLast "1w"}}active-link{{/eq}}" href="#">
- {{t "issues.facet.createdAt.last_week"}}
- </a>
- <a class="js-last-month spacer-right {{#eq createdInLast "1m"}}active-link{{/eq}}" href="#">
- {{t "issues.facet.createdAt.last_month"}}
- </a>
- <a class="js-last-year {{#eq createdInLast "1y"}}active-link{{/eq}}" href="#">
- {{t "issues.facet.createdAt.last_year"}}
- </a>
- {{/if}}
- </div>
- </div>
-{{/if}}
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs
deleted file mode 100644
index 0674f5b87bb..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs
+++ /dev/null
@@ -1,16 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list">
- {{#each values}}
- <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{#if extra}}({{extra}}) {{/if}}{{default label val}}">
- <span class="facet-name">{{default label val}}</span>
- <span class="facet-stat">
- {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{/each}}
-
- <div class="search-navigator-facet-custom-value">
- <input type="hidden" class="js-custom-value">
- </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs
deleted file mode 100644
index 3569040b69c..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs
+++ /dev/null
@@ -1,12 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list search-navigator-facet-list-align-right">
- {{#each values}}
- <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{default label val}}">
- <span class="facet-name">{{default label val}}</span>
- <span class="facet-stat">
- {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{/each}}
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs
deleted file mode 100644
index 28ae140f75b..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs
+++ /dev/null
@@ -1,7 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-container">
- <div class="facet search-navigator-facet active" style="cursor: default;">
- <span class="facet-name">{{issues}}</span>
- </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs
deleted file mode 100644
index af69286d92a..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-mode-facet.hbs
+++ /dev/null
@@ -1,15 +0,0 @@
-<div class="search-navigator-facet-header">
- {{t 'issues.facet.mode'}}
-</div>
-
-
-<div class="search-navigator-facet-list">
- <a class="facet search-navigator-facet search-navigator-facet-half js-facet {{#eq mode 'count'}}active{{/eq}}"
- data-value="count">
- <span class="facet-name">{{t 'issues.facet.mode.issues'}}</span>
- </a>
- <a class="facet search-navigator-facet search-navigator-facet-half js-facet {{#eq mode 'effort'}}active{{/eq}}"
- data-value="effort">
- <span class="facet-name">{{t 'issues.facet.mode.effort'}}</span>
- </a>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs
deleted file mode 100644
index c11181a70dd..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-my-issues-facet.hbs
+++ /dev/null
@@ -1,12 +0,0 @@
-{{#if user.isLoggedIn}}
- <ul class="radio-toggle">
- <li>
- <input type="radio" name="issues-page-my" value="my" id="issues-page-my-my" {{#if me}}checked{{/if}}>
- <label for="issues-page-my-my">My Issues</label>
- </li>
- <li>
- <input type="radio" name="issues-page-my" value="all" id="issues-page-my-all" {{#unless me}}checked{{/unless}}>
- <label for="issues-page-my-all">All</label>
- </li>
- </ul>
-{{/if}}
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs
deleted file mode 100644
index 7693af65549..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-projects-facet.hbs
+++ /dev/null
@@ -1,16 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list">
- {{#each values}}
- <a class="facet search-navigator-facet js-facet" data-value="{{val}}" title="{{#if extra}}({{extra}}) {{/if}}{{default label val}}">
- <span class="facet-name">{{#if organization}}{{organization.name}}<span class="slash-separator"></span>{{/if}}{{default label val}}</span>
- <span class="facet-stat">
- {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{/each}}
-
- <div class="search-navigator-facet-custom-value">
- <input type="hidden" class="js-custom-value">
- </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs
deleted file mode 100644
index 19e47071f8b..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs
+++ /dev/null
@@ -1,24 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list">
- {{#each values}}
- {{#eq val ""}}
- {{! unresolved }}
- <a class="facet search-navigator-facet search-navigator-facet-half js-facet" data-unresolved
- title="{{t "issue.unresolved.description"}}" data-toggle="tooltip" data-placement="right">
- <span class="facet-name">{{t "unresolved"}}</span>
- <span class="facet-stat">
- {{#eq ../../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{else}}
- <a class="facet search-navigator-facet search-navigator-facet-half js-facet" data-value="{{val}}"
- title="{{t "issue.resolution" val "description"}}" data-toggle="tooltip" data-placement="right">
- <span class="facet-name">{{t "issue.resolution" val}}</span>
- <span class="facet-stat">
- {{#eq ../../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{/eq}}
- {{/each}}
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs
deleted file mode 100644
index 88b9bd0585b..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs
+++ /dev/null
@@ -1,13 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list">
- {{#each values}}
- <a class="facet search-navigator-facet search-navigator-facet-half js-facet"
- data-value="{{val}}" title="{{t "severity" val "description"}}" data-toggle="tooltip" data-placement="right">
- <span class="facet-name">{{severityIcon val}} {{t "severity" val}}</span>
- <span class="facet-stat">
- {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{/each}}
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs
deleted file mode 100644
index cc7f1fcefdb..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs
+++ /dev/null
@@ -1,13 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list">
- {{#each values}}
- <a class="facet search-navigator-facet search-navigator-facet-half js-facet"
- data-value="{{val}}" title="{{t "issue.status" val "description"}}" data-toggle="tooltip" data-placement="right">
- <span class="facet-name">{{statusIcon val}} {{t "issue.status" val}}</span>
- <span class="facet-stat">
- {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{/each}}
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs
deleted file mode 100644
index f97ada41dca..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-type-facet.hbs
+++ /dev/null
@@ -1,13 +0,0 @@
-{{> "_issues-facet-header"}}
-
-<div class="search-navigator-facet-list">
- {{#each values}}
- <a class="facet search-navigator-facet js-facet"
- data-value="{{val}}">
- <span class="facet-name">{{issueTypeIcon val}} {{t 'issue.type' val}}</span>
- <span class="facet-stat">
- {{#eq ../state.facetMode 'count'}}{{numberShort count}}{{else}}{{formatMeasure count 'SHORT_WORK_DUR'}}{{/eq}}
- </span>
- </a>
- {{/each}}
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs
deleted file mode 100644
index f18f6845d3a..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs
+++ /dev/null
@@ -1,89 +0,0 @@
-<header class="menu-search">
- <h6>{{t "issue.filter_similar_issues"}}</h6>
-</header>
-
-<ul class="menu">
- <li>
- <a href="#" data-property="types" data-value="{{this.type}}">
- {{issueType this.type}}
- </a>
- </li>
-
- <li>
- <a href="#" data-property="severities" data-value="{{s}}">
- {{severityIcon severity}}&nbsp;{{t "severity" severity}}
- </a>
- </li>
-
- <li>
- <a href="#" data-property="statuses" data-value="{{status}}">
- {{statusIcon status}}&nbsp;{{t "issue.status" status}}
- </a>
- </li>
-
- <li>
- {{#if resolution}}
- <a href="#" data-property="resolutions" data-value="{{resolution}}">
- {{t "issue.resolution" resolution}}
- </a>
- {{else}}
- <a href="#" data-property="resolved" data-value="false">
- {{t "unresolved"}}
- </a>
- {{/if}}
- </li>
-
- <li>
- {{#if assignee}}
- <a href="#" class="issue-action-option" data-property="assignees" data-value="{{assignee}}">
- {{t "assigned_to"}}
- {{#ifShowAvatars}}<span class="spacer-left">{{avatarHelperNew assigneeAvatar 16}}</span>{{/ifShowAvatars}}
- {{assigneeName}}
- </a>
- {{else}}
- <a href="#" data-property="assigned" data-value="false">
- {{t "unassigned"}}
- </a>
- {{/if}}
- </li>
-
- <li class="divider"></li>
-
- <li>
- <a href="#" data-property="rules" data-value="{{rule}}">
- {{limitString ruleName}}
- </a>
- </li>
-
- {{#each tags}}
- <li>
- <a href="#" data-property="tags" data-value="{{this}}">
- <i class="icon-tags icon-half-transparent"></i>&nbsp;{{this}}
- </a>
- </li>
- {{/each}}
-
- <li class="divider"></li>
-
- <li>
- <a href="#" data-property="projectUuids" data-value="{{projectUuid}}">
- {{qualifierIcon "TRK"}}&nbsp;{{projectLongName}}
- </a>
- </li>
-
- {{#if subProject}}
- <li>
- <a href="#" data-property="moduleUuids" data-value="{{subProjectUuid}}">
- {{qualifierIcon "BRC"}}&nbsp;{{subProjectLongName}}
- </a>
- </li>
- {{/if}}
-
- <li>
- <a href="#" data-property="fileUuids" data-value="{{componentUuid}}">
- {{qualifierIcon componentQualifier}}&nbsp;{{fileFromPath componentLongName}}
- </a>
- </li>
-</ul>
-
-<div class="bubble-popup-arrow"></div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs
deleted file mode 100644
index 83add61b366..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs
+++ /dev/null
@@ -1,12 +0,0 @@
-<div class="issues search-navigator">
- <div class="search-navigator-side">
- <div class="issues-header"></div>
- <div class="search-navigator-facets"></div>
- </div>
-
- <div class="search-navigator-workspace">
- <div class="search-navigator-workspace-header"></div>
- <div class="search-navigator-workspace-list"></div>
- <div class="issues-workspace-component-viewer"></div>
- </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs
deleted file mode 100644
index 6a28b925ad8..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs
+++ /dev/null
@@ -1,80 +0,0 @@
-<div class="issues-header-component nowrap">
- <div class="component-name">
- {{#if state.component}}
-
- <div class="component-name-parent">
- <a class="js-back">{{t "issues.return_to_list"}}</a>&nbsp;&nbsp;&nbsp;
- </div>
-
- <div class="component-name-parent">
- {{#if organization}}
- <a href="{{link '/organizations/' organization.key}}">{{organization.name}}</a>
- <span class="slash-separator"></span>
- {{/if}}
- {{#with state.component}}
- {{#if project}}
- <a href="{{dashboardUrl project}}" title="{{projectName}}">{{projectName}}</a>
- <span class="slash-separator"></span>
- {{/if}}
- {{#if subProject}}
- <a href="{{dashboardUrl subProject}}" title="{{subProjectName}}">{{subProjectName}}</a>
- <span class="slash-separator"></span>
- {{/if}}
- <a href="{{dashboardUrl key}}" title="{{name}}">{{collapsePath name}}</a>
- {{/with}}
- </div>
-
- {{else}}
- {{#if state.canBulkChange}}
- <a class="js-selection icon-checkbox {{#if allSelected}}icon-checkbox-checked{{/if}} {{#if someSelected}}icon-checkbox-checked icon-checkbox-single{{/if}}"
- data-toggle="tooltip" title="{{t 'issues.toggle_selection_tooltip'}}"></a>
- {{else}}
- &nbsp;
- {{/if}}
- {{/if}}
- </div>
-</div>
-
-
-<div class="search-navigator-header-actions">
- {{#notNull state.total}}
- {{#unless state.component}}
- <div class="issues-header-order">{{t 'issues.ordered'}} <strong>{{t 'issues.by_creation_date'}}</strong></div>
- {{/unless}}
- <div class="search-navigator-header-pagination flash flash-heavy">
- {{#gt state.total 0}}
- <strong>
- <span class="current">
- {{sum state.selectedIndex 1}}
- /
- <span id="issues-total">{{formatMeasure state.total 'INT'}}</span>
- </span>
- </strong>
- {{else}}
- <span class="current">0 / <span id="issues-total">0</span></span>
- {{/gt}}
- {{t 'issues.issues'}}
- </div>
- {{/notNull}}
-
-
- <div class="search-navigator-header-buttons button-group dropdown">
- <button id="issues-reload" class="js-reload">{{t "reload"}}</button>
- <button class="js-new-search" id="issues-new-search">{{t "issue_filter.new_search"}}</button>
- {{#if state.canBulkChange}}
- <button id="issues-bulk-change" class="dropdown-toggle" data-toggle="dropdown">
- {{t "bulk_change"}}
- </button>
- <ul class="dropdown-menu dropdown-menu-right">
- <li>
- <a class="js-bulk-change" href="#">{{tp 'issues.bulk_change' state.total}}</a>
- </li>
- {{#gt selectedCount 0}}
- <li>
- <a class="js-bulk-change-selected" href="#">{{tp 'issues.bulk_change_selected' selectedCount}}</a>
- </li>
- {{/gt}}
- </ul>
- {{/if}}
- </div>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs
deleted file mode 100644
index b0e305ecddb..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs
+++ /dev/null
@@ -1,26 +0,0 @@
-<div class="component-name issues-workspace-list-component">
- {{#if organization}}
- <a class="link-no-underline" href="{{link '/organizations/' organization.key}}">
- {{organization.name}}
- </a>
- <span class="slash-separator"></span>
- {{/if}}
-
- {{#if project}}
- <a class="link-no-underline" href="{{dashboardUrl project}}">
- {{projectLongName}}
- </a>
- <span class="slash-separator"></span>
- {{/if}}
-
- {{#if subProject}}
- <a class="link-no-underline" href="{{dashboardUrl subProject}}">
- {{subProjectLongName}}
- </a>
- <span class="slash-separator"></span>
- {{/if}}
-
- <a class="link-no-underline" href="{{dashboardUrl component}}">
- {{collapsePath componentLongName}}
- </a>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs
deleted file mode 100644
index cae9c964a0b..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs
+++ /dev/null
@@ -1,5 +0,0 @@
-<div class="js-list"></div>
-
-<div class="search-navigator-workspace-list-more">
- <span class="js-more"><i class="spinner"></i></span>
-</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/utils.js b/server/sonar-web/src/main/js/apps/issues/utils.js
new file mode 100644
index 00000000000..3c9d8a235ed
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/utils.js
@@ -0,0 +1,229 @@
+/*
+ * 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 { isNil, omitBy } from 'lodash';
+import { searchMembers } from '../../api/organizations';
+import { searchUsers } from '../../api/users';
+
+export type RawQuery = { [string]: string };
+
+export type Query = {|
+ assigned: boolean,
+ assignees: Array<string>,
+ authors: Array<string>,
+ createdAfter: string,
+ createdAt: string,
+ createdBefore: string,
+ createdInLast: string,
+ directories: Array<string>,
+ facetMode: string,
+ files: Array<string>,
+ issues: Array<string>,
+ languages: Array<string>,
+ modules: Array<string>,
+ projects: Array<string>,
+ resolved: boolean,
+ resolutions: Array<string>,
+ rules: Array<string>,
+ severities: Array<string>,
+ sinceLeakPeriod: boolean,
+ statuses: Array<string>,
+ tags: Array<string>,
+ types: Array<string>
+|};
+
+export type Paging = {
+ pageIndex: number,
+ pageSize: number,
+ total: number
+};
+
+const parseAsBoolean = (value: ?string, defaultValue: boolean = true): boolean =>
+ value === 'false' ? false : value === 'true' ? true : defaultValue;
+
+const parseAsString = (value: ?string): string => value || '';
+
+const parseAsStringArray = (value: ?string): Array<string> => value ? value.split(',') : [];
+
+const parseAsFacetMode = (facetMode: string) =>
+ facetMode === 'debt' || facetMode === 'effort' ? 'effort' : 'count';
+
+export const parseQuery = (query: RawQuery): Query => ({
+ assigned: parseAsBoolean(query.assigned),
+ assignees: parseAsStringArray(query.assignees),
+ authors: parseAsStringArray(query.authors),
+ createdAfter: parseAsString(query.createdAfter),
+ createdAt: parseAsString(query.createdAt),
+ createdBefore: parseAsString(query.createdBefore),
+ createdInLast: parseAsString(query.createdInLast),
+ directories: parseAsStringArray(query.directories),
+ facetMode: parseAsFacetMode(query.facetMode),
+ files: parseAsStringArray(query.fileUuids),
+ issues: parseAsStringArray(query.issues),
+ languages: parseAsStringArray(query.languages),
+ modules: parseAsStringArray(query.moduleUuids),
+ projects: parseAsStringArray(query.projectUuids),
+ resolved: parseAsBoolean(query.resolved),
+ resolutions: parseAsStringArray(query.resolutions),
+ rules: parseAsStringArray(query.rules),
+ severities: parseAsStringArray(query.severities),
+ sinceLeakPeriod: parseAsBoolean(query.sinceLeakPeriod, false),
+ statuses: parseAsStringArray(query.statuses),
+ tags: parseAsStringArray(query.tags),
+ types: parseAsStringArray(query.types)
+});
+
+export const getOpen = (query: RawQuery) => query.open;
+
+export const areMyIssuesSelected = (query: RawQuery): boolean => query.myIssues === 'true';
+
+const serializeString = (value: string): ?string => value || undefined;
+
+const serializeValue = (value: Array<string>): ?string => value.length ? value.join() : undefined;
+
+export const serializeQuery = (query: Query): RawQuery => {
+ const filter = {
+ assigned: query.assigned ? undefined : 'false',
+ assignees: serializeValue(query.assignees),
+ authors: serializeValue(query.authors),
+ createdAfter: serializeString(query.createdAfter),
+ createdAt: serializeString(query.createdAt),
+ createdBefore: serializeString(query.createdBefore),
+ createdInLast: serializeString(query.createdInLast),
+ directories: serializeValue(query.directories),
+ facetMode: query.facetMode === 'effort' ? serializeString(query.facetMode) : undefined,
+ fileUuids: serializeValue(query.files),
+ issues: serializeValue(query.issues),
+ languages: serializeValue(query.languages),
+ moduleUuids: serializeValue(query.modules),
+ projectUuids: serializeValue(query.projects),
+ resolved: query.resolved ? undefined : 'false',
+ resolutions: serializeValue(query.resolutions),
+ severities: serializeValue(query.severities),
+ sinceLeakPeriod: query.sinceLeakPeriod ? 'true' : undefined,
+ statuses: serializeValue(query.statuses),
+ rules: serializeValue(query.rules),
+ tags: serializeValue(query.tags),
+ types: serializeValue(query.types)
+ };
+ return omitBy(filter, isNil);
+};
+
+const areArraysEqual = (a: Array<string>, b: Array<string>) => {
+ if (a.length !== b.length) {
+ return false;
+ }
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== b[i]) {
+ return false;
+ }
+ }
+ return true;
+};
+
+export const areQueriesEqual = (a: RawQuery, b: RawQuery) => {
+ const parsedA: Query = parseQuery(a);
+ const parsedB: Query = parseQuery(b);
+
+ const keysA = Object.keys(parsedA);
+ const keysB = Object.keys(parsedB);
+
+ if (keysA.length !== keysB.length) {
+ return false;
+ }
+
+ return keysA.every(
+ key =>
+ Array.isArray(parsedA[key]) && Array.isArray(parsedB[key])
+ ? areArraysEqual(parsedA[key], parsedB[key])
+ : parsedA[key] === parsedB[key]
+ );
+};
+
+type RawFacet = {
+ property: string,
+ values: Array<{ val: string, count: number }>
+};
+
+export type Facet = { [string]: number };
+
+export const parseFacets = (facets: Array<RawFacet>): { [string]: Facet } => {
+ // for readability purpose
+ const propertyMapping = {
+ fileUuids: 'files',
+ moduleUuids: 'modules',
+ projectUuids: 'projects'
+ };
+
+ const result = {};
+ facets.forEach(facet => {
+ const values = {};
+ facet.values.forEach(value => {
+ values[value.val] = value.count;
+ });
+ const finalProperty = propertyMapping[facet.property] || facet.property;
+ result[finalProperty] = values;
+ });
+ return result;
+};
+
+export type ReferencedComponent = {
+ key: string,
+ name: string,
+ organization: string,
+ path: string
+};
+
+export type ReferencedUser = {
+ avatar: string,
+ name: string
+};
+
+export type ReferencedLanguage = {
+ name: string
+};
+
+export type Component = {
+ key: string,
+ organization: string,
+ qualifier: string
+};
+
+export type CurrentUser =
+ | { isLoggedIn: false }
+ | { isLoggedIn: true, email?: string, login: string, name: string };
+
+export const searchAssignees = (query: string, component?: Component) => {
+ return component
+ ? searchMembers({ organization: component.organization, ps: 50, q: query }).then(response =>
+ response.users.map(user => ({
+ avatar: user.avatar,
+ label: user.name,
+ value: user.login
+ })))
+ : searchUsers(query, 50).then(response =>
+ response.users.map(user => ({
+ // TODO this WS returns no avatar
+ avatar: user.avatar,
+ email: user.email,
+ label: user.name,
+ value: user.login
+ })));
+};
diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js
deleted file mode 100644
index 0fccbb3ab65..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js
+++ /dev/null
@@ -1,151 +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 WorkspaceHeaderView from '../../components/navigator/workspace-header-view';
-import BulkChangeForm from './BulkChangeForm';
-import Template from './templates/issues-workspace-header.hbs';
-import { getOrganization, areThereCustomOrganizations } from '../../store/organizations/utils';
-
-export default WorkspaceHeaderView.extend({
- template: Template,
-
- initialize() {
- this.context = {
- isContext: this.options.app.state.get('isContext'),
- organization: this.options.app.state.get('contextOrganization')
- };
- },
-
- events() {
- return {
- ...WorkspaceHeaderView.prototype.events.apply(this, arguments),
- 'click .js-selection': 'onSelectionClick',
- 'click .js-back': 'returnToList',
- 'click .js-new-search': 'newSearch',
- 'click .js-bulk-change-selected': 'onBulkChangeSelectedClick'
- };
- },
-
- onSelectionClick(e) {
- e.preventDefault();
- this.toggleSelection();
- },
-
- onBulkChangeSelectedClick(e) {
- e.preventDefault();
- this.bulkChangeSelected();
- },
-
- afterBulkChange() {
- const selectedIndex = this.options.app.state.get('selectedIndex');
- const selectedKeys = this.options.app.list.where({ selected: true }).map(item => item.id);
- this.options.app.controller.fetchList().done(() => {
- this.options.app.state.set({ selectedIndex });
- this.options.app.list.selectByKeys(selectedKeys);
- });
- },
-
- render() {
- if (!this._suppressUpdate) {
- WorkspaceHeaderView.prototype.render.apply(this, arguments);
- }
- },
-
- toggleSelection() {
- this._suppressUpdate = true;
- const selectedCount = this.options.app.list.where({ selected: true }).length;
- const someSelected = selectedCount > 0;
- return someSelected ? this.selectNone() : this.selectAll();
- },
-
- selectNone() {
- this.options.app.list.where({ selected: true }).forEach(issue => {
- issue.set({ selected: false });
- });
- this._suppressUpdate = false;
- this.render();
- },
-
- selectAll() {
- this.options.app.list.forEach(issue => {
- issue.set({ selected: true });
- });
- this._suppressUpdate = false;
- this.render();
- },
-
- returnToList() {
- this.options.app.controller.closeComponentViewer();
- },
-
- newSearch() {
- this.options.app.controller.newSearch();
- },
-
- bulkChange() {
- const query = this.options.app.controller.getQueryAsObject();
- new BulkChangeForm({
- query,
- context: this.context,
- onChange: () => this.afterBulkChange()
- }).render();
- },
-
- bulkChangeSelected() {
- const selected = this.options.app.list.where({ selected: true });
- const selectedKeys = selected.map(item => item.id).slice(0, 500);
- const query = { issues: selectedKeys.join() };
- new BulkChangeForm({
- query,
- context: this.context,
- onChange: () => this.afterBulkChange()
- }).render();
- },
-
- serializeData() {
- const issuesCount = this.options.app.list.length;
- const selectedCount = this.options.app.list.where({ selected: true }).length;
- const allSelected = issuesCount > 0 && issuesCount === selectedCount;
- const someSelected = !allSelected && selectedCount > 0;
- const data = {
- ...WorkspaceHeaderView.prototype.serializeData.apply(this, arguments),
- selectedCount,
- allSelected,
- someSelected
- };
- const component = this.options.app.state.get('component');
- if (component) {
- const qualifier = this.options.app.state.get('contextComponentQualifier');
- if (qualifier === 'VW' || qualifier === 'SVW') {
- // do nothing
- } else if (qualifier === 'TRK') {
- data.state.component.project = null;
- } else if (qualifier === 'BRC') {
- data.state.component.project = null;
- data.state.component.subProject = null;
- } else {
- const organization = areThereCustomOrganizations()
- ? getOrganization(component.projectOrganization)
- : null;
- Object.assign(data, { organization });
- }
- }
- return data;
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js
deleted file mode 100644
index ec59af550c0..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js
+++ /dev/null
@@ -1,162 +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 React from 'react';
-import { render, unmountComponentAtNode } from 'react-dom';
-import Marionette from 'backbone.marionette';
-import ConnectedIssue from '../../components/issue/ConnectedIssue';
-import IssueFilterView from './issue-filter-view';
-import WithStore from '../../components/shared/WithStore';
-import getStore from '../../app/utils/getStore';
-import { getIssueByKey } from '../../store/rootReducer';
-
-const SHOULD_NULL = {
- any: ['issues'],
- resolutions: ['resolved'],
- resolved: ['resolutions'],
- assignees: ['assigned'],
- assigned: ['assignees']
-};
-
-export default Marionette.ItemView.extend({
- className: 'issues-workspace-list-item',
-
- initialize(options) {
- this.openComponentViewer = this.openComponentViewer.bind(this);
- this.onIssueFilterClick = this.onIssueFilterClick.bind(this);
- this.onIssueCheck = this.onIssueCheck.bind(this);
- this.listenTo(options.app.state, 'change:selectedIndex', this.showIssue);
- this.listenTo(this.model, 'change:selected', this.showIssue);
- this.subscribeToStore();
- },
-
- template() {
- return '<div></div>';
- },
-
- subscribeToStore() {
- const store = getStore();
- store.subscribe(() => {
- const issue = getIssueByKey(store.getState(), this.model.get('key'));
- this.model.set(issue);
- });
- },
-
- onRender() {
- this.showIssue();
- },
-
- onDestroy() {
- unmountComponentAtNode(this.el);
- },
-
- showIssue() {
- const selected = this.model.get('index') === this.options.app.state.get('selectedIndex');
-
- render(
- <WithStore>
- <ConnectedIssue
- issueKey={this.model.get('key')}
- checked={this.model.get('selected')}
- onCheck={this.onIssueCheck}
- onClick={this.openComponentViewer}
- onFilterClick={this.onIssueFilterClick}
- selected={selected}
- />
- </WithStore>,
- this.el
- );
- },
-
- onIssueFilterClick(e) {
- const that = this;
- e.preventDefault();
- e.stopPropagation();
- $('body').click();
- this.popup = new IssueFilterView({
- triggerEl: $(e.currentTarget),
- bottomRight: true,
- model: this.model
- });
- this.popup.on('select', (property, value) => {
- const obj = {};
- obj[property] = '' + value;
- SHOULD_NULL.any.forEach(p => {
- obj[p] = null;
- });
- if (SHOULD_NULL[property] != null) {
- SHOULD_NULL[property].forEach(p => {
- obj[p] = null;
- });
- }
- that.options.app.state.updateFilter(obj);
- that.popup.destroy();
- });
- this.popup.render();
- },
-
- onIssueCheck(e) {
- e.preventDefault();
- e.stopPropagation();
- this.model.set({ selected: !this.model.get('selected') });
- const selected = this.model.collection.where({ selected: true }).length;
- this.options.app.state.set({ selected });
- },
-
- changeSelection() {
- const selected = this.model.get('index') === this.options.app.state.get('selectedIndex');
- if (selected) {
- this.select();
- } else {
- this.unselect();
- }
- },
-
- selectCurrent() {
- this.options.app.state.set({ selectedIndex: this.model.get('index') });
- },
-
- resetIssue(options) {
- const that = this;
- const key = this.model.get('key');
- const componentUuid = this.model.get('componentUuid');
- const index = this.model.get('index');
- const selected = this.model.get('selected');
- this.model.reset(
- {
- key,
- componentUuid,
- index,
- selected
- },
- { silent: true }
- );
- return this.model.fetch(options).done(() => that.trigger('reset'));
- },
-
- openComponentViewer() {
- this.options.app.state.set({ selectedIndex: this.model.get('index') });
- if (this.options.app.state.has('component')) {
- return this.options.app.controller.closeComponentViewer();
- } else {
- return this.options.app.controller.showComponentViewer(this.model);
- }
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js
deleted file mode 100644
index f74b7c912b2..00000000000
--- a/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js
+++ /dev/null
@@ -1,125 +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 WorkspaceListView from '../../components/navigator/workspace-list-view';
-import IssueView from './workspace-list-item-view';
-import EmptyView from './workspace-list-empty-view';
-import Template from './templates/issues-workspace-list.hbs';
-import ComponentTemplate from './templates/issues-workspace-list-component.hbs';
-import { getOrganization, areThereCustomOrganizations } from '../../store/organizations/utils';
-
-const COMPONENT_HEIGHT = 29;
-const BOTTOM_OFFSET = 60;
-
-export default WorkspaceListView.extend({
- template: Template,
- componentTemplate: ComponentTemplate,
- childView: IssueView,
- childViewContainer: '.js-list',
- emptyView: EmptyView,
-
- bindShortcuts() {
- const that = this;
- WorkspaceListView.prototype.bindShortcuts.apply(this, arguments);
- key('right', 'list', () => {
- const selectedIssue = that.collection.at(that.options.app.state.get('selectedIndex'));
- that.options.app.controller.showComponentViewer(selectedIssue);
- return false;
- });
- key('space', 'list', () => {
- const selectedIssue = that.collection.at(that.options.app.state.get('selectedIndex'));
- selectedIssue.set({ selected: !selectedIssue.get('selected') });
- return false;
- });
- },
-
- unbindShortcuts() {
- WorkspaceListView.prototype.unbindShortcuts.apply(this, arguments);
- key.unbind('right', 'list');
- key.unbind('space', 'list');
- },
-
- scrollTo() {
- const selectedIssue = this.collection.at(this.options.app.state.get('selectedIndex'));
- if (selectedIssue == null) {
- return;
- }
- const selectedIssueView = this.children.findByModel(selectedIssue);
- const parentTopOffset = this.$el.offset().top;
- let viewTop = selectedIssueView.$el.offset().top - parentTopOffset;
- if (selectedIssueView.$el.prev().is('.issues-workspace-list-component')) {
- viewTop -= COMPONENT_HEIGHT;
- }
- const viewBottom = selectedIssueView.$el.offset().top +
- selectedIssueView.$el.outerHeight() +
- BOTTOM_OFFSET;
- const windowTop = $(window).scrollTop();
- const windowBottom = windowTop + $(window).height();
- if (viewTop < windowTop) {
- $(window).scrollTop(viewTop);
- }
- if (viewBottom > windowBottom) {
- $(window).scrollTop($(window).scrollTop() - windowBottom + viewBottom);
- }
- },
-
- attachHtml(compositeView, childView, index) {
- const container = this.getChildViewContainer(compositeView);
- const model = this.collection.at(index);
- if (model != null) {
- const prev = index > 0 && this.collection.at(index - 1);
- let putComponent = !prev;
- if (prev) {
- const fullComponent = [model.get('project'), model.get('component')].join(' ');
- const fullPrevComponent = [prev.get('project'), prev.get('component')].join(' ');
- if (fullComponent !== fullPrevComponent) {
- putComponent = true;
- }
- }
- if (putComponent) {
- this.displayComponent(container, model);
- }
- }
- container.append(childView.el);
- },
-
- displayComponent(container, model) {
- const data = { ...model.toJSON() };
- const qualifier = this.options.app.state.get('contextComponentQualifier');
- if (qualifier === 'VW' || qualifier === 'SVW') {
- Object.assign(data, { organization: undefined });
- } else if (qualifier === 'TRK') {
- Object.assign(data, { organization: undefined, project: undefined });
- } else if (qualifier === 'BRC') {
- Object.assign(data, { organization: undefined, project: undefined, subProject: undefined });
- } else {
- const organization = areThereCustomOrganizations()
- ? getOrganization(model.get('projectOrganization'))
- : null;
- Object.assign(data, { organization });
- }
- container.append(this.componentTemplate(data));
- },
-
- destroyChildren() {
- WorkspaceListView.prototype.destroyChildren.apply(this, arguments);
- this.$('.issues-workspace-list-component').remove();
- }
-});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js
index bf1b7d89862..1fba6f72119 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationFavoriteProjects.js
@@ -51,7 +51,7 @@ class OrganizationFavoriteProjects extends React.Component {
render() {
return (
- <div id="projects-page" className="page page-limited">
+ <div id="projects-page">
<Helmet title={translate('projects.page')} titleTemplate="%s - SonarQube" />
<FavoriteProjectsContainer
location={this.props.location}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js
index 77eabf062da..67dbdf8bcd4 100644
--- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js
+++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationProjects.js
@@ -51,7 +51,7 @@ class OrganizationProjects extends React.Component {
render() {
return (
- <div id="projects-page" className="page page-limited">
+ <div id="projects-page">
<Helmet title={translate('projects.page')} titleTemplate="%s - SonarQube" />
<AllProjectsContainer
isFavorite={false}
diff --git a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap
index d04badd1faf..b29c17b3a5c 100644
--- a/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/__snapshots__/QualityGateCondition-test.js.snap
@@ -47,7 +47,18 @@ exports[`test new_reliability_rating 1`] = `
className="overview-quality-gate-condition overview-quality-gate-condition-error"
onlyActiveOnIndex={false}
style={Object {}}
- to="/component_issues?id=abcd-key#resolved=false|types=BUG|severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR|sinceLeakPeriod=true">
+ to={
+ Object {
+ "pathname": "/project/issues",
+ "query": Object {
+ "id": "abcd-key",
+ "resolved": "false",
+ "severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
+ "sinceLeakPeriod": "true",
+ "types": "BUG",
+ },
+ }
+ }>
<div
className="overview-quality-gate-condition-container">
<div
@@ -91,7 +102,18 @@ exports[`test new_security_rating 1`] = `
className="overview-quality-gate-condition overview-quality-gate-condition-error"
onlyActiveOnIndex={false}
style={Object {}}
- to="/component_issues?id=abcd-key#resolved=false|types=VULNERABILITY|severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR|sinceLeakPeriod=true">
+ to={
+ Object {
+ "pathname": "/project/issues",
+ "query": Object {
+ "id": "abcd-key",
+ "resolved": "false",
+ "severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
+ "sinceLeakPeriod": "true",
+ "types": "VULNERABILITY",
+ },
+ }
+ }>
<div
className="overview-quality-gate-condition-container">
<div
@@ -135,7 +157,17 @@ exports[`test new_sqale_rating 1`] = `
className="overview-quality-gate-condition overview-quality-gate-condition-error"
onlyActiveOnIndex={false}
style={Object {}}
- to="/component_issues?id=abcd-key#resolved=false|types=CODE_SMELL|sinceLeakPeriod=true">
+ to={
+ Object {
+ "pathname": "/project/issues",
+ "query": Object {
+ "id": "abcd-key",
+ "resolved": "false",
+ "sinceLeakPeriod": "true",
+ "types": "CODE_SMELL",
+ },
+ }
+ }>
<div
className="overview-quality-gate-condition-container">
<div
@@ -223,7 +255,17 @@ exports[`test reliability_rating 1`] = `
className="overview-quality-gate-condition overview-quality-gate-condition-error"
onlyActiveOnIndex={false}
style={Object {}}
- to="/component_issues?id=abcd-key#resolved=false|types=BUG|severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR">
+ to={
+ Object {
+ "pathname": "/project/issues",
+ "query": Object {
+ "id": "abcd-key",
+ "resolved": "false",
+ "severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
+ "types": "BUG",
+ },
+ }
+ }>
<div
className="overview-quality-gate-condition-container">
<div
@@ -267,7 +309,17 @@ exports[`test security_rating 1`] = `
className="overview-quality-gate-condition overview-quality-gate-condition-error"
onlyActiveOnIndex={false}
style={Object {}}
- to="/component_issues?id=abcd-key#resolved=false|types=VULNERABILITY|severities=BLOCKER%2CCRITICAL%2CMAJOR%2CMINOR">
+ to={
+ Object {
+ "pathname": "/project/issues",
+ "query": Object {
+ "id": "abcd-key",
+ "resolved": "false",
+ "severities": "BLOCKER,CRITICAL,MAJOR,MINOR",
+ "types": "VULNERABILITY",
+ },
+ }
+ }>
<div
className="overview-quality-gate-condition-container">
<div
@@ -311,7 +363,16 @@ exports[`test sqale_rating 1`] = `
className="overview-quality-gate-condition overview-quality-gate-condition-error"
onlyActiveOnIndex={false}
style={Object {}}
- to="/component_issues?id=abcd-key#resolved=false|types=CODE_SMELL">
+ to={
+ Object {
+ "pathname": "/project/issues",
+ "query": Object {
+ "id": "abcd-key",
+ "resolved": "false",
+ "types": "CODE_SMELL",
+ },
+ }
+ }>
<div
className="overview-quality-gate-condition-container">
<div
diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js
index 2d97ab9b996..9fd4ffc417e 100644
--- a/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js
+++ b/server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.js
@@ -22,7 +22,7 @@ import { Link } from 'react-router';
import { difference } from 'lodash';
import Backbone from 'backbone';
import { PermissionTemplateType, CallbackType } from '../propTypes';
-import QualifierIcon from '../../../components/shared/qualifier-icon';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
import UpdateView from '../views/UpdateView';
import DeleteView from '../views/DeleteView';
import { translate } from '../../../helpers/l10n';
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js b/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js
index 7928cb05f4c..bb722731c4f 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js
@@ -19,7 +19,7 @@
*/
import React from 'react';
import UpdateKeyForm from './UpdateKeyForm';
-import QualifierIcon from '../../../components/shared/qualifier-icon';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
export default class FineGrainedUpdate extends React.Component {
render() {
diff --git a/server/sonar-web/src/main/js/apps/projects-admin/projects.js b/server/sonar-web/src/main/js/apps/projects-admin/projects.js
index 88c0940368d..f3984b0cbdd 100644
--- a/server/sonar-web/src/main/js/apps/projects-admin/projects.js
+++ b/server/sonar-web/src/main/js/apps/projects-admin/projects.js
@@ -23,7 +23,7 @@ import { Link } from 'react-router';
import { getComponentPermissionsUrl } from '../../helpers/urls';
import ApplyTemplateView from '../permissions/project/views/ApplyTemplateView';
import Checkbox from '../../components/controls/Checkbox';
-import QualifierIcon from '../../components/shared/qualifier-icon';
+import QualifierIcon from '../../components/shared/QualifierIcon';
import { translate } from '../../helpers/l10n';
export default class Projects extends React.Component {
diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
index ec5299b31a8..c62d9e8c612 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
+++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
@@ -24,6 +24,11 @@ import ProjectsListFooterContainer from './ProjectsListFooterContainer';
import PageSidebar from './PageSidebar';
import VisualizationsContainer from '../visualizations/VisualizationsContainer';
import { parseUrlQuery } from '../store/utils';
+import Page from '../../../components/layout/Page';
+import PageMain from '../../../components/layout/PageMain';
+import PageMainInner from '../../../components/layout/PageMainInner';
+import PageSide from '../../../components/layout/PageSide';
+import PageFilters from '../../../components/layout/PageFilters';
import '../styles.css';
export default class AllProjects extends React.Component {
@@ -95,38 +100,41 @@ export default class AllProjects extends React.Component {
const top = this.props.organization ? 95 : 30;
return (
- <div className="page-with-sidebar page-with-left-sidebar projects-page">
- <aside className="page-sidebar-fixed page-sidebar-sticky projects-sidebar">
- <div className="page-sidebar-sticky-inner" style={{ top }}>
+ <Page className="projects-page">
+ <PageSide top={top}>
+ <PageFilters>
<PageSidebar
query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
- </div>
- </aside>
- <div className="page-main">
- <PageHeaderContainer onViewChange={this.handleViewChange} view={view} />
- {view === 'list' &&
- <ProjectsListContainer
- isFavorite={this.props.isFavorite}
- isFiltered={isFiltered}
- organization={this.props.organization}
- />}
- {view === 'list' &&
- <ProjectsListFooterContainer
- query={query}
- isFavorite={this.props.isFavorite}
- organization={this.props.organization}
- />}
- {view === 'visualizations' &&
- <VisualizationsContainer
- onVisualizationChange={this.handleVisualizationChange}
- sort={query.sort}
- visualization={visualization}
- />}
- </div>
- </div>
+ </PageFilters>
+ </PageSide>
+
+ <PageMain>
+ <PageMainInner>
+ <PageHeaderContainer onViewChange={this.handleViewChange} view={view} />
+ {view === 'list' &&
+ <ProjectsListContainer
+ isFavorite={this.props.isFavorite}
+ isFiltered={isFiltered}
+ organization={this.props.organization}
+ />}
+ {view === 'list' &&
+ <ProjectsListFooterContainer
+ query={query}
+ isFavorite={this.props.isFavorite}
+ organization={this.props.organization}
+ />}
+ {view === 'visualizations' &&
+ <VisualizationsContainer
+ onVisualizationChange={this.handleVisualizationChange}
+ sort={query.sort}
+ visualization={visualization}
+ />}
+ </PageMainInner>
+ </PageMain>
+ </Page>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/App.js b/server/sonar-web/src/main/js/apps/projects/components/App.js
index c5c3b94bda1..69c18380bb3 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/App.js
+++ b/server/sonar-web/src/main/js/apps/projects/components/App.js
@@ -32,7 +32,7 @@ export default class App extends React.Component {
render() {
return (
- <div id="projects-page" className="page page-limited">
+ <div id="projects-page">
<Helmet title={translate('projects.page')} titleTemplate="%s - SonarQube" />
{this.props.children}
</div>
diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js
index ed924559a85..d102a9dcef4 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js
+++ b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js
@@ -55,14 +55,14 @@ export default class PageSidebar extends React.PureComponent {
: undefined;
return (
- <div className="search-navigator-facets-list">
+ <div>
<FavoriteFilterContainer organization={this.props.organization} />
<div className="projects-facets-header clearfix">
{isFiltered &&
<div className="projects-facets-reset">
<Link to={{ pathname, query: linkQuery }} className="button button-red">
- {translate('projects.clear_all_filters')}
+ {translate('clear_all_filters')}
</Link>
</div>}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js
index 1c4cae090f2..d692e2ca9ce 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js
+++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectsList.js
@@ -19,9 +19,9 @@
*/
import React from 'react';
import ProjectCardContainer from './ProjectCardContainer';
-import NoProjects from './NoProjects';
import NoFavoriteProjects from './NoFavoriteProjects';
import EmptyInstance from './EmptyInstance';
+import EmptySearch from '../../../components/common/EmptySearch';
export default class ProjectsList extends React.PureComponent {
static propTypes = {
@@ -37,7 +37,7 @@ export default class ProjectsList extends React.PureComponent {
} else if (!this.props.isFiltered) {
return <EmptyInstance />;
} else {
- return <NoProjects />;
+ return <EmptySearch />;
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap
index 211b0fbafb1..faf96ac7d0a 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap
@@ -16,7 +16,7 @@ exports[`test should handle \`view\` and \`visualization\` 2`] = `
},
}
}>
- projects.clear_all_filters
+ clear_all_filters
</Link>
</div>
`;
diff --git a/server/sonar-web/src/main/js/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css
index 45f8cd2663b..99e3232f7d0 100644
--- a/server/sonar-web/src/main/js/apps/projects/styles.css
+++ b/server/sonar-web/src/main/js/apps/projects/styles.css
@@ -10,14 +10,6 @@
margin-bottom: 0;
}
-.projects-empty-list {
- padding: 60px 0;
- border: 1px solid #e6e6e6;
- border-radius: 2px;
- text-align: center;
- color: #777;
-}
-
.project-card {
position: relative;
min-height: 121px;
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js
index 6d877d785fa..171d4ab8e0d 100644
--- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js
+++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js
@@ -21,7 +21,7 @@
import React from 'react';
import { Link } from 'react-router';
import ChangeProjectsView from '../views/ChangeProjectsView';
-import QualifierIcon from '../../../components/shared/qualifier-icon';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
import { getProfileProjects } from '../../../api/quality-profiles';
import { translate } from '../../../helpers/l10n';
import type { Profile } from '../propTypes';