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