@@ -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 |
@@ -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 () { |
@@ -19,23 +19,13 @@ | |||
*/ | |||
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)); |
@@ -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)); |
@@ -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); |
@@ -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 }; | |||
}; |
@@ -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 '); | |||
}; | |||
@@ -112,6 +112,24 @@ | |||
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; | |||
} |
@@ -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 | |||
#------------------------------------------------------------------------------ |