]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8877 Projects searchbox on projects page (#1747)
authorGrégoire Aubert <gregaubert@users.noreply.github.com>
Tue, 7 Mar 2017 08:13:14 +0000 (09:13 +0100)
committerGitHub <noreply@github.com>
Tue, 7 Mar 2017 08:13:14 +0000 (09:13 +0100)
server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js
server/sonar-web/src/main/js/apps/projects/filters/Filter.js
server/sonar-web/src/main/js/apps/projects/filters/FilterContainer.js
server/sonar-web/src/main/js/apps/projects/filters/LanguageFilterFooter.js
server/sonar-web/src/main/js/apps/projects/filters/SearchFilter.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/filters/utils.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/store/utils.js
server/sonar-web/src/main/js/apps/projects/styles.css
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 9020ef0748cca1ebdf6590cf7124deea76832cc6..b3540396a2bc4854a6883cfc8f9e4ce376fc52d2 100644 (file)
@@ -27,6 +27,7 @@ import ReliabilityFilter from '../filters/ReliabilityFilter';
 import SecurityFilter from '../filters/SecurityFilter';
 import MaintainabilityFilter from '../filters/MaintainabilityFilter';
 import LanguageFilter from '../filters/LanguageFilter';
+import SearchFilter from '../filters/SearchFilter';
 import { translate } from '../../../helpers/l10n';
 
 export default class PageSidebar extends React.Component {
@@ -56,6 +57,10 @@ export default class PageSidebar extends React.Component {
             )}
 
             <h3>{translate('filters')}</h3>
+            <SearchFilter
+                query={this.props.query}
+                isFavorite={this.props.isFavorite}
+                organization={this.props.organization}/>
           </div>
 
           <QualityGateFilter
index 5380638906550f8fa5628cbae3a26f4cb7687249..d8d6c4562f05a71843b5b2c98df68c4fd765aad8 100644 (file)
@@ -20,6 +20,7 @@
 import React from 'react';
 import classNames from 'classnames';
 import { Link } from 'react-router';
+import { getFilterUrl } from './utils';
 import { formatMeasure } from '../../../helpers/measures';
 import { translate } from '../../../helpers/l10n';
 
@@ -37,9 +38,7 @@ export default class Filter extends React.Component {
 
     getFacetValueForOption: React.PropTypes.func,
 
-    halfWidth: React.PropTypes.bool,
-
-    getFilterUrl: React.PropTypes.func.isRequired
+    halfWidth: React.PropTypes.bool
   };
 
   static defaultProps = {
@@ -64,7 +63,7 @@ export default class Filter extends React.Component {
     } else {
       urlOption = this.isSelected(option) ? null : option;
     }
-    return this.props.getFilterUrl({ [property]: urlOption });
+    return getFilterUrl(this.props, { [property]: urlOption });
   }
 
   renderHeader () {
index 728a6652db038c4c34e8c8994ffc04429ea7fb94..d690997ab40c43030200b29029cec7b6863cdade 100644 (file)
  */
 import { connect } from 'react-redux';
 import { withRouter } from 'react-router';
-import omitBy from 'lodash/omitBy';
-import isNil from 'lodash/isNil';
 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),
-  getFilterUrl: part => {
-    const basePathName = ownProps.organization ?
-        `/organizations/${ownProps.organization.key}/projects` :
-        '/projects';
-    const pathname = basePathName + (ownProps.isFavorite ? '/favorite' : '');
-    const query = omitBy({ ...ownProps.query, ...part }, isNil);
-    return { pathname, query };
-  }
+  maxFacetValue: getProjectsAppMaxFacetValue(state)
 });
 
 export default connect(mapStateToProps)(withRouter(Filter));
index 9db6ebe304f182c66f9874831cb023dd560709c3..21a09c110c08cd4073450104ab2747e8cc186f9b 100644 (file)
@@ -22,8 +22,7 @@ import { connect } from 'react-redux';
 import { withRouter } from 'react-router';
 import Select from 'react-select';
 import difference from 'lodash/difference';
-import isNil from 'lodash/isNil';
-import omitBy from 'lodash/omitBy';
+import { getFilterUrl } from './utils';
 import { getProjectsAppFacetByProperty, getLanguages } from '../../../store/rootReducer';
 import { translate } from '../../../helpers/l10n';
 
@@ -35,13 +34,12 @@ class LanguageFilterFooter extends React.Component {
     organization: React.PropTypes.object,
     languages: React.PropTypes.object,
     value: React.PropTypes.any,
-    facet: React.PropTypes.object,
-    getFilterUrl: React.PropTypes.func.isRequired
+    facet: React.PropTypes.object
   }
 
   handleLanguageChange = ({ value }) => {
     const urlOptions = (this.props.value || []).concat(value).join(',');
-    const path = this.props.getFilterUrl({ [this.props.property]: urlOptions });
+    const path = getFilterUrl(this.props, { [this.props.property]: urlOptions });
     this.props.router.push(path);
   }
 
@@ -70,15 +68,7 @@ class LanguageFilterFooter extends React.Component {
 const mapStateToProps = (state, ownProps) => ({
   languages: getLanguages(state),
   value: ownProps.query[ownProps.property],
-  facet: getProjectsAppFacetByProperty(state, ownProps.property),
-  getFilterUrl: part => {
-    const basePathName = ownProps.organization ?
-        `/organizations/${ownProps.organization.key}/projects` :
-        '/projects';
-    const pathname = basePathName + (ownProps.isFavorite ? '/favorite' : '');
-    const query = omitBy({ ...ownProps.query, ...part }, isNil);
-    return { pathname, query };
-  }
+  facet: getProjectsAppFacetByProperty(state, ownProps.property)
 });
 
 export default connect(mapStateToProps)(withRouter(LanguageFilterFooter));
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
new file mode 100644 (file)
index 0000000..222bfb8
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { withRouter } from 'react-router';
+import classNames from 'classnames';
+import debounce from 'lodash/debounce';
+import { getFilterUrl } from './utils';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+class SearchFilter extends React.Component {
+  static propTypes = {
+    query: React.PropTypes.object.isRequired,
+    router: React.PropTypes.object.isRequired,
+    isFavorite: React.PropTypes.bool,
+    organization: React.PropTypes.object
+  }
+
+  constructor (props) {
+    super(props);
+    this.state = {
+      userQuery: props.query.search
+    };
+    this.handleSearch = debounce(this.handleSearch.bind(this), 250);
+  }
+
+  componentWillReceiveProps (nextProps) {
+    if (this.props.query.search === this.state.userQuery && nextProps.query.search !== this.props.query.search) {
+      this.setState({
+        userQuery: nextProps.query.search || ''
+      });
+    }
+  }
+
+  handleSearch (userQuery) {
+    const path = getFilterUrl(this.props, { search: userQuery || null });
+    this.props.router.push(path);
+  }
+
+  handleQueryChange (userQuery) {
+    this.setState({ userQuery });
+    if (!userQuery || userQuery.length >= 2) {
+      this.handleSearch(userQuery);
+    }
+  }
+
+  render () {
+    const { userQuery } = this.state;
+    const inputClassName = classNames('input-super-large', {
+      'touched': userQuery && userQuery.length < 2
+    });
+
+    return (
+      <div className="projects-facet-search" data-key="search">
+        <input
+          type="search"
+          value={userQuery || ''}
+          className={inputClassName}
+          placeholder={translate('projects.search')}
+          onChange={event => this.handleQueryChange(event.target.value)}
+          autoComplete="off"/>
+        <span className="note spacer-left">
+          {translateWithParameters('select2.tooShort', 2)}
+        </span>
+      </div>
+    );
+  }
+}
+
+export default withRouter(SearchFilter);
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/utils.js b/server/sonar-web/src/main/js/apps/projects/filters/utils.js
new file mode 100644 (file)
index 0000000..b1d35b6
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import omitBy from 'lodash/omitBy';
+import isNil from 'lodash/isNil';
+
+export const getFilterUrl = (ownProps, part) => {
+  const basePathName = ownProps.organization ?
+      `/organizations/${ownProps.organization.key}/projects` :
+      '/projects';
+  const pathname = basePathName + (ownProps.isFavorite ? '/favorite' : '');
+  const query = omitBy({ ...ownProps.query, ...part }, isNil);
+  return { pathname, query };
+};
index c193d42d57f2ba777a9a6ff9d60842632d76eca8..4d6ef3dc1579c4222cfc4196b146af6e49621463 100644 (file)
@@ -54,7 +54,8 @@ export const parseUrlQuery = urlQuery => ({
   'coverage': getAsNumericRating(urlQuery['coverage']),
   'duplications': getAsNumericRating(urlQuery['duplications']),
   'size': getAsNumericRating(urlQuery['size']),
-  'language': getAsArray(urlQuery['language'], getAsString)
+  'language': getAsArray(urlQuery['language'], getAsString),
+  'search': getAsString(urlQuery['search'])
 });
 
 const convertIssuesRating = (metric, rating) => {
@@ -159,6 +160,10 @@ export const convertToFilter = (query, isFavorite) => {
     }
   }
 
+  if (query['search'] != null) {
+    conditions.push(`query = "${query['search']}"`);
+  }
+
   return conditions.join(' and ');
 };
 
index fb5ffe9016222f0e5186ad7a76443d1dd8097f3b..0729984e8c7bee8389490c4a37f16efd636b2a94 100644 (file)
   border-bottom: 1px solid #e6e6e6;
 }
 
+.projects-facet-search {
+  position: relative;
+  padding-top: 10px;
+  padding-bottom: 10px;
+}
+
+.projects-facet-search .note {
+  position: absolute;
+  opacity: 0;
+  left: 0;
+  bottom: -7px;
+  transition: opacity 0.3s ease;
+}
+
+.projects-facet-search input.touched ~ .note {
+  opacity: 1;
+}
+
 .projects-facets-reset {
   float: right;
 }
index 7fef5712f703df259d0771e11cfaf2b43e3ef10d..25332bd9dd5f978234ee35bc4eb5baae829c9c47 100644 (file)
@@ -823,6 +823,7 @@ projects.no_favorite_projects=You don't have any favorite projects yet.
 projects.no_favorite_projects.engagement=Discover and mark as favorites projects you are interested in to have a quick access to them.
 projects.explore_projects=Explore Projects
 projects.not_analyzed=Project is not analyzed yet.
+projects.search=Search by project name or key
 
 
 #------------------------------------------------------------------------------