]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8922 Add tags facet on project page (#1790)
authorGrégoire Aubert <gregaubert@users.noreply.github.com>
Thu, 16 Mar 2017 09:37:59 +0000 (10:37 +0100)
committerGitHub <noreply@github.com>
Thu, 16 Mar 2017 09:37:59 +0000 (10:37 +0100)
* Refactor the way filters are rendered

* SONAR-8922 Add the tags facet on the projects page

* SONAR-8923 Add the tags searchbox

39 files changed:
it/it-tests/src/test/java/it/projectSearch/ProjectsPageTest.java
it/it-tests/src/test/java/pageobjects/projects/FacetItem.java
server/sonar-web/src/main/js/api/components.js
server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js
server/sonar-web/src/main/js/apps/projects/filters/CoverageFilter.js
server/sonar-web/src/main/js/apps/projects/filters/DuplicationsFilter.js
server/sonar-web/src/main/js/apps/projects/filters/Filter.js
server/sonar-web/src/main/js/apps/projects/filters/FilterContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/FilterHeader.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/IssuesFilter.js
server/sonar-web/src/main/js/apps/projects/filters/LanguageFilter.js [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterFooter.js [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterOption.js [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilter.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/LanguagesFilterContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/MaintainabilityFilter.js
server/sonar-web/src/main/js/apps/projects/filters/QualityGateFilter.js
server/sonar-web/src/main/js/apps/projects/filters/ReliabilityFilter.js
server/sonar-web/src/main/js/apps/projects/filters/SearchFilter.js
server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.js
server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterFooter.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterOption.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/SecurityFilter.js
server/sonar-web/src/main/js/apps/projects/filters/SizeFilter.js
server/sonar-web/src/main/js/apps/projects/filters/SortingFilter.js
server/sonar-web/src/main/js/apps/projects/filters/TagsFilter.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/TagsFilterContainer.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguageFilterFooter-test.js [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchableFilterFooter-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/__tests__/TagsFilter-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/LanguageFilterFooter-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/LanguagesFilter-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchableFilterFooter-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/TagsFilter-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/containers.js [deleted file]
server/sonar-web/src/main/js/apps/projects/store/actions.js
server/sonar-web/src/main/js/apps/projects/store/facetsDuck.js
server/sonar-web/src/main/js/apps/projects/store/utils.js

index 280bd54cb7d004946f2b77d6e8e7ae76a78c609a..45dceeac2c68c626e3a53f3e4610a116b1c86019 100644 (file)
@@ -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,13 +119,30 @@ 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")
       .shouldHaveValue("xoo2", "0");
   }
 
+  @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();
index 79ff84d10056c5db96545975d507e37577a5e7a2..528cb3c2505af17616960fbe193c671efbdfd8b9 100644 (file)
@@ -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;
   }
 
index 456cb90e0e5cdbdc63e05661088f937289bca08f..f0469af18ecc069afeda911e7912951a74a0f76a 100644 (file)
@@ -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,
index f3c3a21b04e79fcfa67bc42712ec823c09b2fce7..11c4fb75cefb5d4be3c25f6e7c396d3ac6d8a5f4 100644 (file)
@@ -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}/>
index 8789a3aef5e84c9297023ac6a4501e1c383a5ac5..69312bb565d5fa931fcd2ed302fba463da1046c3 100644 (file)
@@ -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>
+        }/>
     );
   }
 }
index 94e47088095d13b7e22916d2660654cfd830d334..48dbc3331cd746c2a0827ece2d1867e80f91b075 100644 (file)
@@ -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>
+          }/>
     );
   }
 }
index 7e407cb23c2324f48f9121080c39c476b816ce7a..7008f2bc86f7ff65ae45a5a338fe5986d4e1a905 100644 (file)
@@ -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 (file)
index 0000000..e70b9b5
--- /dev/null
@@ -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 (file)
index 0000000..ee98c26
--- /dev/null
@@ -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>
+    );
+  }
+}
index c39cf006769bee91419f4ceabd72a0c89aafd160..b73200036ccec5b1bc590b2d8cc908f0457820fc 100644 (file)
@@ -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 (file)
index d7cdb39..0000000
+++ /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/LanguageFilterFooter.js b/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterFooter.js
deleted file mode 100644 (file)
index 12b8058..0000000
+++ /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 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
-  }
-
-  handleLanguageChange = ({ value }) => {
-    const urlOptions = (this.props.value || []).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}
-          className="input-super-large"
-          options={this.getOptions()}
-          placeholder={translate('search_verb')}
-          clearable={false}
-          searchable={true}/>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterOption.js b/server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterOption.js
deleted file mode 100644 (file)
index ef940b2..0000000
+++ /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 React from 'react';
-import { translate } from '../../../helpers/l10n';
-
-export default class LanguageFilterOption extends React.Component {
-  static propTypes = {
-    languageKey: React.PropTypes.string.isRequired,
-    language: 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>
-    );
-  }
-}
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 (file)
index 0000000..7aa13b9
--- /dev/null
@@ -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 (file)
index 0000000..8143103
--- /dev/null
@@ -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));
index afd7bdff8818f34464337c5d98b378e37d91b6b1..e05067f668496ebe5b25a9a8204ab1ec0d2201b2 100644 (file)
@@ -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"/>;
   }
 }
index abfff98dd478629084c35ac858ebd2edb90b0a1c..fc19041593694fbdfa33833bc37a46fa7a2ad905 100644 (file)
  * 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"/>
+        }/>
     );
   }
 }
index 0db72f303528d09ee6bcf55daeebda7a7dc9de26..1ae8889363ee5ef259ad51c84281d65a756f25c2 100644 (file)
@@ -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"/>;
   }
 }
index 9f414926c4d1c3a9bc06674c8d891b0d74d8aacf..50c83dd967e562787c5cc9d169156c38900d6bdd 100644 (file)
@@ -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 || ''
       });
index 696bf7acc127b39a5d3f1f4e3961f4e324548235..be8b92a2bfbd4c10b1a8b155e84cb8f2a6c34a6b 100644 (file)
@@ -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/SearchableFilterFooter.js b/server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterFooter.js
new file mode 100644 (file)
index 0000000..1d23bbc
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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 { getFilterUrl } from './utils';
+import { translate } from '../../../helpers/l10n';
+
+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;
+
+  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);
+  };
+
+  render () {
+    return (
+      <div className="search-navigator-facet-footer projects-facet-footer">
+        <Select
+          onChange={this.handleOptionChange}
+          className="input-super-large"
+          placeholder={translate('search_verb')}
+          clearable={false}
+          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/SearchableFilterOption.js b/server/sonar-web/src/main/js/apps/projects/filters/SearchableFilterOption.js
new file mode 100644 (file)
index 0000000..af658f6
--- /dev/null
@@ -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 React from 'react';
+import { translate } from '../../../helpers/l10n';
+
+export default class SearchableFilterOption extends React.PureComponent {
+  static propTypes = {
+    optionKey: React.PropTypes.string.isRequired,
+    option: React.PropTypes.object
+  };
+
+  render () {
+    const optionName = this.props.option ? this.props.option.name : this.props.optionKey;
+    return <span>{this.props.optionKey !== '<null>' ? optionName : translate('unknown')}</span>;
+  }
+}
index f84b730cd92d5001dc8fc514fd31876941eb7975..61120c1a245434b4f010068b0ac44067c0469afd 100644 (file)
@@ -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"/>;
   }
 }
index 9b920f50c2272602f426ec3309cd37f1639ac4b3..65e61a2c4fbb04592b53e9454e581ac20a9b6749 100644 (file)
@@ -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>
+        }/>
     );
   }
 }
index 7bf9cd829f8b005d037ee5655cce485ecc0a3dda..8003046890934b6d735bd4fe997e0d87778ac1c1 100644 (file)
@@ -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 (file)
index 0000000..5046330
--- /dev/null
@@ -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 (file)
index 0000000..8c84134
--- /dev/null
@@ -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__/LanguageFilterFooter-test.js
deleted file mode 100644 (file)
index 1c90902..0000000
+++ /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 React from 'react';
-import { shallow } from 'enzyme';
-import LanguageFilterFooter from '../LanguageFilterFooter';
-
-const languages = {
-  java: {
-    key: 'java',
-    name: 'Java'
-  },
-  cs: {
-    key: 'cs',
-    name: 'C#'
-  },
-  js: {
-    key: 'js',
-    name: 'JavaScript'
-  },
-  flex: {
-    key: 'flex',
-    name: 'Flex'
-  },
-  php: {
-    key: 'php',
-    name: 'PHP'
-  },
-  py: {
-    key: 'py',
-    name: 'Python'
-  }
-};
-const facet = { java: 39, cs: 4, js: 1 };
-
-it('should render the languages without the ones in the facet', () => {
-  const wrapper = shallow(
-    <LanguageFilterFooter
-      property="foo"
-      query={{ languages: null }}
-      facet={facet}
-      languages={languages}/>
-  );
-  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__/LanguagesFilter-test.js b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/LanguagesFilter-test.js
new file mode 100644 (file)
index 0000000..c508af7
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import LanguagesFilter from '../LanguagesFilter';
+
+const languages = {
+  java: {
+    key: 'java',
+    name: 'Java'
+  },
+  cs: {
+    key: 'cs',
+    name: 'C#'
+  },
+  js: {
+    key: 'js',
+    name: 'JavaScript'
+  },
+  flex: {
+    key: 'flex',
+    name: 'Flex'
+  },
+  php: {
+    key: 'php',
+    name: 'PHP'
+  },
+  py: {
+    key: 'py',
+    name: 'Python'
+  }
+};
+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(
+    <LanguagesFilter
+      query={{ languages: null }}
+      languages={languages}
+      router={fakeRouter}
+      facet={languagesFacet}/>
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+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 (file)
index 0000000..e62d8f8
--- /dev/null
@@ -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 (file)
index 0000000..26f8eda
--- /dev/null
@@ -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 (file)
index 22d352f..0000000
+++ /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 (file)
index 0000000..b37704b
--- /dev/null
@@ -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 (file)
index 0000000..4fc96d9
--- /dev/null
@@ -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 (file)
index 0000000..257acaf
--- /dev/null
@@ -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 (file)
index d9e7669..0000000
+++ /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);
-})();
index ad6cb7f037755444f077a52405ffd6e447381b22..4f1b29642ff7822a457f8c6ce43402cfd63a6585 100644 (file)
@@ -54,7 +54,8 @@ const FACETS = [
   'duplicated_lines_density',
   'ncloc',
   'alert_status',
-  'languages'
+  'languages',
+  'tags'
 ];
 
 const onFail = dispatch => error => {
index 9523540b065004f915d0437b2ae846126f9d12aa..03640a8b2f47ef8c93d25a40229dd28d5eddce3b 100644 (file)
@@ -29,8 +29,7 @@ const CUMULATIVE_FACETS = [
   'maintainability',
   'coverage',
   'duplications',
-  'size',
-  'language'
+  'size'
 ];
 
 const REVERSED_FACETS = [
index b9d15c5140cf9510d172752ff43a3acb696c1da4..ca6feab5b2194f43ddd45c9e44fe138876dbcb85 100644 (file)
@@ -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']}"`);