From d4a017262d4c7e510f9abbfe7f36b3d24b885776 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Wed, 8 Aug 2018 09:17:13 +0200 Subject: SONAR-6400 Move the search box above the list of facet items (#592) --- server/sonar-web/src/main/js/api/components.ts | 17 +- .../apps/coding-rules/components/LanguageFacet.tsx | 78 +++-- .../components/LanguageFacetFooter.tsx | 63 ---- .../js/apps/coding-rules/components/TagFacet.tsx | 52 +-- .../src/main/js/apps/issues/components/App.tsx | 7 +- .../js/apps/issues/components/BulkChangeModal.tsx | 4 +- .../main/js/apps/issues/sidebar/AssigneeFacet.tsx | 181 +++++++++-- .../main/js/apps/issues/sidebar/LanguageFacet.tsx | 158 ++++----- .../main/js/apps/issues/sidebar/ProjectFacet.tsx | 235 +++++--------- .../src/main/js/apps/issues/sidebar/RuleFacet.tsx | 153 +++------ .../src/main/js/apps/issues/sidebar/Sidebar.tsx | 3 +- .../main/js/apps/issues/sidebar/StandardFacet.tsx | 73 +++-- .../src/main/js/apps/issues/sidebar/TagFacet.tsx | 142 +++----- .../sidebar/__tests__/AssigneeFacet-test.tsx | 9 - .../sidebar/__tests__/StandardFacet-test.tsx | 10 +- .../__snapshots__/AssigneeFacet-test.tsx.snap | 42 ++- .../__tests__/__snapshots__/Sidebar-test.tsx.snap | 12 +- .../__snapshots__/StandardFacet-test.tsx.snap | 131 ++++++-- server/sonar-web/src/main/js/apps/issues/utils.ts | 40 +-- .../src/main/js/components/facet/FacetFooter.tsx | 38 --- .../src/main/js/components/facet/FacetHeader.tsx | 2 +- .../src/main/js/components/facet/FacetItem.tsx | 8 +- .../main/js/components/facet/ListStyleFacet.tsx | 272 ++++++++++++++++ .../facet/__tests__/FacetFooter-test.tsx | 26 -- .../facet/__tests__/ListStyleFacet-test.tsx | 144 +++++++++ .../__snapshots__/FacetFooter-test.tsx.snap | 13 - .../__snapshots__/FacetHeader-test.tsx.snap | 9 +- .../__snapshots__/ListStyleFacet-test.tsx.snap | 360 +++++++++++++++++++++ server/sonar-web/src/main/js/helpers/search.tsx | 33 ++ 29 files changed, 1513 insertions(+), 802 deletions(-) delete mode 100644 server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacetFooter.tsx delete mode 100644 server/sonar-web/src/main/js/components/facet/FacetFooter.tsx create mode 100644 server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx delete mode 100644 server/sonar-web/src/main/js/components/facet/__tests__/FacetFooter-test.tsx create mode 100644 server/sonar-web/src/main/js/components/facet/__tests__/ListStyleFacet-test.tsx delete mode 100644 server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetFooter-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/helpers/search.tsx (limited to 'server') diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index 23b90e37a33..de4f0cb7a4f 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -25,7 +25,8 @@ import { BranchParameters, MyProject, Metric, - ComponentMeasure + ComponentMeasure, + LightComponent } from '../app/types'; export interface BaseSearchProjectsParameters { @@ -136,7 +137,19 @@ export function getComponent( return getJSON('/api/measures/component', data).then(r => r.component); } -export function getTree(component: string, options: RequestData = {}): Promise { +export interface TreeComponent extends LightComponent { + id: string; + name: string; + refId?: string; + refKey?: string; + tags?: string[]; + visibility: Visibility; +} + +export function getTree( + component: string, + options: RequestData = {} +): Promise<{ baseComponent: TreeComponent; components: TreeComponent[]; paging: Paging }> { return getJSON('/api/components/tree', { ...options, component }); } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx index c883f930557..2157620ad2f 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacet.tsx @@ -19,58 +19,80 @@ */ import * as React from 'react'; import { connect } from 'react-redux'; -import { uniq } from 'lodash'; -import Facet, { BasicProps } from './Facet'; -import LanguageFacetFooter from './LanguageFacetFooter'; +import { uniqBy } from 'lodash'; +import { BasicProps } from './Facet'; import { getLanguages } from '../../../store/rootReducer'; +import ListStyleFacet from '../../../components/facet/ListStyleFacet'; +import { translate } from '../../../helpers/l10n'; +import { highlightTerm } from '../../../helpers/search'; + +interface InstalledLanguage { + key: string; + name: string; +} interface StateProps { - referencedLanguages: { [language: string]: { key: string; name: string } }; + installedLanguages: InstalledLanguage[]; } interface Props extends BasicProps, StateProps {} class LanguageFacet extends React.PureComponent { - getLanguageName = (language: string) => { - const { referencedLanguages } = this.props; - return referencedLanguages[language] ? referencedLanguages[language].name : language; + getLanguageName = (languageKey: string) => { + const language = this.props.installedLanguages.find(l => l.key === languageKey); + return language ? language.name : languageKey; }; - handleSelect = (language: string) => { - const { values } = this.props; - this.props.onChange({ languages: uniq([...values, language]) }); + handleSearch = (query: string) => { + const options = this.getAllPossibleOptions(); + const results = options.filter(language => + language.name.toLowerCase().includes(query.toLowerCase()) + ); + const paging = { pageIndex: 1, pageSize: results.length, total: results.length }; + return Promise.resolve({ paging, results }); }; - renderFooter = () => { - if (!this.props.stats) { - return null; - } + getAllPossibleOptions = () => { + const { installedLanguages, stats = {} } = this.props; - return ( - + // add any language that presents in the facet, but might not be installed + // for such language we don't know their display name, so let's just use their key + // and make sure we reference each language only once + return uniqBy( + [...installedLanguages, ...Object.keys(stats).map(key => ({ key, name: key }))], + language => language.key ); }; + renderSearchResult = ({ name }: InstalledLanguage, term: string) => { + return highlightTerm(name, term); + }; + render() { - const { referencedLanguages, ...facetProps } = this.props; return ( - language.key} + getSearchResultText={(language: InstalledLanguage) => language.name} + onChange={this.props.onChange} + onSearch={this.handleSearch} + onToggle={this.props.onToggle} + open={this.props.open} property="languages" - renderFooter={this.renderFooter} - renderName={this.getLanguageName} - renderTextName={this.getLanguageName} + renderFacetItem={this.getLanguageName} + renderSearchResult={this.renderSearchResult} + searchPlaceholder={translate('search.search_for_languages')} + stats={this.props.stats} + values={this.props.values} /> ); } } -const mapStateToProps = (state: any): StateProps => ({ - referencedLanguages: getLanguages(state) +const mapStateToProps = (state: any) => ({ + installedLanguages: Object.values(getLanguages(state)) }); export default connect(mapStateToProps)(LanguageFacet); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacetFooter.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacetFooter.tsx deleted file mode 100644 index 97d247d1363..00000000000 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/LanguageFacetFooter.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 * as React from 'react'; -import { difference } from 'lodash'; -import Select from '../../../components/controls/Select'; -import { translate } from '../../../helpers/l10n'; - -type Option = { label: string; value: string }; - -interface Props { - referencedLanguages: { [language: string]: { key: string; name: string } }; - onSelect: (value: string) => void; - selected: string[]; -} - -export default class LanguageFacetFooter extends React.PureComponent { - handleChange = (option: Option) => this.props.onSelect(option.value); - - render() { - const options = difference( - Object.keys(this.props.referencedLanguages), - this.props.selected - ).map(key => ({ - label: this.props.referencedLanguages[key].name, - value: key - })); - - if (options.length === 0) { - return null; - } - - return ( -
- -
+ ); } @@ -317,8 +346,8 @@ export default class StandardFacet extends React.PureComponent { {this.props.cweOpen && ( <> - {this.renderCWEList()} {this.renderCWESearch()} + {this.renderCWEList()} {this.renderCWEHint()} )} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx index 9ebf4422d95..75d251a4dfe 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx @@ -18,20 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { sortBy, uniq, without } from 'lodash'; -import { formatFacetStat, Query } from '../utils'; +import { Query } from '../utils'; import { searchIssueTags } from '../../../api/issues'; import * as theme from '../../../app/theme'; import { Component } from '../../../app/types'; -import FacetBox from '../../../components/facet/FacetBox'; -import FacetFooter from '../../../components/facet/FacetFooter'; -import FacetHeader from '../../../components/facet/FacetHeader'; -import FacetItem from '../../../components/facet/FacetItem'; -import FacetItemsList from '../../../components/facet/FacetItemsList'; import TagsIcon from '../../../components/icons-components/TagsIcon'; import { translate } from '../../../helpers/l10n'; -import DeferredSpinner from '../../../components/common/DeferredSpinner'; -import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint'; +import ListStyleFacet from '../../../components/facet/ListStyleFacet'; +import { highlightTerm } from '../../../helpers/search'; interface Props { component: Component | undefined; @@ -46,116 +40,54 @@ interface Props { } export default class TagFacet extends React.PureComponent { - property = 'tags'; - - static defaultProps = { - open: true - }; - - handleItemClick = (itemValue: string, multiple: boolean) => { - const { tags } = this.props; - if (multiple) { - const { tags } = this.props; - const newValue = sortBy( - tags.includes(itemValue) ? without(tags, itemValue) : [...tags, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); - } else { - this.props.onChange({ - [this.property]: tags.includes(itemValue) && tags.length < 2 ? [] : [itemValue] - }); - } - }; - - handleHeaderClick = () => { - this.props.onToggle(this.property); - }; - - handleClear = () => { - this.props.onChange({ [this.property]: [] }); - }; - handleSearch = (query: string) => { - return searchIssueTags({ organization: this.props.organization, ps: 50, q: query }).then(tags => - tags.map(tag => ({ label: tag, value: tag })) + return searchIssueTags({ organization: this.props.organization, ps: 50, q: query }).then( + tags => ({ + paging: { pageIndex: 1, pageSize: tags.length, total: tags.length }, + results: tags + }) ); }; - handleSelect = (option: { value: string }) => { - const { tags } = this.props; - this.props.onChange({ [this.property]: uniq([...tags, option.value]) }); + getTagName = (tag: string) => { + return tag; }; - getStat(tag: string) { - const { stats } = this.props; - return stats ? stats[tag] : undefined; - } - - renderTag(tag: string) { + renderTag = (tag: string) => { return ( - + <> {tag} - + ); - } - - renderList() { - const { stats } = this.props; - - if (!stats) { - return null; - } - - const tags = sortBy(Object.keys(stats), key => -stats[key]); - - return ( - - {tags.map(tag => ( - - ))} - - ); - } + }; - renderFooter() { - if (!this.props.stats) { - return null; - } - - return ; - } + renderSearchResult = (tag: string, term: string) => ( + <> + + {highlightTerm(tag, term)} + + ); render() { - const { tags, stats = {} } = this.props; return ( - - - - - {this.props.open && ( - <> - {this.renderList()} - {this.renderFooter()} - - - )} - + tag} + getSearchResultText={tag => tag} + onChange={this.props.onChange} + onSearch={this.handleSearch} + onToggle={this.props.onToggle} + open={this.props.open} + property="tags" + renderFacetItem={this.renderTag} + renderSearchResult={this.renderSearchResult} + searchPlaceholder={translate('search.search_for_tags')} + stats={this.props.stats} + values={this.props.tags} + /> ); } } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx index 64217b4de0b..0d797101882 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx @@ -89,12 +89,3 @@ it('should call onToggle', () => { headerOnClick(); expect(onToggle).lastCalledWith('assignees'); }); - -it('should handle footer callbacks', () => { - const onChange = jest.fn(); - const wrapper = renderAssigneeFacet({ assignees: ['foo'], onChange }); - const onSelect = wrapper.find('FacetFooter').prop('onSelect'); - - onSelect({ value: 'qux' }); - expect(onChange).lastCalledWith({ assigned: true, assignees: ['foo', 'qux'] }); -}); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx index 815d6b03f93..41d59a42a92 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/StandardFacet-test.tsx @@ -138,13 +138,13 @@ it('should display correct selection', () => { }); it('should search CWE', () => { - const onChange = jest.fn(); - const wrapper = shallowRender({ onChange, open: true, cwe: ['42'], cweOpen: true }); + const wrapper = shallowRender({ open: true, cwe: ['42'], cweOpen: true }); wrapper .find('FacetBox[property="cwe"]') - .find('Select') - .prop('onChange')({ value: '111' }); - expect(onChange).toBeCalledWith({ cwe: ['111', '42'] }); + .find('SearchBox') + .prop('onChange')('unkn'); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); }); function shallowRender(props: Partial = {}) { diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap index ed02501141f..1ab0e42e2a6 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap @@ -16,6 +16,15 @@ exports[`should render 1`] = ` timeout={100} /> + - + - + - + -
-