From: Stas Vilchik Date: Thu, 2 Nov 2017 16:50:18 +0000 (+0100) Subject: SONAR-9225 Make all search bars consistent X-Git-Tag: 7.0-RC1~380 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=6805619766655e0e2e0d375503d65d5818c0c6a6;p=sonarqube.git SONAR-9225 Make all search bars consistent --- diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx index a4d7899387b..07f7e0508cd 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx @@ -29,6 +29,7 @@ import { } from '../../../../helpers/branches'; import { translate } from '../../../../helpers/l10n'; import { getProjectBranchUrl } from '../../../../helpers/urls'; +import SearchBox from '../../../../components/controls/SearchBox'; import Tooltip from '../../../../components/controls/Tooltip'; interface Props { @@ -75,8 +76,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent) => - this.setState({ query: event.currentTarget.value, selected: null }); + handleSearchChange = (query: string) => this.setState({ query, selected: null }); handleKeyDown = (event: React.KeyboardEvent) => { switch (event.keyCode) { @@ -84,10 +84,6 @@ export default class ComponentNavBranchesMenu extends React.PureComponent branch.name === this.getSelected(); renderSearch = () => ( -
- - +
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx index 6232c936b4d..ae5b7a80d72 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx @@ -66,13 +66,13 @@ it('selects next & previous', () => { onClose={jest.fn()} /> ); - elementKeydown(wrapper.find('input'), 40); + elementKeydown(wrapper.find('SearchBox'), 40); wrapper.update(); expect(wrapper.state().selected).toBe('foo'); - elementKeydown(wrapper.find('input'), 40); + elementKeydown(wrapper.find('SearchBox'), 40); wrapper.update(); expect(wrapper.state().selected).toBe('foobar'); - elementKeydown(wrapper.find('input'), 38); + elementKeydown(wrapper.find('SearchBox'), 38); wrapper.update(); expect(wrapper.state().selected).toBe('foo'); }); diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap index 7a01722ba4c..6f39c721b9d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap @@ -5,22 +5,13 @@ exports[`renders list 1`] = ` className="dropdown-menu" >
- -
@@ -181,22 +172,13 @@ exports[`searches 1`] = ` className="dropdown-menu" >
- -
diff --git a/server/sonar-web/src/main/js/app/components/search/Search.css b/server/sonar-web/src/main/js/app/components/search/Search.css index a3fe6f5b49b..f3730f2b276 100644 --- a/server/sonar-web/src/main/js/app/components/search/Search.css +++ b/server/sonar-web/src/main/js/app/components/search/Search.css @@ -3,6 +3,12 @@ padding-right: 3px; } +.navbar-search .search-box, +.navbar-search .search-box-input { + width: 310px; + max-width: none; +} + .navbar-search-input { vertical-align: middle; width: 310px; @@ -13,22 +19,20 @@ .navbar-search-input-hint { position: absolute; - top: 4px; - right: 30px; + top: 1px; + right: 27px; line-height: var(--controlHeight); font-size: var(--smallFontSize); color: var(--secondFontColor); } -.navbar-search-input-hint.is-shifted { - z-index: 7501; - top: 32px; -} .navbar-search-icon { position: relative; + z-index: var(--aboveNormalZIndex); vertical-align: middle; width: 16px; margin-right: -20px; + background-color: #fff; color: var(--secondFontColor); } @@ -78,9 +82,10 @@ } .global-navbar-search-dropdown { + top: calc(100% + 3px) !important; max-height: 80vh; width: 440px; - padding: 0; + padding: 0 !important; overflow-y: auto; overflow-x: hidden; } diff --git a/server/sonar-web/src/main/js/app/components/search/Search.js b/server/sonar-web/src/main/js/app/components/search/Search.js index 5ea8a0f915d..463c4dc5d73 100644 --- a/server/sonar-web/src/main/js/app/components/search/Search.js +++ b/server/sonar-web/src/main/js/app/components/search/Search.js @@ -23,6 +23,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import key from 'keymaster'; import { debounce, keyBy, uniqBy } from 'lodash'; +import { FormattedMessage } from 'react-intl'; import SearchResults from './SearchResults'; import SearchResult from './SearchResult'; import { sortQualifiers } from './utils'; @@ -30,6 +31,7 @@ import { sortQualifiers } from './utils'; import RecentHistory from '../../components/RecentHistory'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; import ClockIcon from '../../../components/common/ClockIcon'; +import SearchBox from '../../../components/controls/SearchBox'; import { getSuggestions } from '../../../api/components'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { scrollToElement } from '../../../helpers/scrolling'; @@ -59,7 +61,7 @@ type State = { */ export default class Search extends React.PureComponent { - /*:: input: HTMLElement; */ + /*:: input: HTMLInputElement | null; */ /*:: mounted: boolean; */ /*:: node: HTMLElement; */ /*:: nodes: { [string]: HTMLElement }; @@ -92,7 +94,9 @@ export default class Search extends React.PureComponent { componentDidMount() { this.mounted = true; key('s', () => { - this.input.focus(); + if (this.input) { + this.input.focus(); + } this.openSearch(); return false; }); @@ -169,6 +173,12 @@ export default class Search extends React.PureComponent { return uniqBy([...components, ...recentlyBrowsed], 'key'); }; + stopLoading = () => { + if (this.mounted) { + this.setState({ loading: false }); + } + }; + search = (query /*: string */) => { if (query.length === 0 || query.length >= 2) { this.setState({ loading: true }); @@ -191,10 +201,10 @@ export default class Search extends React.PureComponent { projects: { ...state.projects, ...keyBy(response.projects, 'key') }, results, selected: list.length > 0 ? list[0] : null, - shortQuery: response.warning === 'short_input' + shortQuery: query.length > 2 && response.warning === 'short_input' })); } - }); + }, this.stopLoading); } else { this.setState({ loading: false }); } @@ -221,12 +231,11 @@ export default class Search extends React.PureComponent { selected: moreResults.length > 0 ? moreResults[0].key : state.selected })); } - }); + }, this.stopLoading); } }; - handleQueryChange = (event /*: { currentTarget: HTMLInputElement } */) => { - const query = event.currentTarget.value; + handleQueryChange = (query /*: string */) => { this.setState({ query, shortQuery: query.length === 1 }); this.search(query); }; @@ -278,10 +287,6 @@ export default class Search extends React.PureComponent { event.preventDefault(); this.openSelected(); return; - case 27: - event.preventDefault(); - this.closeSearch(); - return; case 38: event.preventDefault(); this.selectPrevious(); @@ -297,10 +302,18 @@ export default class Search extends React.PureComponent { this.setState({ selected }); }; + handleClick = (event /*: Event */) => { + event.stopPropagation(); + }; + innerRef = (component /*: string */, node /*: HTMLElement */) => { this.nodes[component] = node; }; + searchInputRef = (node /*: HTMLInputElement | null */) => { + this.input = node; + }; + renderResult = (component /*: Component */) => ( - - - - - + + event.stopPropagation()} + onClick={this.handleClick} onFocus={this.openSearch} onKeyDown={this.handleKeyDown} - ref={node => (this.input = node)} placeholder={translate('search.placeholder')} - type="search" value={this.state.query} /> {this.state.shortQuery && ( - 5 - })}> + {translateWithParameters('select2.tooShort', 2)} )} @@ -375,12 +379,11 @@ export default class Search extends React.PureComponent { {translate('recently_browsed')} -
s' - ) + s }} />
diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js index 822d42b58f1..82e8d8d4fae 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js @@ -38,12 +38,12 @@ function component(key /*: string */, qualifier /*: string */ = 'TRK') { } function next(form /*: ShallowWrapper */, expected /*: string */) { - elementKeydown(form.find('input'), 40); + elementKeydown(form.find('SearchBox'), 40); expect(form.state().selected).toBe(expected); } function prev(form /*: ShallowWrapper */, expected /*: string */) { - elementKeydown(form.find('input'), 38); + elementKeydown(form.find('SearchBox'), 38); expect(form.state().selected).toBe(expected); } @@ -83,7 +83,7 @@ it('opens selected on enter', () => { }); const openSelected = jest.fn(); form.instance().openSelected = openSelected; - elementKeydown(form.find('input'), 13); + elementKeydown(form.find('SearchBox'), 13); expect(openSelected).toBeCalled(); }); @@ -95,14 +95,6 @@ it('shows warning about short input', () => { expect(form.find('.navbar-search-input-hint')).toMatchSnapshot(); }); -it('closes on escape', () => { - const form = render(); - form.instance().openSearch(); - expect(form.state().open).toBe(true); - elementKeydown(form.find('input'), 27); - expect(form.state().open).toBe(false); -}); - it('closes on click outside', () => { const form = mount( diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.js.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.js.snap index 86b9f83f770..6541c673539 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.js.snap @@ -10,7 +10,7 @@ exports[`shows warning about short input 1`] = ` exports[`shows warning about short input 2`] = ` select2.tooShort.2 diff --git a/server/sonar-web/src/main/js/app/styles/components/menu.css b/server/sonar-web/src/main/js/app/styles/components/menu.css index 62f7314addc..58878f990c1 100644 --- a/server/sonar-web/src/main/js/app/styles/components/menu.css +++ b/server/sonar-web/src/main/js/app/styles/components/menu.css @@ -99,22 +99,10 @@ padding: 4px 16px 0; } +.menu-search .search-box, .menu-search .search-box-input { - font-size: var(--smallFontSize); -} - -.menu-search .search-box-submit { - vertical-align: baseline; -} - -.menu-search-full-width { - display: flex; - align-items: center; -} - -.menu-search-full-width .search-box-input { - flex-grow: 1; - width: auto; + max-width: none; + min-width: 240px; } .menu-search ~ .menu > li > a:hover, diff --git a/server/sonar-web/src/main/js/app/styles/components/search-navigator.css b/server/sonar-web/src/main/js/app/styles/components/search-navigator.css index 39c48ca9fc9..2bfdbb7415b 100644 --- a/server/sonar-web/src/main/js/app/styles/components/search-navigator.css +++ b/server/sonar-web/src/main/js/app/styles/components/search-navigator.css @@ -534,11 +534,7 @@ a.search-navigator-facet:focus .facet-stat { } .search-navigator-facet-query { - padding: 7px 10px 27px; -} - -.search-navigator-facet-query input { - width: 100%; + padding: 7px 0 27px; } .search-navigator-facet-custom-value { diff --git a/server/sonar-web/src/main/js/app/styles/components/search.css b/server/sonar-web/src/main/js/app/styles/components/search.css deleted file mode 100644 index 2eed12626d5..00000000000 --- a/server/sonar-web/src/main/js/app/styles/components/search.css +++ /dev/null @@ -1,65 +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. - */ -.search-box { - position: relative; - font-size: 0; - white-space: nowrap; -} - -.search-box-input { - vertical-align: middle; - width: 250px; - border: none !important; - font-size: var(--baseFontSize); -} - -.search-box-input ~ .note { - opacity: 0; - transition: opacity 0.3s ease; -} - -.search-box-input.touched ~ .note { - opacity: 1; -} - -.search-box-submit { - display: inline-block; - vertical-align: middle; -} - -.search-box-submit .icon-search:before { - color: var(--secondFontColor); - font-size: var(--mediumFontSize); -} - -.search-box-submit .icon-search-new { - position: relative; - top: 1px; -} - -.search-box-input-note { - position: absolute; - top: 100%; - left: 0; - line-height: 1; - color: var(--secondFontColor); - font-size: var(--smallFontSize); - white-space: nowrap; -} diff --git a/server/sonar-web/src/main/js/app/styles/init/icons.css b/server/sonar-web/src/main/js/app/styles/init/icons.css index 82baae3dac5..315803eb185 100644 --- a/server/sonar-web/src/main/js/app/styles/init/icons.css +++ b/server/sonar-web/src/main/js/app/styles/init/icons.css @@ -709,21 +709,6 @@ a:hover > .icon-radio { font-size: var(--bigFontSize); } -.icon-search:before { - content: '\f002'; - font-size: var(--bigFontSize); -} - -.icon-search-new { - display: inline-block; - vertical-align: top; - width: 16px; - height: 16px; - background-size: 13px 14px; - background: no-repeat center center; - background-image: url('data:image/svg+xml,%3Csvg%20width%3D%2213%22%20height%3D%2214%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cpath%20d%3D%22M9%206.5c0-.964-.342-1.788-1.027-2.473C7.288%203.342%206.463%203%205.5%203c-.964%200-1.788.342-2.473%201.027C2.342%204.712%202%205.537%202%206.5c0%20.964.342%201.788%201.027%202.473C3.712%209.658%204.537%2010%205.5%2010c.964%200%201.788-.342%202.473-1.027C8.658%208.288%209%207.463%209%206.5zm4%206.5c0%20.27-.1.505-.297.703-.198.198-.432.297-.703.297-.28%200-.516-.1-.703-.297l-2.68-2.672c-.932.647-1.97.97-3.117.97-.745%200-1.457-.145-2.137-.434-.68-.29-1.265-.68-1.758-1.171-.492-.493-.882-1.08-1.17-1.758C.144%207.957%200%207.245%200%206.5c0-.745.145-1.457.434-2.137.29-.68.68-1.265%201.17-1.758.494-.492%201.08-.882%201.76-1.17C4.043%201.144%204.753%201%205.5%201c.745%200%201.457.145%202.137.434.68.29%201.265.68%201.758%201.17.492.494.882%201.08%201.17%201.76.29.68.435%201.39.435%202.136%200%201.146-.323%202.185-.97%203.117l2.68%202.68c.194.193.29.427.29.703z%22%20fill%3D%22%23777%22%20fill-rule%3D%22nonzero%22%2F%3E%3C%2Fsvg%3E'); -} - .icon-edit:before { content: '\f040'; font-size: var(--mediumFontSize); diff --git a/server/sonar-web/src/main/js/app/styles/sonar.css b/server/sonar-web/src/main/js/app/styles/sonar.css index bccf053cfe7..6f0a00b31e5 100644 --- a/server/sonar-web/src/main/js/app/styles/sonar.css +++ b/server/sonar-web/src/main/js/app/styles/sonar.css @@ -47,7 +47,6 @@ @import './components/panels.css'; @import './components/badges.css'; @import './components/columns.css'; -@import './components/search.css'; @import './components/side-tabs.css'; @import './components/boxed-group.css'; diff --git a/server/sonar-web/src/main/js/apps/background-tasks/__tests__/background-tasks-test.js b/server/sonar-web/src/main/js/apps/background-tasks/__tests__/background-tasks-test.js index c6c69e6757f..5764615be32 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/__tests__/background-tasks-test.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/__tests__/background-tasks-test.js @@ -48,19 +48,19 @@ describe('Search', () => { it('should render search form', () => { const component = shallow(); - expect(component.find('.js-search').length).toBe(1); + expect(component.find('SearchBox').exists()).toBeTruthy(); }); it('should not render search form', () => { const component = shallow(); - expect(component.find('.js-search').length).toBe(0); + expect(component.find('SearchBox').exists()).toBeFalsy(); }); it('should search', done => { const searchSpy = jest.fn(); const component = shallow(); - const searchInput = component.find('.js-search'); - change(searchInput, 'some search query'); + const searchInput = component.find('SearchBox'); + searchInput.prop('onChange')('some search query'); setTimeout(() => { expect(searchSpy).toBeCalledWith({ query: 'some search query' }); done(); diff --git a/server/sonar-web/src/main/js/apps/background-tasks/background-tasks.css b/server/sonar-web/src/main/js/apps/background-tasks/background-tasks.css index 74307146109..6f421e50e88 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/background-tasks.css +++ b/server/sonar-web/src/main/js/apps/background-tasks/background-tasks.css @@ -4,7 +4,7 @@ } .bt-search-form > li + li { - margin-left: 40px; + margin-left: 16px; } .bt-search-form-label { @@ -15,8 +15,8 @@ padding: 4px 0; } -.bt-search-form-right { - margin-left: auto !important; +.bt-search-form-large { + flex: 1; } .bt-workers-warning-icon { diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Search.js b/server/sonar-web/src/main/js/apps/background-tasks/components/Search.js index d8bf4203b0c..023ab973177 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/Search.js +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Search.js @@ -25,6 +25,7 @@ import TypesFilter from './TypesFilter'; import CurrentsFilter from './CurrentsFilter'; import DateFilter from './DateFilter'; import { DEFAULT_FILTERS } from './../constants'; +import SearchBox from '../../../components/controls/SearchBox'; import { translate } from '../../../helpers/l10n'; export default class Search extends React.PureComponent { @@ -54,9 +55,9 @@ export default class Search extends React.PureComponent { this.props.onFilterUpdate(date); } - handleQueryChange(query /*: string */) { + handleQueryChange = (query /*: string */) => { this.props.onFilterUpdate({ query }); - } + }; handleReload(e /*: Object */) { e.target.blur(); @@ -78,18 +79,11 @@ export default class Search extends React.PureComponent { } return ( -
  • -
    - {translate('background_tasks.search_by_task_or_component')} -
    - - this.handleQueryChange(e.target.value)} +
  • +
  • ); @@ -143,7 +137,7 @@ export default class Search extends React.PureComponent { {this.renderSearchBox()} -
  • +
  • {' '} diff --git a/server/sonar-web/src/main/js/apps/code/code.css b/server/sonar-web/src/main/js/apps/code/code.css index 2de8ede896b..66658e61943 100644 --- a/server/sonar-web/src/main/js/apps/code/code.css +++ b/server/sonar-web/src/main/js/apps/code/code.css @@ -55,20 +55,6 @@ display: none; } -.code-search .search-box { - padding-right: 10px; -} - -.code-search .search-box .note { - vertical-align: middle; - opacity: 0; - transition: opacity 0.3s ease; -} - -.code-search .search-box input.touched ~ .note { - opacity: 1; -} - .code-components-header { position: sticky; top: 95px; diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.tsx b/server/sonar-web/src/main/js/apps/code/components/Search.tsx index 3f254bf87ec..3e666e19317 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Search.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Search.tsx @@ -20,13 +20,13 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import * as classNames from 'classnames'; -import { debounce } from 'lodash'; import Components from './Components'; import { getTree } from '../../../api/components'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; import { parseError } from '../utils'; import { getProjectUrl } from '../../../helpers/urls'; import { Component } from '../types'; +import SearchBox from '../../../components/controls/SearchBox'; +import { translate } from '../../../helpers/l10n'; interface Props { branch?: string; @@ -43,7 +43,6 @@ interface State { } export default class Search extends React.PureComponent { - input: HTMLInputElement; mounted: boolean; static contextTypes = { @@ -55,10 +54,6 @@ export default class Search extends React.PureComponent { loading: false }; - componentWillMount() { - this.handleSearch = debounce(this.handleSearch, 250); - } - componentDidMount() { this.mounted = true; } @@ -79,10 +74,6 @@ export default class Search extends React.PureComponent { this.mounted = false; } - checkInputValue(query: string) { - return this.input.value === query; - } - handleSelectNext() { const { selectedIndex, results } = this.state; if (results != null && selectedIndex != null && selectedIndex < results.length - 1) { @@ -114,27 +105,26 @@ export default class Search extends React.PureComponent { } } - handleKeyDown(e: React.KeyboardEvent) { - switch (e.keyCode) { + handleKeyDown = (event: React.KeyboardEvent) => { + switch (event.keyCode) { case 13: - e.preventDefault(); + event.preventDefault(); this.handleSelectCurrent(); break; case 38: - e.preventDefault(); + event.preventDefault(); this.handleSelectPrevious(); break; case 40: - e.preventDefault(); + event.preventDefault(); this.handleSelectNext(); break; default: // do nothing } - } + }; handleSearch = (query: string) => { - // first time check if value has changed due to debounce - if (this.mounted && this.checkInputValue(query)) { + if (this.mounted) { const { branch, component, onError } = this.props; this.setState({ loading: true }); @@ -143,8 +133,7 @@ export default class Search extends React.PureComponent { getTree(component.key, { branch, q: query, s: 'qualifier,name', qualifiers }) .then(r => { - // second time check if value has change due to api request - if (this.mounted && this.checkInputValue(query)) { + if (this.mounted) { this.setState({ results: r.components, selectedIndex: r.components.length > 0 ? 0 : undefined, @@ -153,8 +142,7 @@ export default class Search extends React.PureComponent { } }) .catch(e => { - // second time check if value has change due to api request - if (this.mounted && this.checkInputValue(query)) { + if (this.mounted) { this.setState({ loading: false }); parseError(e).then(onError); } @@ -162,61 +150,36 @@ export default class Search extends React.PureComponent { } }; - handleQueryChange(query: string) { + handleQueryChange = (query: string) => { this.setState({ query }); - if (query.length < 3) { + if (query.length === 0) { this.setState({ results: undefined }); } else { this.handleSearch(query); } - } - - handleInputChange(event: React.SyntheticEvent) { - const query = event.currentTarget.value; - this.handleQueryChange(query); - } - - handleSubmit(event: React.SyntheticEvent) { - event.preventDefault(); - const query = this.input.value; - this.handleQueryChange(query); - } + }; render() { const { component } = this.props; - const { query, loading, selectedIndex, results } = this.state; + const { loading, selectedIndex, results } = this.state; const selected = selectedIndex != null && results != null ? results[selectedIndex] : undefined; const containerClassName = classNames('code-search', { 'code-search-with-results': results != null }); - const inputClassName = classNames('search-box-input', { - touched: query.length > 0 && query.length < 3 - }); + const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier); return ( diff --git a/server/sonar-web/src/main/js/apps/groups/components/GroupsAppContainer.js b/server/sonar-web/src/main/js/apps/groups/components/GroupsAppContainer.js index 16525d55ee5..a8a10e4a2c2 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/GroupsAppContainer.js +++ b/server/sonar-web/src/main/js/apps/groups/components/GroupsAppContainer.js @@ -21,6 +21,7 @@ import React from 'react'; import Helmet from 'react-helmet'; import init from '../init'; import { translate } from '../../../helpers/l10n'; +import '../../../components/controls/SearchBox.css'; export default class GroupsAppContainer extends React.PureComponent { componentDidMount() { diff --git a/server/sonar-web/src/main/js/apps/groups/search-view.js b/server/sonar-web/src/main/js/apps/groups/search-view.js index 9acd7109e57..1c8401913bd 100644 --- a/server/sonar-web/src/main/js/apps/groups/search-view.js +++ b/server/sonar-web/src/main/js/apps/groups/search-view.js @@ -24,10 +24,15 @@ import Template from './templates/groups-search.hbs'; export default Marionette.ItemView.extend({ template: Template, + ui: { + reset: '.js-reset' + }, + events: { 'submit #groups-search-form': 'onFormSubmit', - 'search #groups-search-query': 'debouncedOnKeyUp', - 'keyup #groups-search-query': 'debouncedOnKeyUp' + 'search #groups-search-query': 'initialOnKeyUp', + 'keyup #groups-search-query': 'initialOnKeyUp', + 'click .js-reset': 'onResetClick' }, initialize() { @@ -44,6 +49,12 @@ export default Marionette.ItemView.extend({ this.debouncedOnKeyUp(); }, + initialOnKeyUp() { + const q = this.getQuery(); + this.ui.reset.toggleClass('hidden', q.length === 0); + this.debouncedOnKeyUp(); + }, + onKeyUp() { const q = this.getQuery(); if (q === this._bufferedValue) { @@ -62,5 +73,14 @@ export default Marionette.ItemView.extend({ search(q) { return this.collection.fetch({ reset: true, data: { q } }); + }, + + onResetClick(e) { + e.preventDefault(); + e.currentTarget.blur(); + this.$('#groups-search-query') + .val('') + .focus(); + this.onKeyUp(); } }); diff --git a/server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs b/server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs index 013f7cba90a..e0d8614362f 100644 --- a/server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs +++ b/server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs @@ -1,6 +1,15 @@
    diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js index e75f10633a4..33675c7bbb4 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js @@ -56,7 +56,7 @@ class LanguageFacetFooter extends React.PureComponent { noResultsText={translate('select2.noMatches')} onChange={this.handleChange} options={options} - placeholder={translate('search_verb')} + placeholder={translate('search.search_for_languages')} searchable={true} /> diff --git a/server/sonar-web/src/main/js/apps/marketplace/Search.tsx b/server/sonar-web/src/main/js/apps/marketplace/Search.tsx index 3bef8189711..1eaef17728e 100644 --- a/server/sonar-web/src/main/js/apps/marketplace/Search.tsx +++ b/server/sonar-web/src/main/js/apps/marketplace/Search.tsx @@ -18,9 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { debounce } from 'lodash'; -import RadioToggle from '../../components/controls/RadioToggle'; import { Query } from './utils'; +import RadioToggle from '../../components/controls/RadioToggle'; +import SearchBox from '../../components/controls/SearchBox'; import { translate } from '../../helpers/l10n'; interface Props { @@ -29,33 +29,13 @@ interface Props { updateQuery: (newQuery: Partial) => void; } -interface State { - search?: string; -} - -export default class Search extends React.PureComponent { - constructor(props: Props) { - super(props); - this.state = { search: props.query.search }; - this.updateSearch = debounce(this.updateSearch, 250); - } - - componentWillReceiveProps(nextProps: Props) { - if (nextProps.query.search !== this.state.search) { - this.setState({ search: nextProps.query.search }); - } - } - - handleSearch = (e: React.SyntheticEvent) => { - const search = e.currentTarget.value; - this.setState({ search }); - this.updateSearch(search); +export default class Search extends React.PureComponent { + handleSearch = (search: string) => { + this.props.updateQuery({ search }); }; handleFilterChange = (filter: string) => this.props.updateQuery({ filter }); - updateSearch = (search: string) => this.props.updateQuery({ search }); - render() { const { query, updateCenterActive } = this.props; const radioOptions = [ @@ -77,21 +57,11 @@ export default class Search extends React.PureComponent { value={query.filter} /> -
    - - -
    + ); } diff --git a/server/sonar-web/src/main/js/apps/organizations/components/MembersListHeader.js b/server/sonar-web/src/main/js/apps/organizations/components/MembersListHeader.js index 8303d700623..97371919773 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/MembersListHeader.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/MembersListHeader.js @@ -19,7 +19,7 @@ */ //@flow import React from 'react'; -import UsersSearch from '../../users/components/UsersSearch'; +import SearchBox from '../../../components/controls/SearchBox'; import { formatMeasure } from '../../../helpers/measures'; import { translate } from '../../../helpers/l10n'; @@ -30,21 +30,19 @@ type Props = { }; */ -export default class MembersListHeader extends React.PureComponent { - /*:: props: Props; */ - - render() { - const { total } = this.props; - return ( -
    - - {total != null && ( - - {formatMeasure(total, 'INT')}{' '} - {translate('organization.members.members')} - - )} -
    - ); - } +export default function MembersListHeader({ handleSearch, total } /*: Props */) { + return ( +
    + + {total != null && ( + + {formatMeasure(total, 'INT')} {translate('organization.members.members')} + + )} +
    + ); } diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListHeader-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListHeader-test.js.snap index 4dce97f4aef..42461a5120c 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListHeader-test.js.snap +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListHeader-test.js.snap @@ -4,9 +4,10 @@ exports[`should render with the total 1`] = `
    - -
    `; diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js index 76e25c9c1f1..507aa1a8368 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js @@ -19,7 +19,7 @@ */ //@flow import React from 'react'; -import { debounce, without } from 'lodash'; +import { without } from 'lodash'; import TagsSelector from '../../../components/tags/TagsSelector'; import { searchProjectTags } from '../../../api/components'; @@ -42,13 +42,7 @@ const LIST_SIZE = 10; export default class MetaTagsSelector extends React.PureComponent { /*:: props: Props; */ - /*:: state: State; */ - - constructor(props /*: Props */) { - super(props); - this.state = { searchResult: [] }; - this.onSearch = debounce(this.onSearch, 250); - } + state /*: State */ = { searchResult: [] }; componentDidMount() { this.onSearch(''); @@ -56,12 +50,10 @@ export default class MetaTagsSelector extends React.PureComponent { onSearch = (query /*: string */) => { searchProjectTags({ - q: query || '', + q: query, ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100) }).then(result => { - this.setState({ - searchResult: result.tags - }); + this.setState({ searchResult: result.tags }); }); }; diff --git a/server/sonar-web/src/main/js/apps/permission-templates/components/Template.js b/server/sonar-web/src/main/js/apps/permission-templates/components/Template.js index 82260039e0e..c54dfd00607 100644 --- a/server/sonar-web/src/main/js/apps/permission-templates/components/Template.js +++ b/server/sonar-web/src/main/js/apps/permission-templates/components/Template.js @@ -37,18 +37,14 @@ export default class Template extends React.PureComponent { topQualifiers: PropTypes.array.isRequired }; - constructor(props) { - super(props); - this.state = { - loading: false, - users: [], - groups: [], - query: '', - filter: 'all', - selectedPermission: null - }; - this.requestHoldersDebounced = debounce(this.requestHolders, 250); - } + state = { + loading: false, + users: [], + groups: [], + query: '', + filter: 'all', + selectedPermission: null + }; componentDidMount() { this.mounted = true; @@ -140,9 +136,7 @@ export default class Template extends React.PureComponent { handleSearch = query => { this.setState({ query }); - if (query.length === 0 || query.length > 2) { - this.requestHoldersDebounced(query); - } + this.requestHolders(query); }; handleFilter = filter => { diff --git a/server/sonar-web/src/main/js/apps/permissions/global/store/actions.js b/server/sonar-web/src/main/js/apps/permissions/global/store/actions.js index 639097378a0..82dfc9e1b67 100644 --- a/server/sonar-web/src/main/js/apps/permissions/global/store/actions.js +++ b/server/sonar-web/src/main/js/apps/permissions/global/store/actions.js @@ -87,9 +87,7 @@ export const updateQuery = (query /*: string */ = '', organization /*: ?string * dispatch /*: Dispatch */ ) => { dispatch({ type: UPDATE_QUERY, query }); - if (query.length === 0 || query.length > 2) { - dispatch(loadHolders(organization)); - } + dispatch(loadHolders(organization)); }; export const updateFilter = (filter /*: string */, organization /*: ?string */) => ( diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js index 4ab52668b98..69f811a5f45 100644 --- a/server/sonar-web/src/main/js/apps/permissions/project/components/App.js +++ b/server/sonar-web/src/main/js/apps/permissions/project/components/App.js @@ -151,11 +151,7 @@ export default class App extends React.PureComponent { handleQueryChange = (query /*: string */) => { if (this.mounted) { - this.setState({ query }, () => { - if (query.length === 0 || query.length > 2) { - this.loadHolders(); - } - }); + this.setState({ query }, this.loadHolders); } }; diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js index 0fdb5568931..0115c49a688 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js @@ -20,79 +20,33 @@ import React from 'react'; import PropTypes from 'prop-types'; import RadioToggle from '../../../../components/controls/RadioToggle'; +import SearchBox from '../../../../components/controls/SearchBox'; import { translate, translateWithParameters } from '../../../../helpers/l10n'; -export default class SearchForm extends React.PureComponent { - static propTypes = { - query: PropTypes.string, - filter: PropTypes.oneOf(['all', 'users', 'groups']), - onSearch: PropTypes.func, - onFilter: PropTypes.func - }; - - componentWillMount() { - this.handleSubmit = this.handleSubmit.bind(this); - this.handleSearch = this.handleSearch.bind(this); - } - - handleSubmit(e) { - e.preventDefault(); - this.handleSearch(); - } - - handleSearch() { - const { value } = this.refs.searchInput; - this.props.onSearch(value); - } - - handleFilter(filter) { - this.props.onFilter(filter); - } - - render() { - const { query, filter } = this.props; - - const filterOptions = [ - { value: 'all', label: translate('all') }, - { value: 'users', label: translate('users.page') }, - { value: 'groups', label: translate('user_groups.page') } - ]; - - return ( -
    - + + +
    + - -
    - - - {query.length > 0 && - query.length < 3 && ( -
    -
    - {translateWithParameters('select2.tooShort', 3)} -
    -
    -
    - )} -
    - ); - } +
    + ); } diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx index 264cb863e52..c8de3ad3906 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx @@ -79,7 +79,6 @@ export default function PageHeader(props: Props) { )} void; - query: { search?: string | undefined }; -} - -interface State { - userQuery?: string; -} - -export default class SearchFilter extends React.PureComponent { - constructor(props: Props) { - super(props); - this.state = { userQuery: props.query.search }; - } - - componentWillReceiveProps(nextProps: Props) { - if ( - this.props.query.search === this.state.userQuery && - nextProps.query.search !== this.props.query.search - ) { - this.setState({ userQuery: nextProps.query.search || '' }); - } - } - - handleQueryChange = (event: React.SyntheticEvent) => { - const { value } = event.currentTarget; - this.setState({ userQuery: value }); - if (!value || value.length >= 2) { - this.props.handleSearch(value); - } - }; - - render() { - const { userQuery } = this.state; - const shortQuery = userQuery != null && userQuery.length === 1; - return ( -
    - - {shortQuery && ( - {translateWithParameters('select2.tooShort', 2)} - )} -
    - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.tsx b/server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.tsx index b17ca9a1a21..4934f7ecaf3 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.tsx +++ b/server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.tsx @@ -19,9 +19,9 @@ */ import * as React from 'react'; import * as PropTypes from 'prop-types'; -import { debounce } from 'lodash'; import { getFilterUrl } from './utils'; -import SearchFilter from './SearchFilter'; +import SearchBox from '../../../components/controls/SearchBox'; +import { translate } from '../../../helpers/l10n'; interface Props { className?: string; @@ -35,11 +35,6 @@ export default class SearchFilterContainer extends React.PureComponent { router: PropTypes.object.isRequired }; - constructor(props: Props) { - super(props); - this.handleSearch = debounce(this.handleSearch, 250); - } - handleSearch = (userQuery?: string) => { const path = getFilterUrl(this.props, { search: userQuery }); this.context.router.push(path); @@ -47,11 +42,13 @@ export default class SearchFilterContainer extends React.PureComponent { render() { return ( - +
    + +
    ); } } diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilter-test.tsx deleted file mode 100644 index 2724f834a36..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilter-test.tsx +++ /dev/null @@ -1,58 +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 * as React from 'react'; -import { shallow } from 'enzyme'; -import SearchFilter from '../SearchFilter'; -import { change } from '../../../../helpers/testUtils'; - -it('should render correctly without any search query', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); -}); - -it('should render with a search query', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); -}); - -it('should display a help message when there is less than 2 characters', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - wrapper.setState({ userQuery: 'foo' }); - expect(wrapper).toMatchSnapshot(); -}); - -it('searches', () => { - const handleSearch = jest.fn(); - const wrapper = shallow(); - - change(wrapper.find('input'), 'a'); - expect(handleSearch).not.toBeCalled(); - - change(wrapper.find('input'), 'abc'); - expect(handleSearch).toBeCalledWith('abc'); -}); - -it('updates state to new props', () => { - const wrapper = shallow(); - expect(wrapper.state()).toEqual({ userQuery: 'abc' }); - wrapper.setProps({ query: { search: 'def' } }); - expect(wrapper.state()).toEqual({ userQuery: 'def' }); -}); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilterContainer-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilterContainer-test.tsx index 7825caf1ca2..3154a9488fd 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilterContainer-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilterContainer-test.tsx @@ -21,17 +21,10 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import SearchFilterContainer from '../SearchFilterContainer'; -// mocking lodash, because mocking timers is now working for some reason :'( -jest.mock('lodash', () => { - const lodash = require.requireActual('lodash'); - lodash.debounce = (fn: Function) => (...args: any[]) => fn(args); - return lodash; -}); - it('searches', () => { const push = jest.fn(); const wrapper = shallow(, { context: { router: { push } } }); expect(wrapper).toMatchSnapshot(); - wrapper.prop('handleSearch')('foo'); + wrapper.find('SearchBox').prop('onChange')('foo'); expect(push).toBeCalledWith({ pathname: '/projects', query: { search: 'foo' } }); }); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilter-test.tsx.snap deleted file mode 100644 index 86e0c761aa1..00000000000 --- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilter-test.tsx.snap +++ /dev/null @@ -1,54 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should display a help message when there is less than 2 characters 1`] = ` -
    - - - select2.tooShort.2 - -
    -`; - -exports[`should display a help message when there is less than 2 characters 2`] = ` -
    - -
    -`; - -exports[`should render correctly without any search query 1`] = ` -
    - -
    -`; - -exports[`should render with a search query 1`] = ` -
    - -
    -`; diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilterContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilterContainer-test.tsx.snap index d3d2ac3eb74..870a9ad499a 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilterContainer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilterContainer-test.tsx.snap @@ -1,8 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`searches 1`] = ` - +
    + +
    `; diff --git a/server/sonar-web/src/main/js/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css index 84756987169..1bd7e286548 100644 --- a/server/sonar-web/src/main/js/apps/projects/styles.css +++ b/server/sonar-web/src/main/js/apps/projects/styles.css @@ -41,19 +41,7 @@ .projects-topbar-item-search { position: relative; flex: 1; -} - -.projects-topbar-item-search input { - width: 100%; - max-width: 300px; -} - -.projects-topbar-item-search .note { - position: absolute; - top: 1px; - left: 80px; - line-height: var(--controlHeight); - pointer-events: none; + height: var(--controlHeight); } .projects-list .page-actions { diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx index 1d799214c95..67c82b0da6a 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx @@ -29,6 +29,7 @@ import QualifierIcon from '../../components/shared/QualifierIcon'; import Tooltip from '../../components/controls/Tooltip'; import DateInput from '../../components/controls/DateInput'; import Select from '../../components/controls/Select'; +import SearchBox from '../../components/controls/SearchBox'; export interface Props { analyzedBefore?: string; @@ -60,16 +61,6 @@ export default class Search extends React.PureComponent { mounted: boolean; state: State = { bulkApplyTemplateModal: false, deleteModal: false }; - onSubmit = (event: React.SyntheticEvent) => { - event.preventDefault(); - this.search(); - }; - - search = (event?: React.SyntheticEvent) => { - const q = event ? event.currentTarget.value : this.input.value; - this.props.onSearch(q); - }; - getQualifierOptions = () => { const options = this.props.topLevelQualifiers.map(q => ({ label: translate('qualifiers', q), @@ -206,19 +197,12 @@ export default class Search extends React.PureComponent { {this.renderDateFilter()} {this.renderTypeFilter()} -
    - - (this.input = node!)} - className="search-box-input input-medium" - type="search" - placeholder={translate('search_verb')} - /> - + - - + -
    - - - + void, - className?: string -}; -*/ - -/*:: -type State = { - query?: string -}; -*/ - -export default class UsersSearch extends React.PureComponent { - /*:: props: Props; */ - /*:: state: State; */ - - constructor(props /*: Props */) { - super(props); - this.state = { - query: '' - }; - this.handleSearch = debounce(this.handleSearch, 250); - } - - handleSearch = (query /*: string */) => { - this.props.onSearch(query); - }; - - handleInputChange = ({ target } /*: { target: HTMLInputElement } */) => { - this.setState({ query: target.value }); - if (!target.value || target.value.length >= 2) { - this.handleSearch(target.value); - } - }; - - render() { - const { query } = this.state; - const searchBoxClass = classNames('search-box', this.props.className); - const inputClassName = classNames('search-box-input', { - touched: query != null && query.length === 1 - }); - return ( -
    - - - - {translateWithParameters('select2.tooShort', 2)} - -
    - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserSearch-test.js b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserSearch-test.js deleted file mode 100644 index 71fd3cc465e..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserSearch-test.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; -import { shallow } from 'enzyme'; -import UsersSearch from '../UsersSearch'; - -it('should render correctly', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - wrapper.setState({ query: 'foo' }); - expect(wrapper).toMatchSnapshot(); -}); - -it('should display a help message when there is less than 2 characters', () => { - const wrapper = shallow(); - wrapper.setState({ query: 'f' }); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserSearch-test.js.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserSearch-test.js.snap deleted file mode 100644 index de9d0313c54..00000000000 --- a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserSearch-test.js.snap +++ /dev/null @@ -1,82 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should display a help message when there is less than 2 characters 1`] = ` -
    - - - - select2.tooShort.2 - -
    -`; - -exports[`should render correctly 1`] = ` -
    - - - - select2.tooShort.2 - -
    -`; - -exports[`should render correctly 2`] = ` -
    - - - - select2.tooShort.2 - -
    -`; diff --git a/server/sonar-web/src/main/js/apps/users/search-view.js b/server/sonar-web/src/main/js/apps/users/search-view.js index a0f52d20cac..f7afc9e81aa 100644 --- a/server/sonar-web/src/main/js/apps/users/search-view.js +++ b/server/sonar-web/src/main/js/apps/users/search-view.js @@ -25,13 +25,15 @@ export default Marionette.ItemView.extend({ template: Template, ui: { - hint: '.js-hint' + hint: '.js-hint', + reset: '.js-reset' }, events: { 'submit #users-search-form': 'onFormSubmit', 'search #users-search-query': 'initialOnKeyUp', - 'keyup #users-search-query': 'initialOnKeyUp' + 'keyup #users-search-query': 'initialOnKeyUp', + 'click .js-reset': 'onResetClick' }, initialize() { @@ -51,6 +53,7 @@ export default Marionette.ItemView.extend({ initialOnKeyUp() { const q = this.getQuery(); this.ui.hint.toggleClass('hidden', q.length !== 1); + this.ui.reset.toggleClass('hidden', q.length === 0); this.debouncedOnKeyUp(); }, @@ -64,6 +67,7 @@ export default Marionette.ItemView.extend({ this.searchRequest.abort(); } this.ui.hint.toggleClass('hidden', q.length !== 1); + this.ui.reset.toggleClass('hidden', q.length === 0); if (q.length !== 1) { this.searchRequest = this.search(q); } @@ -75,5 +79,14 @@ export default Marionette.ItemView.extend({ search(q) { return this.collection.fetch({ reset: true, data: { q } }); + }, + + onResetClick(e) { + e.preventDefault(); + e.currentTarget.blur(); + this.$('#users-search-query') + .val('') + .focus(); + this.onKeyUp(); } }); diff --git a/server/sonar-web/src/main/js/apps/users/templates/users-search.hbs b/server/sonar-web/src/main/js/apps/users/templates/users-search.hbs index 4b879a050ba..a95ed5eb718 100644 --- a/server/sonar-web/src/main/js/apps/users/templates/users-search.hbs +++ b/server/sonar-web/src/main/js/apps/users/templates/users-search.hbs @@ -1,8 +1,17 @@