aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGrégoire Aubert <gregaubert@users.noreply.github.com>2017-03-16 10:37:59 +0100
committerGitHub <noreply@github.com>2017-03-16 10:37:59 +0100
commit24998ec360f43a7a745d88a5856d5828ba2d11ad (patch)
tree90aa0d3cbece6841da79a6782e16bfac6ebc2a78
parent9db4eb325adf58b3c51bc7ae0bce65a361e98a74 (diff)
downloadsonarqube-24998ec360f43a7a745d88a5856d5828ba2d11ad.tar.gz
sonarqube-24998ec360f43a7a745d88a5856d5828ba2d11ad.zip
SONAR-8922 Add tags facet on project page (#1790)
* Refactor the way filters are rendered * SONAR-8922 Add the tags facet on the projects page * SONAR-8923 Add the tags searchbox
-rw-r--r--it/it-tests/src/test/java/it/projectSearch/ProjectsPageTest.java20
-rw-r--r--it/it-tests/src/test/java/pageobjects/projects/FacetItem.java5
-rw-r--r--server/sonar-web/src/main/js/api/components.js10
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js9
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js48
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js54
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/Filter.js47
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/FilterContainer.js32
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/FilterHeader.js39
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.js46
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/LanguageFilter.js73
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.js92
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.js35
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.js7
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js34
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.js7
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SearchFilter.js5
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.js5
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterFooter.js (renamed from server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterFooter.js)52
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterOption.js (renamed from server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterOption.js)14
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.js7
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js38
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/SortingFilter.js31
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/TagsFilter.js119
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/TagsFilterContainer.js33
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.js (renamed from server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguageFilterFooter-test.js)28
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchableFilterFooter-test.js58
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/TagsFilter-test.js51
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/LanguageFilterFooter-test.js.snap58
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/LanguagesFilter-test.js.snap268
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchableFilterFooter-test.js.snap123
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/TagsFilter-test.js.snap279
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/containers.js55
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/actions.js3
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js3
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/utils.js19
36 files changed, 1387 insertions, 420 deletions
diff --git a/it/it-tests/src/test/java/it/projectSearch/ProjectsPageTest.java b/it/it-tests/src/test/java/it/projectSearch/ProjectsPageTest.java
index 280bd54cb7d..45dceeac2c6 100644
--- a/it/it-tests/src/test/java/it/projectSearch/ProjectsPageTest.java
+++ b/it/it-tests/src/test/java/it/projectSearch/ProjectsPageTest.java
@@ -26,6 +26,7 @@ import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
+import org.sonarqube.ws.client.PostRequest;
import org.sonarqube.ws.client.WsClient;
import pageobjects.Navigation;
import pageobjects.projects.ProjectsPage;
@@ -118,7 +119,7 @@ public class ProjectsPageTest {
}
@Test
- public void should_add_language() {
+ public void should_add_language_to_facet() {
ProjectsPage page = nav.openProjects();
page.getFacetByProperty("languages")
.selectOptionItem("xoo2")
@@ -126,6 +127,23 @@ public class ProjectsPageTest {
}
@Test
+ public void should_add_tag_to_facet() {
+ // Add some tags to this project
+ wsClient.wsConnector().call(
+ new PostRequest("api/project_tags/set")
+ .setParam("project", PROJECT_KEY)
+ .setParam("tags", "aa,bb,cc,dd,ee,ff,gg,hh,ii,jj,zz")
+ );
+
+ ProjectsPage page = nav.openProjects();
+ page.getFacetByProperty("tags")
+ .shouldHaveValue("aa", "1")
+ .shouldHaveValue("ii", "1")
+ .selectOptionItem("zz")
+ .shouldHaveValue("zz", "1");
+ }
+
+ @Test
public void should_sort_by_facet() {
ProjectsPage page = nav.openProjects();
page.getFacetByProperty("duplications")
diff --git a/it/it-tests/src/test/java/pageobjects/projects/FacetItem.java b/it/it-tests/src/test/java/pageobjects/projects/FacetItem.java
index 79ff84d1005..528cb3c2505 100644
--- a/it/it-tests/src/test/java/pageobjects/projects/FacetItem.java
+++ b/it/it-tests/src/test/java/pageobjects/projects/FacetItem.java
@@ -42,7 +42,10 @@ public class FacetItem {
}
public FacetItem selectOptionItem(String value) {
- this.elt.$(".Select-input input").val(value).pressEnter();
+ SelenideElement selectInput = this.elt.$(".Select-input input");
+ selectInput.val(value);
+ this.elt.$("div.Select-option.is-focused").should(Condition.exist);
+ selectInput.pressEnter();
return this;
}
diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js
index 456cb90e0e5..f0469af18ec 100644
--- a/server/sonar-web/src/main/js/api/components.js
+++ b/server/sonar-web/src/main/js/api/components.js
@@ -56,6 +56,16 @@ export function createProject (data: {
return postJSON(url, data);
}
+export function searchProjectTags (data?: { ps?: number, q?: string }) {
+ const url = '/api/project_tags/search';
+ return getJSON(url, data);
+}
+
+export function setProjectTags (data: { project: string, tags: string }) {
+ const url = '/api/project_tags/set';
+ return postJSON(url, data);
+}
+
export function getComponentTree (
strategy: string,
componentKey: string,
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 f3c3a21b04e..11c4fb75cef 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
@@ -27,8 +27,9 @@ import QualityGateFilter from '../filters/QualityGateFilter';
import ReliabilityFilter from '../filters/ReliabilityFilter';
import SecurityFilter from '../filters/SecurityFilter';
import MaintainabilityFilter from '../filters/MaintainabilityFilter';
-import LanguageFilter from '../filters/LanguageFilter';
+import TagsFilterContainer from '../filters/TagsFilterContainer';
import SearchFilterContainer from '../filters/SearchFilterContainer';
+import LanguagesFilterContainer from '../filters/LanguagesFilterContainer';
import { translate } from '../../../helpers/l10n';
export default class PageSidebar extends React.PureComponent {
@@ -93,7 +94,11 @@ export default class PageSidebar extends React.PureComponent {
query={this.props.query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}/>
- <LanguageFilter
+ <LanguagesFilterContainer
+ query={this.props.query}
+ isFavorite={this.props.isFavorite}
+ organization={this.props.organization}/>
+ <TagsFilterContainer
query={this.props.query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}/>
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js
index 8789a3aef5e..69312bb565d 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js
+++ b/server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js
@@ -18,7 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import { FilterContainer } from './containers';
+import FilterContainer from './FilterContainer';
+import FilterHeader from './FilterHeader';
import SortingFilter from './SortingFilter';
import CoverageRating from '../../../components/ui/CoverageRating';
import { getCoverageRatingLabel, getCoverageRatingAverageValue } from '../../../helpers/ratings';
@@ -32,7 +33,7 @@ export default class CoverageFilter extends React.PureComponent {
property = 'coverage';
- renderOption = (option, selected) => {
+ renderOption (option, selected) {
return (
<span>
<CoverageRating
@@ -44,41 +45,34 @@ export default class CoverageFilter extends React.PureComponent {
</span>
</span>
);
- };
-
- renderSort = () => {
- return (
- <SortingFilter
- property={this.property}
- query={this.props.query}
- isFavorite={this.props.isFavorite}
- organization={this.props.organization}
- sortDesc="right"/>
- );
- };
+ }
- getFacetValueForOption = (facet, option) => {
+ getFacetValueForOption (facet, option) {
const map = ['80.0-*', '70.0-80.0', '50.0-70.0', '30.0-50.0', '*-30.0'];
return facet[map[option - 1]];
- };
-
- getOptions = () => [1, 2, 3, 4, 5];
-
- renderName = () => 'Coverage';
+ }
render () {
return (
<FilterContainer
property={this.property}
- getOptions={this.getOptions}
- renderName={this.renderName}
- renderOption={this.renderOption}
- renderSort={this.renderSort}
- highlightUnder={1}
- getFacetValueForOption={this.getFacetValueForOption}
+ options={[1, 2, 3, 4, 5]}
query={this.props.query}
+ renderOption={this.renderOption}
isFavorite={this.props.isFavorite}
- organization={this.props.organization}/>
+ organization={this.props.organization}
+ getFacetValueForOption={this.getFacetValueForOption}
+ highlightUnder={1}
+ header={
+ <FilterHeader name="Coverage">
+ <SortingFilter
+ property={this.property}
+ query={this.props.query}
+ isFavorite={this.props.isFavorite}
+ organization={this.props.organization}
+ sortDesc="right"/>
+ </FilterHeader>
+ }/>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js
index 94e47088095..48dbc3331cd 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js
+++ b/server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js
@@ -18,7 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import { FilterContainer } from './containers';
+import FilterContainer from './FilterContainer';
+import FilterHeader from './FilterHeader';
import SortingFilter from './SortingFilter';
import DuplicationsRating from '../../../components/ui/DuplicationsRating';
import {
@@ -35,7 +36,7 @@ export default class DuplicationsFilter extends React.PureComponent {
property = 'duplications';
- renderOption = (option, selected) => {
+ renderOption (option, selected) {
return (
<span>
<DuplicationsRating
@@ -47,40 +48,33 @@ export default class DuplicationsFilter extends React.PureComponent {
</span>
</span>
);
- };
-
- renderSort = () => {
- return (
- <SortingFilter
- property={this.property}
- query={this.props.query}
- isFavorite={this.props.isFavorite}
- organization={this.props.organization}/>
- );
- };
+ }
- getFacetValueForOption = (facet, option) => {
+ getFacetValueForOption (facet, option) {
const map = ['*-3.0', '3.0-5.0', '5.0-10.0', '10.0-20.0', '20.0-*'];
return facet[map[option - 1]];
- };
-
- getOptions = () => [1, 2, 3, 4, 5];
-
- renderName = () => 'Duplications';
+ }
render () {
return (
- <FilterContainer
- property={this.property}
- getOptions={this.getOptions}
- renderName={this.renderName}
- renderOption={this.renderOption}
- renderSort={this.renderSort}
- highlightUnder={1}
- getFacetValueForOption={this.getFacetValueForOption}
- query={this.props.query}
- isFavorite={this.props.isFavorite}
- organization={this.props.organization}/>
+ <FilterContainer
+ property={this.property}
+ options={[1, 2, 3, 4, 5]}
+ query={this.props.query}
+ renderOption={this.renderOption}
+ isFavorite={this.props.isFavorite}
+ organization={this.props.organization}
+ getFacetValueForOption={this.getFacetValueForOption}
+ highlightUnder={1}
+ header={
+ <FilterHeader name="Duplications">
+ <SortingFilter
+ property={this.property}
+ query={this.props.query}
+ isFavorite={this.props.isFavorite}
+ organization={this.props.organization}/>
+ </FilterHeader>
+ }/>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/Filter.js b/server/sonar-web/src/main/js/apps/projects/filters/Filter.js
index 7e407cb23c2..7008f2bc86f 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/Filter.js
+++ b/server/sonar-web/src/main/js/apps/projects/filters/Filter.js
@@ -26,21 +26,25 @@ import { translate } from '../../../helpers/l10n';
export default class Filter extends React.PureComponent {
static propTypes = {
- value: React.PropTypes.any,
property: React.PropTypes.string.isRequired,
- getOptions: React.PropTypes.func.isRequired,
+ options: React.PropTypes.array.isRequired,
+ query: React.PropTypes.object.isRequired,
+ renderOption: React.PropTypes.func.isRequired,
+
+ value: React.PropTypes.any,
+ facet: React.PropTypes.object,
maxFacetValue: React.PropTypes.number,
optionClassName: React.PropTypes.string,
-
- renderName: React.PropTypes.func.isRequired,
- renderOption: React.PropTypes.func.isRequired,
- renderFooter: React.PropTypes.func,
- renderSort: React.PropTypes.func,
+ isFavorite: React.PropTypes.bool,
+ organization: React.PropTypes.object,
getFacetValueForOption: React.PropTypes.func,
halfWidth: React.PropTypes.bool,
- highlightUnder: React.PropTypes.number
+ highlightUnder: React.PropTypes.number,
+
+ header: React.PropTypes.object,
+ footer: React.PropTypes.object
};
static defaultProps = {
@@ -74,20 +78,10 @@ export default class Filter extends React.PureComponent {
return getFilterUrl(this.props, { [property]: urlOption });
}
- renderHeader () {
- return (
- <div className="search-navigator-facet-header projects-facet-header">
- {this.props.renderName()}
- {this.props.renderSort && this.props.renderSort()}
- </div>
- );
- }
-
renderOptionBar (facetValue) {
if (facetValue == null || !this.props.maxFacetValue) {
return null;
}
-
return (
<div className="projects-facet-bar">
<div
@@ -133,7 +127,7 @@ export default class Filter extends React.PureComponent {
}
renderOptions () {
- const options = this.props.getOptions(this.props.facet);
+ const { options } = this.props;
if (options && options.length > 0) {
return (
<div className="search-navigator-facet-list">
@@ -149,23 +143,12 @@ export default class Filter extends React.PureComponent {
}
}
- renderFooter () {
- if (!this.props.renderFooter) {
- return null;
- }
- return (
- <div className="search-navigator-facet-footer projects-facet-footer">
- {this.props.renderFooter()}
- </div>
- );
- }
-
render () {
return (
<div className="search-navigator-facet-box" data-key={this.props.property}>
- {this.renderHeader()}
+ {this.props.header}
{this.renderOptions()}
- {this.renderFooter()}
+ {this.props.footer}
</div>
);
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/FilterContainer.js b/server/sonar-web/src/main/js/apps/projects/filters/FilterContainer.js
new file mode 100644
index 00000000000..e70b9b5904e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/filters/FilterContainer.js
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { connect } from 'react-redux';
+import Filter from './Filter';
+import {
+ getProjectsAppFacetByProperty,
+ getProjectsAppMaxFacetValue
+} from '../../../store/rootReducer';
+
+const mapStateToProps = (state, ownProps) => ({
+ value: ownProps.query[ownProps.property],
+ facet: getProjectsAppFacetByProperty(state, ownProps.property),
+ maxFacetValue: getProjectsAppMaxFacetValue(state)
+});
+export default connect(mapStateToProps)(Filter);
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/FilterHeader.js b/server/sonar-web/src/main/js/apps/projects/filters/FilterHeader.js
new file mode 100644
index 00000000000..ee98c26230b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/filters/FilterHeader.js
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+//@flow
+import React from 'react';
+
+type Props = {
+ name: string,
+ children?: {}
+};
+
+export default class FilterHeader extends React.PureComponent {
+ props: Props;
+
+ render () {
+ return (
+ <div className="search-navigator-facet-header projects-facet-header">
+ {this.props.name}
+ {this.props.children}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.js
index c39cf006769..b73200036cc 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.js
+++ b/server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.js
@@ -18,7 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import { FilterContainer } from './containers';
+import FilterContainer from './FilterContainer';
+import FilterHeader from './FilterHeader';
import SortingFilter from './SortingFilter';
import Rating from '../../../components/ui/Rating';
@@ -31,46 +32,39 @@ export default class IssuesFilter extends React.PureComponent {
organization: React.PropTypes.object
};
- renderOption = (option, selected) => {
+ renderOption (option, selected) {
return (
<span>
<Rating value={option} small={true} muted={!selected}/>
{option > 1 && option < 5 && <span className="note spacer-left">and worse</span>}
</span>
);
- };
-
- renderSort = () => {
- return (
- <SortingFilter
- property={this.props.property}
- query={this.props.query}
- isFavorite={this.props.isFavorite}
- organization={this.props.organization}/>
- );
- };
+ }
- getFacetValueForOption = (facet, option) => {
+ getFacetValueForOption (facet, option) {
return facet[option];
- };
-
- getOptions = () => [1, 2, 3, 4, 5];
-
- renderName = () => this.props.name;
+ }
render () {
return (
<FilterContainer
property={this.props.property}
- getOptions={this.getOptions}
- renderName={this.renderName}
- renderOption={this.renderOption}
- renderSort={this.renderSort}
- highlightUnder={1}
- getFacetValueForOption={this.getFacetValueForOption}
+ options={[1, 2, 3, 4, 5]}
query={this.props.query}
+ renderOption={this.renderOption}
isFavorite={this.props.isFavorite}
- organization={this.props.organization}/>
+ organization={this.props.organization}
+ getFacetValueForOption={this.getFacetValueForOption}
+ highlightUnder={1}
+ header={
+ <FilterHeader name={this.props.name}>
+ <SortingFilter
+ property={this.props.property}
+ query={this.props.query}
+ isFavorite={this.props.isFavorite}
+ organization={this.props.organization}/>
+ </FilterHeader>
+ }/>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilter.js
deleted file mode 100644
index d7cdb3947c3..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilter.js
+++ /dev/null
@@ -1,73 +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 React from 'react';
-import sortBy from 'lodash/sortBy';
-import {
- FilterContainer,
- LanguageFilterFooterContainer,
- LanguageFilterOptionContainer
-} from './containers';
-
-export default class LanguageFilter extends React.PureComponent {
- static propTypes = {
- query: React.PropTypes.object.isRequired,
- isFavorite: React.PropTypes.bool,
- organization: React.PropTypes.object
- };
-
- property = 'languages';
-
- renderOption = option => {
- return <LanguageFilterOptionContainer languageKey={option}/>;
- };
-
- getSortedOptions (facet) {
- return sortBy(Object.keys(facet), [option => -facet[option]]);
- }
-
- renderFooter = () => (
- <LanguageFilterFooterContainer
- property={this.property}
- query={this.props.query}
- isFavorite={this.props.isFavorite}
- organization={this.props.organization}/>
- );
-
- getFacetValueForOption = (facet, option) => facet[option];
-
- getOptions = facet => facet ? this.getSortedOptions(facet) : [];
-
- renderName = () => 'Languages';
-
- render () {
- return (
- <FilterContainer
- property={this.property}
- getOptions={this.getOptions}
- renderName={this.renderName}
- renderOption={this.renderOption}
- renderFooter={this.renderFooter}
- getFacetValueForOption={this.getFacetValueForOption}
- query={this.props.query}
- isFavorite={this.props.isFavorite}
- organization={this.props.organization}/>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.js
new file mode 100644
index 00000000000..7aa13b9fad5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.js
@@ -0,0 +1,92 @@
+/*
+ * 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 difference from 'lodash/difference';
+import sortBy from 'lodash/sortBy';
+import Filter from './Filter';
+import FilterHeader from './FilterHeader';
+import SearchableFilterFooter from './SearchableFilterFooter';
+import SearchableFilterOption from './SearchableFilterOption';
+import { getLanguageByKey } from '../../../store/languages/reducer';
+
+type Props = {
+ query: {},
+ languages: {},
+ router: { push: ({ pathname: string, query?: {} }) => void },
+ value?: Array<string>,
+ facet?: {},
+ isFavorite?: boolean,
+ organization?: {},
+ maxFacetValue?: number
+};
+
+export default class LanguagesFilter extends React.PureComponent {
+ getSearchOptions: () => [{ label: string, value: string }];
+ props: Props;
+ property = 'languages';
+
+ renderOption = (option: string) => (
+ <SearchableFilterOption
+ optionKey={option}
+ option={getLanguageByKey(this.props.languages, option)}/>
+ );
+
+ getSearchOptions (facet: {}, languages: {}) {
+ let languageKeys = Object.keys(languages);
+ if (facet) {
+ languageKeys = difference(languageKeys, Object.keys(facet));
+ }
+ return languageKeys.map(key => ({ label: languages[key].name, value: key }));
+ }
+
+ getSortedOptions (facet: {} = {}) {
+ return sortBy(Object.keys(facet), [option => -facet[option], option => option]);
+ }
+
+ getFacetValueForOption = (facet: {} = {}, option: string) => facet[option];
+
+ render () {
+ return (
+ <Filter
+ property={this.property}
+ options={this.getSortedOptions(this.props.facet)}
+ query={this.props.query}
+ renderOption={this.renderOption}
+ value={this.props.value}
+ facet={this.props.facet}
+ maxFacetValue={this.props.maxFacetValue}
+ isFavorite={this.props.isFavorite}
+ organization={this.props.organization}
+ getFacetValueForOption={this.getFacetValueForOption}
+ highlightUnder={1}
+ header={<FilterHeader name="Languages"/>}
+ footer={
+ <SearchableFilterFooter
+ property={this.property}
+ query={this.props.query}
+ options={this.getSearchOptions(this.props.facet, this.props.languages)}
+ isFavorite={this.props.isFavorite}
+ organization={this.props.organization}
+ router={this.props.router}/>
+ }/>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.js b/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.js
new file mode 100644
index 00000000000..8143103d283
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.js
@@ -0,0 +1,35 @@
+/*
+ * 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 { connect } from 'react-redux';
+import { withRouter } from 'react-router';
+import LanguagesFilter from './LanguagesFilter';
+import {
+ getProjectsAppFacetByProperty,
+ getProjectsAppMaxFacetValue,
+ getLanguages
+} from '../../../store/rootReducer';
+
+const mapStateToProps = (state, ownProps) => ({
+ languages: getLanguages(state),
+ value: ownProps.query['languages'],
+ facet: getProjectsAppFacetByProperty(state, 'languages'),
+ maxFacetValue: getProjectsAppMaxFacetValue(state)
+});
+export default connect(mapStateToProps)(withRouter(LanguagesFilter));
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.js
index afd7bdff881..e05067f6684 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.js
+++ b/server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.js
@@ -22,11 +22,6 @@ import IssuesFilter from './IssuesFilter';
export default class MaintainabilityFilter extends React.Component {
render () {
- return (
- <IssuesFilter
- {...this.props}
- name="Maintainability"
- property="maintainability"/>
- );
+ return <IssuesFilter {...this.props} name="Maintainability" property="maintainability"/>;
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js
index abfff98dd47..fc190415936 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js
+++ b/server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js
@@ -18,33 +18,39 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import { FilterContainer } from './containers';
+import FilterContainer from './FilterContainer';
+import FilterHeader from './FilterHeader';
import Level from '../../../components/ui/Level';
export default class QualityGateFilter extends React.PureComponent {
- renderOption = (option, selected) => {
- return <Level level={option} small={true} muted={!selected}/>;
- };
-
- getFacetValueForOption = (facet, option) => {
- return facet[option];
+ static propTypes = {
+ query: React.PropTypes.object.isRequired,
+ isFavorite: React.PropTypes.bool,
+ organization: React.PropTypes.object
};
- getOptions = () => ['OK', 'WARN', 'ERROR'];
+ renderOption (option, selected) {
+ return <Level level={option} small={true} muted={!selected}/>;
+ }
- renderName = () => 'Quality Gate';
+ getFacetValueForOption (facet, option) {
+ return facet[option];
+ }
render () {
return (
<FilterContainer
property="gate"
- getOptions={this.getOptions}
- renderName={this.renderName}
- renderOption={this.renderOption}
- getFacetValueForOption={this.getFacetValueForOption}
+ options={['OK', 'WARN', 'ERROR']}
query={this.props.query}
+ renderOption={this.renderOption}
isFavorite={this.props.isFavorite}
- organization={this.props.organization}/>
+ organization={this.props.organization}
+ getFacetValueForOption={this.getFacetValueForOption}
+ highlightUnder={1}
+ header={
+ <FilterHeader name="Quality Gate"/>
+ }/>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.js
index 0db72f30352..1ae8889363e 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.js
+++ b/server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.js
@@ -22,11 +22,6 @@ import IssuesFilter from './IssuesFilter';
export default class ReliabilityFilter extends React.Component {
render () {
- return (
- <IssuesFilter
- {...this.props}
- name="Reliability"
- property="reliability"/>
- );
+ return <IssuesFilter {...this.props} name="Reliability" property="reliability"/>;
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SearchFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/SearchFilter.js
index 9f414926c4d..50c83dd967e 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/SearchFilter.js
+++ b/server/sonar-web/src/main/js/apps/projects/filters/SearchFilter.js
@@ -43,7 +43,10 @@ export default class SearchFilter extends React.PureComponent {
}
componentWillReceiveProps (nextProps: Props) {
- if (this.props.query.search === this.state.userQuery && nextProps.query.search !== this.props.query.search) {
+ if (
+ this.props.query.search === this.state.userQuery &&
+ nextProps.query.search !== this.props.query.search
+ ) {
this.setState({
userQuery: nextProps.query.search || ''
});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.js b/server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.js
index 696bf7acc12..be8b92a2bfb 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.js
+++ b/server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.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.
*/
+//@flow
import React from 'react';
import { withRouter } from 'react-router';
import debounce from 'lodash/debounce';
@@ -24,8 +25,8 @@ import { getFilterUrl } from './utils';
import SearchFilter from './SearchFilter';
type Props = {
- query: {},
- router: { push: (string) => void },
+ query: { search?: string },
+ router: { push: ({ pathname: string }) => void },
isFavorite?: boolean,
organization?: {}
};
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterFooter.js b/server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterFooter.js
index 12b8058bbea..1d23bbced21 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterFooter.js
+++ b/server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterFooter.js
@@ -17,45 +17,47 @@
* 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 difference from 'lodash/difference';
import { getFilterUrl } from './utils';
import { translate } from '../../../helpers/l10n';
-export default class LanguageFilterFooter extends React.Component {
- static propTypes = {
- property: React.PropTypes.string.isRequired,
- query: React.PropTypes.object.isRequired,
- languages: React.PropTypes.object,
- value: React.PropTypes.any,
- facet: React.PropTypes.object
- }
+type Props = {
+ property: string,
+ query: {},
+ options: [{ label: string, value: string }],
+ router: { push: ({ pathname: string, query?: {}}) => void },
+ onInputChange?: (string) => void,
+ onOpen?: (void) => void,
+ isLoading?: boolean,
+ isFavorite?: boolean,
+ organization?: {}
+};
+
+export default class SearchableFilterFooter extends React.PureComponent {
+ props: Props;
- handleLanguageChange = ({ value }) => {
- const urlOptions = (this.props.value || []).concat(value).join(',');
+ handleOptionChange: ({ value: string }) => void = ({ value }) => {
+ const urlOptions = (this.props.query[this.props.property] || []).concat(value).join(',');
const path = getFilterUrl(this.props, { [this.props.property]: urlOptions });
this.props.router.push(path);
- }
-
- getOptions () {
- const { languages, facet } = this.props;
- let options = Object.keys(languages);
- if (facet) {
- options = difference(options, Object.keys(facet));
- }
- return options.map(key => ({ label: languages[key].name, value: key }));
- }
+ };
render () {
return (
- <Select
- onChange={this.handleLanguageChange}
+ <div className="search-navigator-facet-footer projects-facet-footer">
+ <Select
+ onChange={this.handleOptionChange}
className="input-super-large"
- options={this.getOptions()}
placeholder={translate('search_verb')}
clearable={false}
- searchable={true}/>
+ searchable={true}
+ onInputChange={this.props.onInputChange}
+ onOpen={this.props.onOpen}
+ isLoading={this.props.isLoading}
+ options={this.props.options}/>
+ </div>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterOption.js b/server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterOption.js
index ef940b2cf45..af658f6834d 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterOption.js
+++ b/server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterOption.js
@@ -20,16 +20,14 @@
import React from 'react';
import { translate } from '../../../helpers/l10n';
-export default class LanguageFilterOption extends React.Component {
+export default class SearchableFilterOption extends React.PureComponent {
static propTypes = {
- languageKey: React.PropTypes.string.isRequired,
- language: React.PropTypes.object
- }
+ optionKey: React.PropTypes.string.isRequired,
+ option: React.PropTypes.object
+ };
render () {
- const languageName = this.props.language ? this.props.language.name : this.props.languageKey;
- return (
- <span>{this.props.languageKey !== '<null>' ? languageName : translate('unknown')}</span>
- );
+ const optionName = this.props.option ? this.props.option.name : this.props.optionKey;
+ return <span>{this.props.optionKey !== '<null>' ? optionName : translate('unknown')}</span>;
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.js
index f84b730cd92..61120c1a245 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.js
+++ b/server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.js
@@ -22,11 +22,6 @@ import IssuesFilter from './IssuesFilter';
export default class SecurityFilter extends React.Component {
render () {
- return (
- <IssuesFilter
- {...this.props}
- name="Security"
- property="security"/>
- );
+ return <IssuesFilter {...this.props} name="Security" property="security"/>;
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js
index 9b920f50c22..65e61a2c4fb 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js
+++ b/server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js
@@ -18,7 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import { FilterContainer } from './containers';
+import FilterContainer from './FilterContainer';
+import FilterHeader from './FilterHeader';
import SortingFilter from './SortingFilter';
import SizeRating from '../../../components/ui/SizeRating';
import { translate } from '../../../helpers/l10n';
@@ -33,7 +34,7 @@ export default class SizeFilter extends React.PureComponent {
property = 'size';
- renderOption = (option, selected) => {
+ renderOption (option, selected) {
return (
<span>
<SizeRating value={getSizeRatingAverageValue(option)} small={true} muted={!selected}/>
@@ -42,7 +43,7 @@ export default class SizeFilter extends React.PureComponent {
</span>
</span>
);
- };
+ }
renderSort = () => {
return (
@@ -56,7 +57,7 @@ export default class SizeFilter extends React.PureComponent {
);
};
- getFacetValueForOption = (facet, option) => {
+ getFacetValueForOption (facet, option) {
const map = [
'*-1000.0',
'1000.0-10000.0',
@@ -65,25 +66,30 @@ export default class SizeFilter extends React.PureComponent {
'500000.0-*'
];
return facet[map[option - 1]];
- };
-
- getOptions = () => [1, 2, 3, 4, 5];
-
- renderName = () => 'Size';
+ }
render () {
return (
<FilterContainer
property={this.property}
- getOptions={this.getOptions}
- renderName={this.renderName}
- renderOption={this.renderOption}
- renderSort={this.renderSort}
- highlightUnder={1}
- getFacetValueForOption={this.getFacetValueForOption}
+ options={[1, 2, 3, 4, 5]}
query={this.props.query}
+ renderOption={this.renderOption}
isFavorite={this.props.isFavorite}
- organization={this.props.organization}/>
+ organization={this.props.organization}
+ getFacetValueForOption={this.getFacetValueForOption}
+ highlightUnder={1}
+ header={
+ <FilterHeader name="Size">
+ <SortingFilter
+ property={this.property}
+ query={this.props.query}
+ isFavorite={this.props.isFavorite}
+ organization={this.props.organization}
+ leftText={translate('biggest')}
+ rightText={translate('smallest')}/>
+ </FilterHeader>
+ }/>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SortingFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/SortingFilter.js
index 7bf9cd829f8..80030468909 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/SortingFilter.js
+++ b/server/sonar-web/src/main/js/apps/projects/filters/SortingFilter.js
@@ -32,8 +32,7 @@ export default class SortingFilter extends React.PureComponent {
sortDesc: React.PropTypes.oneOf(['left', 'right']),
leftText: React.PropTypes.string,
rightText: React.PropTypes.string
- }
-
+ };
static defaultProps = {
sortDesc: 'left',
leftText: translate('worst'),
@@ -72,19 +71,23 @@ export default class SortingFilter extends React.PureComponent {
const { leftText, rightText } = this.props;
return (
- <div className="projects-facet-sort">
- <span>{translate('projects.sort_list')}</span>
- <div className="spacer-left button-group">
- <Link
- onClick={this.blurLink}
- className={this.getLinkClass('left')}
- to={this.getLinkPath('left')}>{leftText}</Link>
- <Link
- onClick={this.blurLink}
- className={this.getLinkClass('right')}
- to={this.getLinkPath('right')}>{rightText}</Link>
- </div>
+ <div className="projects-facet-sort">
+ <span>{translate('projects.sort_list')}</span>
+ <div className="spacer-left button-group">
+ <Link
+ onClick={this.blurLink}
+ className={this.getLinkClass('left')}
+ to={this.getLinkPath('left')}>
+ {leftText}
+ </Link>
+ <Link
+ onClick={this.blurLink}
+ className={this.getLinkClass('right')}
+ to={this.getLinkPath('right')}>
+ {rightText}
+ </Link>
</div>
+ </div>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/TagsFilter.js b/server/sonar-web/src/main/js/apps/projects/filters/TagsFilter.js
new file mode 100644
index 00000000000..50463304e54
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/filters/TagsFilter.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 debounce from 'lodash/debounce';
+import difference from 'lodash/difference';
+import sortBy from 'lodash/sortBy';
+import Filter from './Filter';
+import FilterHeader from './FilterHeader';
+import SearchableFilterFooter from './SearchableFilterFooter';
+import SearchableFilterOption from './SearchableFilterOption';
+import { searchProjectTags } from '../../../api/components';
+
+type Props = {
+ query: {},
+ router: { push: ({ pathname: string, query?: {} }) => void },
+ value?: Array<string>,
+ facet?: {},
+ isFavorite?: boolean,
+ organization?: {},
+ maxFacetValue?: number,
+};
+
+type State = {
+ isLoading: boolean,
+ search: string,
+ tags: Array<string>
+};
+
+const PAGE_SIZE = 20;
+
+export default class TagsFilter extends React.PureComponent {
+ getSearchOptions: () => [{ label: string, value: string }];
+ props: Props;
+ state: State = {
+ isLoading: false,
+ search: '',
+ tags: []
+ };
+ property = 'tags';
+
+ constructor (props: Props) {
+ super(props);
+ this.handleSearch = debounce(this.handleSearch.bind(this), 250);
+ }
+
+ renderOption = (option: string) => <SearchableFilterOption optionKey={option}/>;
+
+ getSearchOptions (facet: {}, tags: Array<string>) {
+ let tagsCopy = [...tags];
+ if (facet) {
+ tagsCopy = difference(tagsCopy, Object.keys(facet));
+ }
+ return tagsCopy.map(tag => ({ label: tag, value: tag }));
+ }
+
+ handleSearch = (search?: string) => {
+ if (search !== this.state.search) {
+ search = search || '';
+ this.setState({ search, isLoading: true });
+ searchProjectTags({ q: search, ps: PAGE_SIZE }).then(result => {
+ this.setState({ isLoading: false, tags: result.tags });
+ });
+ }
+ };
+
+ getSortedOptions (facet: {} = {}) {
+ return sortBy(Object.keys(facet), [option => -facet[option], option => option]);
+ }
+
+ getFacetValueForOption = (facet: {}, option: string) => facet[option];
+
+ render () {
+ return (
+ <Filter
+ property={this.property}
+ options={this.getSortedOptions(this.props.facet)}
+ query={this.props.query}
+ renderOption={this.renderOption}
+ value={this.props.value}
+ facet={this.props.facet}
+ maxFacetValue={this.props.maxFacetValue}
+ isFavorite={this.props.isFavorite}
+ organization={this.props.organization}
+ getFacetValueForOption={this.getFacetValueForOption}
+ highlightUnder={1}
+ header={<FilterHeader name="Tags"/>}
+ footer={
+ <SearchableFilterFooter
+ property={this.property}
+ query={this.props.query}
+ options={this.getSearchOptions(this.props.facet, this.state.tags)}
+ isLoading={this.state.isLoading}
+ onOpen={this.handleSearch}
+ onInputChange={this.handleSearch}
+ isFavorite={this.props.isFavorite}
+ organization={this.props.organization}
+ router={this.props.router}/>
+ }/>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/TagsFilterContainer.js b/server/sonar-web/src/main/js/apps/projects/filters/TagsFilterContainer.js
new file mode 100644
index 00000000000..8c841346a42
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/filters/TagsFilterContainer.js
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { connect } from 'react-redux';
+import { withRouter } from 'react-router';
+import TagsFilter from './TagsFilter';
+import {
+ getProjectsAppFacetByProperty,
+ getProjectsAppMaxFacetValue
+} from '../../../store/rootReducer';
+
+const mapStateToProps = (state, ownProps) => ({
+ value: ownProps.query['tags'],
+ facet: getProjectsAppFacetByProperty(state, 'tags'),
+ maxFacetValue: getProjectsAppMaxFacetValue(state)
+});
+export default connect(mapStateToProps)(withRouter(TagsFilter));
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguageFilterFooter-test.js b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.js
index 1c90902fe30..c508af7616a 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguageFilterFooter-test.js
+++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.js
@@ -19,7 +19,7 @@
*/
import React from 'react';
import { shallow } from 'enzyme';
-import LanguageFilterFooter from '../LanguageFilterFooter';
+import LanguagesFilter from '../LanguagesFilter';
const languages = {
java: {
@@ -47,16 +47,30 @@ const languages = {
name: 'Python'
}
};
-const facet = { java: 39, cs: 4, js: 1 };
+const languagesFacet = { java: 39, cs: 4, js: 1 };
+const fakeRouter = { push: () => {} };
it('should render the languages without the ones in the facet', () => {
const wrapper = shallow(
- <LanguageFilterFooter
- property="foo"
+ <LanguagesFilter
query={{ languages: null }}
- facet={facet}
- languages={languages}/>
+ languages={languages}
+ router={fakeRouter}
+ facet={languagesFacet}/>
);
expect(wrapper).toMatchSnapshot();
- expect(wrapper.find('Select').props().options.length).toBe(3);
+});
+
+it('should render the languages facet with the selected languages', () => {
+ const wrapper = shallow(
+ <LanguagesFilter
+ query={{ languages: ['java', 'cs'] }}
+ value={['java', 'cs']}
+ languages={languages}
+ router={fakeRouter}
+ facet={languagesFacet}
+ isFavorite={true}/>
+ );
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('Filter').shallow()).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchableFilterFooter-test.js b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchableFilterFooter-test.js
new file mode 100644
index 00000000000..e62d8f89299
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchableFilterFooter-test.js
@@ -0,0 +1,58 @@
+/*
+ * 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 SearchableFilterFooter from '../SearchableFilterFooter';
+
+const languageOptions = [
+ { label: 'Flex', value: 'flex' },
+ { label: 'PHP', value: 'php' },
+ { label: 'Python', value: 'py' }
+];
+const tagOptions = [
+ { label: 'lang', value: 'lang' },
+ { label: 'sonar', value: 'sonar' },
+ { label: 'csharp', value: 'csharp' }
+];
+const fakeRouter = { push: () => {} };
+
+it('should render the languages without the ones in the facet', () => {
+ const wrapper = shallow(
+ <SearchableFilterFooter
+ property="languages"
+ query={{ languages: null }}
+ options={languageOptions}
+ router={fakeRouter}/>
+ );
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('Select').props().options.length).toBe(3);
+});
+
+it('should render the tags without the ones in the facet', () => {
+ const wrapper = shallow(
+ <SearchableFilterFooter
+ property="tags"
+ query={{ tags: ['java'] }}
+ options={tagOptions}
+ isFavorite={true}/>
+ );
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('Select').props().options.length).toBe(3);
+});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/TagsFilter-test.js b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/TagsFilter-test.js
new file mode 100644
index 00000000000..26f8eda70d1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/TagsFilter-test.js
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import TagsFilter from '../TagsFilter';
+
+const tags = ['lang', 'sonar', 'csharp', 'dotnet', 'it', 'net'];
+const tagsFacet = { lang: 4, sonar: 3, csharp: 1 };
+const fakeRouter = { push: () => {} };
+
+it('should render the tags without the ones in the facet', () => {
+ const wrapper = shallow(
+ <TagsFilter
+ query={{ tags: null }}
+ router={fakeRouter}
+ facet={tagsFacet}/>
+ );
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setState({ tags });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should render the tags facet with the selected tags', () => {
+ const wrapper = shallow(
+ <TagsFilter
+ query={{ tags: ['lang', 'sonar'] }}
+ value={['lang', 'sonar']}
+ router={fakeRouter}
+ facet={tagsFacet}
+ isFavorite={true}/>
+ );
+ expect(wrapper).toMatchSnapshot();
+ expect(wrapper.find('Filter').shallow()).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/LanguageFilterFooter-test.js.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/LanguageFilterFooter-test.js.snap
deleted file mode 100644
index 22d352f4ca5..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/LanguageFilterFooter-test.js.snap
+++ /dev/null
@@ -1,58 +0,0 @@
-exports[`test should render the languages without the ones in the facet 1`] = `
-<Select
- addLabelText="Add \"{label}\"?"
- arrowRenderer={[Function]}
- autosize={true}
- backspaceRemoves={true}
- backspaceToRemoveMessage="Press backspace to remove {label}"
- className="input-super-large"
- clearAllText="Clear all"
- clearValueText="Clear value"
- clearable={false}
- delimiter=","
- disabled={false}
- escapeClearsValue={true}
- 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="No results found"
- onBlurResetsInput={true}
- onChange={[Function]}
- onCloseResetsInput={true}
- openAfterFocus={false}
- optionComponent={[Function]}
- options={
- Array [
- Object {
- "label": "Flex",
- "value": "flex",
- },
- Object {
- "label": "PHP",
- "value": "php",
- },
- Object {
- "label": "Python",
- "value": "py",
- },
- ]
- }
- 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/projects/filters/__tests__/__snapshots__/LanguagesFilter-test.js.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/LanguagesFilter-test.js.snap
new file mode 100644
index 00000000000..b37704bad80
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/LanguagesFilter-test.js.snap
@@ -0,0 +1,268 @@
+exports[`test should render the languages facet with the selected languages 1`] = `
+<Filter
+ facet={
+ Object {
+ "cs": 4,
+ "java": 39,
+ "js": 1,
+ }
+ }
+ footer={
+ <SearchableFilterFooter
+ isFavorite={true}
+ options={
+ Array [
+ Object {
+ "label": "Flex",
+ "value": "flex",
+ },
+ Object {
+ "label": "PHP",
+ "value": "php",
+ },
+ Object {
+ "label": "Python",
+ "value": "py",
+ },
+ ]
+ }
+ property="languages"
+ query={
+ Object {
+ "languages": Array [
+ "java",
+ "cs",
+ ],
+ }
+ }
+ router={
+ Object {
+ "push": [Function],
+ }
+ } />
+ }
+ getFacetValueForOption={[Function]}
+ halfWidth={false}
+ header={
+ <FilterHeader
+ name="Languages" />
+ }
+ highlightUnder={1}
+ isFavorite={true}
+ options={
+ Array [
+ "java",
+ "cs",
+ "js",
+ ]
+ }
+ property="languages"
+ query={
+ Object {
+ "languages": Array [
+ "java",
+ "cs",
+ ],
+ }
+ }
+ renderOption={[Function]}
+ value={
+ Array [
+ "java",
+ "cs",
+ ]
+ } />
+`;
+
+exports[`test should render the languages facet with the selected languages 2`] = `
+<div
+ className="search-navigator-facet-box"
+ data-key="languages">
+ <FilterHeader
+ name="Languages" />
+ <div
+ className="search-navigator-facet-list">
+ <Link
+ className="facet search-navigator-facet projects-facet active"
+ data-key="java"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/projects/favorite",
+ "query": Object {
+ "languages": "cs",
+ },
+ }
+ }>
+ <span
+ className="facet-name">
+ <SearchableFilterOption
+ option={
+ Object {
+ "key": "java",
+ "name": "Java",
+ }
+ }
+ optionKey="java" />
+ </span>
+ <span
+ className="facet-stat">
+ 39
+ </span>
+ </Link>
+ <Link
+ className="facet search-navigator-facet projects-facet active"
+ data-key="cs"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/projects/favorite",
+ "query": Object {
+ "languages": "java",
+ },
+ }
+ }>
+ <span
+ className="facet-name">
+ <SearchableFilterOption
+ option={
+ Object {
+ "key": "cs",
+ "name": "C#",
+ }
+ }
+ optionKey="cs" />
+ </span>
+ <span
+ className="facet-stat">
+ 4
+ </span>
+ </Link>
+ <Link
+ className="facet search-navigator-facet projects-facet"
+ data-key="js"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/projects/favorite",
+ "query": Object {
+ "languages": "java,cs,js",
+ },
+ }
+ }>
+ <span
+ className="facet-name">
+ <SearchableFilterOption
+ option={
+ Object {
+ "key": "js",
+ "name": "JavaScript",
+ }
+ }
+ optionKey="js" />
+ </span>
+ <span
+ className="facet-stat">
+ 1
+ </span>
+ </Link>
+ </div>
+ <SearchableFilterFooter
+ isFavorite={true}
+ options={
+ Array [
+ Object {
+ "label": "Flex",
+ "value": "flex",
+ },
+ Object {
+ "label": "PHP",
+ "value": "php",
+ },
+ Object {
+ "label": "Python",
+ "value": "py",
+ },
+ ]
+ }
+ property="languages"
+ query={
+ Object {
+ "languages": Array [
+ "java",
+ "cs",
+ ],
+ }
+ }
+ router={
+ Object {
+ "push": [Function],
+ }
+ } />
+</div>
+`;
+
+exports[`test should render the languages without the ones in the facet 1`] = `
+<Filter
+ facet={
+ Object {
+ "cs": 4,
+ "java": 39,
+ "js": 1,
+ }
+ }
+ footer={
+ <SearchableFilterFooter
+ options={
+ Array [
+ Object {
+ "label": "Flex",
+ "value": "flex",
+ },
+ Object {
+ "label": "PHP",
+ "value": "php",
+ },
+ Object {
+ "label": "Python",
+ "value": "py",
+ },
+ ]
+ }
+ property="languages"
+ query={
+ Object {
+ "languages": null,
+ }
+ }
+ router={
+ Object {
+ "push": [Function],
+ }
+ } />
+ }
+ getFacetValueForOption={[Function]}
+ halfWidth={false}
+ header={
+ <FilterHeader
+ name="Languages" />
+ }
+ highlightUnder={1}
+ options={
+ Array [
+ "java",
+ "cs",
+ "js",
+ ]
+ }
+ property="languages"
+ query={
+ Object {
+ "languages": null,
+ }
+ }
+ renderOption={[Function]} />
+`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchableFilterFooter-test.js.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchableFilterFooter-test.js.snap
new file mode 100644
index 00000000000..4fc96d9d2e6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchableFilterFooter-test.js.snap
@@ -0,0 +1,123 @@
+exports[`test should render the languages without the ones in the facet 1`] = `
+<div
+ className="search-navigator-facet-footer projects-facet-footer">
+ <Select
+ addLabelText="Add \"{label}\"?"
+ arrowRenderer={[Function]}
+ autosize={true}
+ backspaceRemoves={true}
+ backspaceToRemoveMessage="Press backspace to remove {label}"
+ className="input-super-large"
+ clearAllText="Clear all"
+ clearValueText="Clear value"
+ clearable={false}
+ delimiter=","
+ disabled={false}
+ escapeClearsValue={true}
+ 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="No results found"
+ onBlurResetsInput={true}
+ onChange={[Function]}
+ onCloseResetsInput={true}
+ openAfterFocus={false}
+ optionComponent={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "Flex",
+ "value": "flex",
+ },
+ Object {
+ "label": "PHP",
+ "value": "php",
+ },
+ Object {
+ "label": "Python",
+ "value": "py",
+ },
+ ]
+ }
+ pageSize={5}
+ placeholder="search_verb"
+ required={false}
+ scrollMenuIntoView={true}
+ searchable={true}
+ simpleValue={false}
+ tabSelectsValue={true}
+ valueComponent={[Function]}
+ valueKey="value" />
+</div>
+`;
+
+exports[`test should render the tags without the ones in the facet 1`] = `
+<div
+ className="search-navigator-facet-footer projects-facet-footer">
+ <Select
+ addLabelText="Add \"{label}\"?"
+ arrowRenderer={[Function]}
+ autosize={true}
+ backspaceRemoves={true}
+ backspaceToRemoveMessage="Press backspace to remove {label}"
+ className="input-super-large"
+ clearAllText="Clear all"
+ clearValueText="Clear value"
+ clearable={false}
+ delimiter=","
+ disabled={false}
+ escapeClearsValue={true}
+ 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="No results found"
+ onBlurResetsInput={true}
+ onChange={[Function]}
+ onCloseResetsInput={true}
+ openAfterFocus={false}
+ optionComponent={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "lang",
+ "value": "lang",
+ },
+ Object {
+ "label": "sonar",
+ "value": "sonar",
+ },
+ Object {
+ "label": "csharp",
+ "value": "csharp",
+ },
+ ]
+ }
+ pageSize={5}
+ placeholder="search_verb"
+ required={false}
+ scrollMenuIntoView={true}
+ searchable={true}
+ simpleValue={false}
+ tabSelectsValue={true}
+ valueComponent={[Function]}
+ valueKey="value" />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/TagsFilter-test.js.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/TagsFilter-test.js.snap
new file mode 100644
index 00000000000..257acafad4c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/TagsFilter-test.js.snap
@@ -0,0 +1,279 @@
+exports[`test should render the tags facet with the selected tags 1`] = `
+<Filter
+ facet={
+ Object {
+ "csharp": 1,
+ "lang": 4,
+ "sonar": 3,
+ }
+ }
+ footer={
+ <SearchableFilterFooter
+ isFavorite={true}
+ isLoading={false}
+ onInputChange={[Function]}
+ onOpen={[Function]}
+ options={Array []}
+ property="tags"
+ query={
+ Object {
+ "tags": Array [
+ "lang",
+ "sonar",
+ ],
+ }
+ }
+ router={
+ Object {
+ "push": [Function],
+ }
+ } />
+ }
+ getFacetValueForOption={[Function]}
+ halfWidth={false}
+ header={
+ <FilterHeader
+ name="Tags" />
+ }
+ highlightUnder={1}
+ isFavorite={true}
+ options={
+ Array [
+ "lang",
+ "sonar",
+ "csharp",
+ ]
+ }
+ property="tags"
+ query={
+ Object {
+ "tags": Array [
+ "lang",
+ "sonar",
+ ],
+ }
+ }
+ renderOption={[Function]}
+ value={
+ Array [
+ "lang",
+ "sonar",
+ ]
+ } />
+`;
+
+exports[`test should render the tags facet with the selected tags 2`] = `
+<div
+ className="search-navigator-facet-box"
+ data-key="tags">
+ <FilterHeader
+ name="Tags" />
+ <div
+ className="search-navigator-facet-list">
+ <Link
+ className="facet search-navigator-facet projects-facet active"
+ data-key="lang"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/projects/favorite",
+ "query": Object {
+ "tags": "sonar",
+ },
+ }
+ }>
+ <span
+ className="facet-name">
+ <SearchableFilterOption
+ optionKey="lang" />
+ </span>
+ <span
+ className="facet-stat">
+ 4
+ </span>
+ </Link>
+ <Link
+ className="facet search-navigator-facet projects-facet active"
+ data-key="sonar"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/projects/favorite",
+ "query": Object {
+ "tags": "lang",
+ },
+ }
+ }>
+ <span
+ className="facet-name">
+ <SearchableFilterOption
+ optionKey="sonar" />
+ </span>
+ <span
+ className="facet-stat">
+ 3
+ </span>
+ </Link>
+ <Link
+ className="facet search-navigator-facet projects-facet"
+ data-key="csharp"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/projects/favorite",
+ "query": Object {
+ "tags": "lang,sonar,csharp",
+ },
+ }
+ }>
+ <span
+ className="facet-name">
+ <SearchableFilterOption
+ optionKey="csharp" />
+ </span>
+ <span
+ className="facet-stat">
+ 1
+ </span>
+ </Link>
+ </div>
+ <SearchableFilterFooter
+ isFavorite={true}
+ isLoading={false}
+ onInputChange={[Function]}
+ onOpen={[Function]}
+ options={Array []}
+ property="tags"
+ query={
+ Object {
+ "tags": Array [
+ "lang",
+ "sonar",
+ ],
+ }
+ }
+ router={
+ Object {
+ "push": [Function],
+ }
+ } />
+</div>
+`;
+
+exports[`test should render the tags without the ones in the facet 1`] = `
+<Filter
+ facet={
+ Object {
+ "csharp": 1,
+ "lang": 4,
+ "sonar": 3,
+ }
+ }
+ footer={
+ <SearchableFilterFooter
+ isLoading={false}
+ onInputChange={[Function]}
+ onOpen={[Function]}
+ options={Array []}
+ property="tags"
+ query={
+ Object {
+ "tags": null,
+ }
+ }
+ router={
+ Object {
+ "push": [Function],
+ }
+ } />
+ }
+ getFacetValueForOption={[Function]}
+ halfWidth={false}
+ header={
+ <FilterHeader
+ name="Tags" />
+ }
+ highlightUnder={1}
+ options={
+ Array [
+ "lang",
+ "sonar",
+ "csharp",
+ ]
+ }
+ property="tags"
+ query={
+ Object {
+ "tags": null,
+ }
+ }
+ renderOption={[Function]} />
+`;
+
+exports[`test should render the tags without the ones in the facet 2`] = `
+<Filter
+ facet={
+ Object {
+ "csharp": 1,
+ "lang": 4,
+ "sonar": 3,
+ }
+ }
+ footer={
+ <SearchableFilterFooter
+ isLoading={false}
+ onInputChange={[Function]}
+ onOpen={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "dotnet",
+ "value": "dotnet",
+ },
+ Object {
+ "label": "it",
+ "value": "it",
+ },
+ Object {
+ "label": "net",
+ "value": "net",
+ },
+ ]
+ }
+ property="tags"
+ query={
+ Object {
+ "tags": null,
+ }
+ }
+ router={
+ Object {
+ "push": [Function],
+ }
+ } />
+ }
+ getFacetValueForOption={[Function]}
+ halfWidth={false}
+ header={
+ <FilterHeader
+ name="Tags" />
+ }
+ highlightUnder={1}
+ options={
+ Array [
+ "lang",
+ "sonar",
+ "csharp",
+ ]
+ }
+ property="tags"
+ query={
+ Object {
+ "tags": null,
+ }
+ }
+ renderOption={[Function]} />
+`;
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/containers.js b/server/sonar-web/src/main/js/apps/projects/filters/containers.js
deleted file mode 100644
index d9e76690138..00000000000
--- a/server/sonar-web/src/main/js/apps/projects/filters/containers.js
+++ /dev/null
@@ -1,55 +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 { connect } from 'react-redux';
-import { withRouter } from 'react-router';
-import Filter from './Filter';
-import LanguageFilterFooter from './LanguageFilterFooter';
-import LanguageFilterOption from './LanguageFilterOption';
-import {
- getProjectsAppFacetByProperty,
- getProjectsAppMaxFacetValue,
- getLanguages,
- getLanguageByKey
-} from '../../../store/rootReducer';
-
-export const FilterContainer = (function () {
- const mapStateToProps = (state, ownProps) => ({
- value: ownProps.query[ownProps.property],
- facet: getProjectsAppFacetByProperty(state, ownProps.property),
- maxFacetValue: getProjectsAppMaxFacetValue(state)
- });
- return connect(mapStateToProps)(withRouter(Filter));
-})();
-
-export const LanguageFilterFooterContainer = (function () {
- const mapStateToProps = (state, ownProps) => ({
- languages: getLanguages(state),
- value: ownProps.query[ownProps.property],
- facet: getProjectsAppFacetByProperty(state, ownProps.property)
- });
- return connect(mapStateToProps)(withRouter(LanguageFilterFooter));
-})();
-
-export const LanguageFilterOptionContainer = (function () {
- const mapStateToProps = (state, ownProps) => ({
- language: getLanguageByKey(state, ownProps.languageKey)
- });
- return connect(mapStateToProps)(LanguageFilterOption);
-})();
diff --git a/server/sonar-web/src/main/js/apps/projects/store/actions.js b/server/sonar-web/src/main/js/apps/projects/store/actions.js
index ad6cb7f0377..4f1b29642ff 100644
--- a/server/sonar-web/src/main/js/apps/projects/store/actions.js
+++ b/server/sonar-web/src/main/js/apps/projects/store/actions.js
@@ -54,7 +54,8 @@ const FACETS = [
'duplicated_lines_density',
'ncloc',
'alert_status',
- 'languages'
+ 'languages',
+ 'tags'
];
const onFail = dispatch => error => {
diff --git a/server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js b/server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js
index 9523540b065..03640a8b2f4 100644
--- a/server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js
+++ b/server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js
@@ -29,8 +29,7 @@ const CUMULATIVE_FACETS = [
'maintainability',
'coverage',
'duplications',
- 'size',
- 'language'
+ 'size'
];
const REVERSED_FACETS = [
diff --git a/server/sonar-web/src/main/js/apps/projects/store/utils.js b/server/sonar-web/src/main/js/apps/projects/store/utils.js
index b9d15c5140c..ca6feab5b21 100644
--- a/server/sonar-web/src/main/js/apps/projects/store/utils.js
+++ b/server/sonar-web/src/main/js/apps/projects/store/utils.js
@@ -55,6 +55,7 @@ export const parseUrlQuery = urlQuery => ({
'duplications': getAsNumericRating(urlQuery['duplications']),
'size': getAsNumericRating(urlQuery['size']),
'languages': getAsArray(urlQuery['languages'], getAsString),
+ 'tags': getAsArray(urlQuery['tags'], getAsString),
'search': getAsString(urlQuery['search']),
'sort': getAsString(urlQuery['sort'])
});
@@ -69,6 +70,7 @@ export const mapMetricToProperty = metricKey => {
'ncloc': 'size',
'alert_status': 'gate',
'languages': 'languages',
+ 'tags': 'tags',
'query': 'search'
};
return map[metricKey];
@@ -84,6 +86,7 @@ export const mapPropertyToMetric = property => {
'size': 'ncloc',
'gate': 'alert_status',
'languages': 'languages',
+ 'tags': 'tags',
'search': 'query'
};
return map[property];
@@ -177,14 +180,16 @@ const convertToFilter = (query, isFavorite) => {
}
});
- const { languages } = query;
- if (languages != null) {
- if (!Array.isArray(languages) || languages.length < 2) {
- conditions.push(mapPropertyToMetric('languages') + ' = ' + languages);
- } else {
- conditions.push(`${mapPropertyToMetric('languages')} IN (${languages.join(', ')})`);
+ ['languages', 'tags'].forEach(property => {
+ const items = query[property];
+ if (items != null) {
+ if (!Array.isArray(items) || items.length < 2) {
+ conditions.push(mapPropertyToMetric(property) + ' = ' + items);
+ } else {
+ conditions.push(`${mapPropertyToMetric(property)} IN (${items.join(', ')})`);
+ }
}
- }
+ });
if (query['search'] != null) {
conditions.push(`${mapPropertyToMetric('search')} = "${query['search']}"`);