} 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 {
}
};
- handleSearchChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
- this.setState({ query: event.currentTarget.value, selected: null });
+ handleSearchChange = (query: string) => this.setState({ query, selected: null });
handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
switch (event.keyCode) {
event.preventDefault();
this.openSelected();
return;
- case 27:
- event.preventDefault();
- this.props.onClose();
- return;
case 38:
event.preventDefault();
this.selectPrevious();
isSelected = (branch: Branch) => branch.name === this.getSelected();
renderSearch = () => (
- <div className="search-box menu-search">
- <button className="search-box-submit button-clean">
- <i className="icon-search-new" />
- </button>
- <input
+ <div className="menu-search">
+ <SearchBox
autoFocus={true}
- className="search-box-input"
onChange={this.handleSearchChange}
onKeyDown={this.handleKeyDown}
- placeholder={translate('search_verb')}
- type="search"
+ placeholder={translate('branches.search_for_branches')}
value={this.state.query}
/>
</div>
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');
});
className="dropdown-menu"
>
<div
- className="search-box menu-search"
+ className="menu-search"
>
- <button
- className="search-box-submit button-clean"
- >
- <i
- className="icon-search-new"
- />
- </button>
- <input
+ <SearchBox
autoFocus={true}
- className="search-box-input"
onChange={[Function]}
onKeyDown={[Function]}
- placeholder="search_verb"
- type="search"
+ placeholder="branches.search_for_branches"
value=""
/>
</div>
className="dropdown-menu"
>
<div
- className="search-box menu-search"
+ className="menu-search"
>
- <button
- className="search-box-submit button-clean"
- >
- <i
- className="icon-search-new"
- />
- </button>
- <input
+ <SearchBox
autoFocus={true}
- className="search-box-input"
onChange={[Function]}
onKeyDown={[Function]}
- placeholder="search_verb"
- type="search"
+ placeholder="branches.search_for_branches"
value="bar"
/>
</div>
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;
.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);
}
}
.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;
}
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';
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';
*/
export default class Search extends React.PureComponent {
- /*:: input: HTMLElement; */
+ /*:: input: HTMLInputElement | null; */
/*:: mounted: boolean; */
/*:: node: HTMLElement; */
/*:: nodes: { [string]: HTMLElement };
componentDidMount() {
this.mounted = true;
key('s', () => {
- this.input.focus();
+ if (this.input) {
+ this.input.focus();
+ }
this.openSearch();
return false;
});
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 });
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 });
}
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);
};
event.preventDefault();
this.openSelected();
return;
- case 27:
- event.preventDefault();
- this.closeSearch();
- return;
case 38:
event.preventDefault();
this.selectPrevious();
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 */) => (
<SearchResult
appState={this.props.appState}
return (
<li className={dropdownClassName}>
- <DeferredSpinner className="navbar-search-icon" loading={this.state.loading}>
- <i className="navbar-search-icon icon-search" />
- </DeferredSpinner>
-
- <input
- autoComplete="off"
- className="navbar-search-input js-search-input"
- maxLength="30"
- name="q"
+ <DeferredSpinner className="navbar-search-icon" loading={this.state.loading} />
+
+ <SearchBox
+ innerRef={this.searchInputRef}
+ minLength={2}
onChange={this.handleQueryChange}
- onClick={event => 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 && (
- <span
- className={classNames('navbar-search-input-hint', {
- 'is-shifted': this.state.query.length > 5
- })}>
+ <span className={classNames('navbar-search-input-hint')}>
{translateWithParameters('select2.tooShort', 2)}
</span>
)}
<ClockIcon className="little-spacer-right" size={12} />
{translate('recently_browsed')}
</div>
- <div
- dangerouslySetInnerHTML={{
- __html: translateWithParameters(
- 'search.shortcut_hint',
- '<span class="shortcut-button shortcut-button-small">s</span>'
- )
+ <FormattedMessage
+ defaultMessage={translate('search.shortcut_hint')}
+ id="search.shortcut_hint"
+ values={{
+ shortcut: <span className="shortcut-button shortcut-button-small">s</span>
}}
/>
</div>
}
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);
}
});
const openSelected = jest.fn();
form.instance().openSelected = openSelected;
- elementKeydown(form.find('input'), 13);
+ elementKeydown(form.find('SearchBox'), 13);
expect(openSelected).toBeCalled();
});
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(
<Search appState={{ organizationsEnabled: false }} currentUser={{ isLoggedIn: false }} />
exports[`shows warning about short input 2`] = `
<span
- className="navbar-search-input-hint is-shifted"
+ className="navbar-search-input-hint"
>
select2.tooShort.2
</span>
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,
}
.search-navigator-facet-query {
- padding: 7px 10px 27px;
-}
-
-.search-navigator-facet-query input {
- width: 100%;
+ padding: 7px 0 27px;
}
.search-navigator-facet-custom-value {
+++ /dev/null
-/*
- * 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;
-}
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);
@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';
it('should render search form', () => {
const component = shallow(<Search {...defaultProps} />);
- expect(component.find('.js-search').length).toBe(1);
+ expect(component.find('SearchBox').exists()).toBeTruthy();
});
it('should not render search form', () => {
const component = shallow(<Search {...defaultProps} component={{ id: 'ABCD' }} />);
- 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(<Search {...defaultProps} onFilterUpdate={searchSpy} />);
- 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();
}
.bt-search-form > li + li {
- margin-left: 40px;
+ margin-left: 16px;
}
.bt-search-form-label {
padding: 4px 0;
}
-.bt-search-form-right {
- margin-left: auto !important;
+.bt-search-form-large {
+ flex: 1;
}
.bt-workers-warning-icon {
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 {
this.props.onFilterUpdate(date);
}
- handleQueryChange(query /*: string */) {
+ handleQueryChange = (query /*: string */) => {
this.props.onFilterUpdate({ query });
- }
+ };
handleReload(e /*: Object */) {
e.target.blur();
}
return (
- <li>
- <h6 className="bt-search-form-label">
- {translate('background_tasks.search_by_task_or_component')}
- </h6>
-
- <input
- onChange={e => this.handleQueryChange(e.target.value)}
+ <li className="bt-search-form-large">
+ <SearchBox
+ onChange={this.handleQueryChange}
+ placeholder={translate('background_tasks.search_by_task_or_component')}
value={query}
- ref="searchInput"
- className="js-search input-medium"
- type="search"
- placeholder={translate('search_verb')}
/>
</li>
);
{this.renderSearchBox()}
- <li className="bt-search-form-right nowrap">
+ <li className="nowrap">
<button className="js-reload" onClick={this.handleReload.bind(this)} disabled={loading}>
{translate('reload')}
</button>{' '}
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;
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;
}
export default class Search extends React.PureComponent<Props, State> {
- input: HTMLInputElement;
mounted: boolean;
static contextTypes = {
loading: false
};
- componentWillMount() {
- this.handleSearch = debounce(this.handleSearch, 250);
- }
-
componentDidMount() {
this.mounted = true;
}
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) {
}
}
- handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
- switch (e.keyCode) {
+ handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
+ 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 });
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,
}
})
.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);
}
}
};
- 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<HTMLInputElement>) {
- const query = event.currentTarget.value;
- this.handleQueryChange(query);
- }
-
- handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) {
- 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 (
<div id="code-search" className={containerClassName}>
- <form className="search-box" onSubmit={this.handleSubmit.bind(this)}>
- <button className="search-box-submit button-clean">
- <i className="icon-search" />
- </button>
-
- <input
- ref={node => (this.input = node as HTMLInputElement)}
- onKeyDown={this.handleKeyDown.bind(this)}
- onChange={this.handleInputChange.bind(this)}
- value={query}
- className={inputClassName}
- type="search"
- name="q"
- placeholder={translate('search_verb')}
- maxLength={100}
- autoComplete="off"
- />
-
- {loading && <i className="spinner spacer-left" />}
-
- <span className="note spacer-left">{translateWithParameters('select2.tooShort', 3)}</span>
- </form>
+ <SearchBox
+ minLength={3}
+ onChange={this.handleQueryChange}
+ onKeyDown={this.handleKeyDown}
+ placeholder={translate(
+ isPortfolio ? 'code.search_placeholder.portfolio' : 'code.search_placeholder'
+ )}
+ value={this.state.query}
+ />
+ {loading && <i className="spinner spacer-left" />}
{results != null && (
<Components
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { debounce } from 'lodash';
import BaseFacet from './base-facet';
import Template from '../templates/facets/coding-rules-query-facet.hbs';
export default BaseFacet.extend({
template: Template,
- events() {
+ events(...args) {
return {
- ...BaseFacet.prototype.events.apply(this, arguments),
+ ...BaseFacet.prototype.events.apply(this, args),
'submit form': 'onFormSubmit',
- 'search input': 'onInputSearch'
+ 'keyup input': 'onKeyUp',
+ 'search input': 'onSearch',
+ 'click .js-reset': 'onResetClick'
};
},
const value = query.q;
if (value != null) {
this.$('input').val(value);
+ this.$('.js-hint').toggleClass('hidden', value.length !== 1);
+ this.$('.js-reset').toggleClass('hidden', value.length === 0);
}
},
this.applyFacet();
},
- onInputSearch() {
- this.applyFacet();
+ onKeyUp() {
+ const q = this.$('input').val();
+ this.$('.js-hint').toggleClass('hidden', q.length !== 1);
+ this.$('.js-reset').toggleClass('hidden', q.length === 0);
+ },
+
+ onSearch() {
+ const q = this.$('input').val();
+ if (q.length !== 1) {
+ this.applyFacet();
+ }
+ },
+
+ onResetClick(e) {
+ e.preventDefault();
+ this.$('input')
+ .val('')
+ .focus();
},
applyFacet() {
<div class="search-navigator-facet-query">
- <form>
- <input type="search" class="search-navigator-facet-input" name="q" placeholder="{{t 'search_verb'}}">
+ <form class="search-box">
+ <input class="search-box-input" type="text" name="q" placeholder="{{t 'search.search_for_rules'}}" maxlength="100">
+ <svg class="search-box-magnifier" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
+ <g transform="matrix(0.0288462,0,0,0.0288462,2,1.07692)">
+ <path d="M288,208C288,177.167 277.042,150.792 255.125,128.875C233.208,106.958 206.833,96 176,96C145.167,96 118.792,106.958 96.875,128.875C74.958,150.792 64,177.167 64,208C64,238.833 74.958,265.208 96.875,287.125C118.792,309.042 145.167,320 176,320C206.833,320 233.208,309.042 255.125,287.125C277.042,265.208 288,238.833 288,208ZM416,416C416,424.667 412.833,432.167 406.5,438.5C400.167,444.833 392.667,448 384,448C375,448 367.5,444.833 361.5,438.5L275.75,353C245.917,373.667 212.667,384 176,384C152.167,384 129.375,379.375 107.625,370.125C85.875,360.875 67.125,348.375 51.375,332.625C35.625,316.875 23.125,298.125 13.875,276.375C4.625,254.625 0,231.833 0,208C0,184.167 4.625,161.375 13.875,139.625C23.125,117.875 35.625,99.125 51.375,83.375C67.125,67.625 85.875,55.125 107.625,45.875C129.375,36.625 152.167,32 176,32C199.833,32 222.625,36.625 244.375,45.875C266.125,55.125 284.875,67.625 300.625,83.375C316.375,99.125 328.875,117.875 338.125,139.625C347.375,161.375 352,184.167 352,208C352,244.667 341.667,277.917 321,307.75L406.75,393.5C412.917,399.667 416,407.167 416,416Z" style="fill:currentColor;fill-rule:nonzero;"/>
+ </g>
+ </svg>
+ <button class="js-reset hidden button-tiny search-box-clear button-icon" style="color: rgb(153, 153, 153);" type="reset">
+ <svg width="12" height="12" viewBox="0 0 16 16" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
+ <path d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z" style="fill: currentcolor;"/>
+ </svg>
+ </button>
+ <span class="js-hint search-box-note hidden" title="{{tp 'select2.tooShort' 2}}">
+ {{tp 'select2.tooShort' 2}}
+ </span>
</form>
</div>
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() {
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() {
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) {
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();
}
});
<div class="panel panel-vertical bordered-bottom spacer-bottom">
<form id="groups-search-form" class="search-box">
- <button id="groups-search-submit" class="search-box-submit button-clean"><i class="icon-search"></i></button>
- <input id="groups-search-query" class="search-box-input" type="search" name="q" placeholder="{{t 'search_verb'}}" maxlength="100">
+ <input id="groups-search-query" class="search-box-input" type="text" name="q" placeholder="{{t 'search.search_by_name'}}" maxlength="100">
+ <svg class="search-box-magnifier" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
+ <g transform="matrix(0.0288462,0,0,0.0288462,2,1.07692)">
+ <path d="M288,208C288,177.167 277.042,150.792 255.125,128.875C233.208,106.958 206.833,96 176,96C145.167,96 118.792,106.958 96.875,128.875C74.958,150.792 64,177.167 64,208C64,238.833 74.958,265.208 96.875,287.125C118.792,309.042 145.167,320 176,320C206.833,320 233.208,309.042 255.125,287.125C277.042,265.208 288,238.833 288,208ZM416,416C416,424.667 412.833,432.167 406.5,438.5C400.167,444.833 392.667,448 384,448C375,448 367.5,444.833 361.5,438.5L275.75,353C245.917,373.667 212.667,384 176,384C152.167,384 129.375,379.375 107.625,370.125C85.875,360.875 67.125,348.375 51.375,332.625C35.625,316.875 23.125,298.125 13.875,276.375C4.625,254.625 0,231.833 0,208C0,184.167 4.625,161.375 13.875,139.625C23.125,117.875 35.625,99.125 51.375,83.375C67.125,67.625 85.875,55.125 107.625,45.875C129.375,36.625 152.167,32 176,32C199.833,32 222.625,36.625 244.375,45.875C266.125,55.125 284.875,67.625 300.625,83.375C316.375,99.125 328.875,117.875 338.125,139.625C347.375,161.375 352,184.167 352,208C352,244.667 341.667,277.917 321,307.75L406.75,393.5C412.917,399.667 416,407.167 416,416Z" style="fill:currentColor;fill-rule:nonzero;"/>
+ </g>
+ </svg>
+ <button class="js-reset hidden button-tiny search-box-clear button-icon" style="color: rgb(153, 153, 153);" type="reset">
+ <svg width="12" height="12" viewBox="0 0 16 16" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
+ <path d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z" style="fill: currentcolor;"/>
+ </svg>
+ </button>
</form>
</div>
noResultsText={translate('select2.noMatches')}
onChange={this.handleChange}
options={options}
- placeholder={translate('search_verb')}
+ placeholder={translate('search.search_for_languages')}
searchable={true}
/>
</div>
* 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 {
updateQuery: (newQuery: Partial<Query>) => void;
}
-interface State {
- search?: string;
-}
-
-export default class Search extends React.PureComponent<Props, State> {
- 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<HTMLInputElement>) => {
- const search = e.currentTarget.value;
- this.setState({ search });
- this.updateSearch(search);
+export default class Search extends React.PureComponent<Props> {
+ 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 = [
value={query.filter}
/>
</div>
- <div className="search-box display-inline-block text-top">
- <button className="search-box-submit button-clean">
- <i className="icon-search" />
- </button>
- <input
- onChange={this.handleSearch}
- value={this.state.search}
- className="search-box-input"
- type="search"
- name="search"
- placeholder={translate('search_verb')}
- maxLength={100}
- autoComplete="off"
- />
- </div>
+ <SearchBox
+ onChange={this.handleSearch}
+ placeholder={translate('marketplace.search')}
+ value={query.search}
+ />
</div>
);
}
*/
//@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';
};
*/
-export default class MembersListHeader extends React.PureComponent {
- /*:: props: Props; */
-
- render() {
- const { total } = this.props;
- return (
- <div className="panel panel-vertical bordered-bottom spacer-bottom">
- <UsersSearch onSearch={this.props.handleSearch} className="display-inline-block" />
- {total != null && (
- <span className="pull-right little-spacer-top">
- <strong>{formatMeasure(total, 'INT')}</strong>{' '}
- {translate('organization.members.members')}
- </span>
- )}
- </div>
- );
- }
+export default function MembersListHeader({ handleSearch, total } /*: Props */) {
+ return (
+ <div className="panel panel-vertical bordered-bottom spacer-bottom">
+ <SearchBox
+ minLength={2}
+ onChange={handleSearch}
+ placeholder={translate('search.search_for_users')}
+ />
+ {total != null && (
+ <span className="pull-right little-spacer-top">
+ <strong>{formatMeasure(total, 'INT')}</strong> {translate('organization.members.members')}
+ </span>
+ )}
+ </div>
+ );
}
<div
className="panel panel-vertical bordered-bottom spacer-bottom"
>
- <UsersSearch
- className="display-inline-block"
- onSearch={[Function]}
+ <SearchBox
+ minLength={2}
+ onChange={[Function]}
+ placeholder="search.search_for_users"
/>
<span
className="pull-right little-spacer-top"
<div
className="panel panel-vertical bordered-bottom spacer-bottom"
>
- <UsersSearch
- className="display-inline-block"
- onSearch={[Function]}
+ <SearchBox
+ minLength={2}
+ onChange={[Function]}
+ placeholder="search.search_for_users"
/>
</div>
`;
*/
//@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';
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('');
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 });
});
};
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;
handleSearch = query => {
this.setState({ query });
- if (query.length === 0 || query.length > 2) {
- this.requestHoldersDebounced(query);
- }
+ this.requestHolders(query);
};
handleFilter = filter => {
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 */) => (
handleQueryChange = (query /*: string */) => {
if (this.mounted) {
- this.setState({ query }, () => {
- if (query.length === 0 || query.length > 2) {
- this.loadHolders();
- }
- });
+ this.setState({ query }, this.loadHolders);
}
};
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 (
- <div>
- <RadioToggle
- value={filter}
- options={filterOptions}
- name="users-or-groups"
- onCheck={this.handleFilter.bind(this)}
+export default function SearchForm(props) {
+ const filterOptions = [
+ { value: 'all', label: translate('all') },
+ { value: 'users', label: translate('users.page') },
+ { value: 'groups', label: translate('user_groups.page') }
+ ];
+
+ return (
+ <div className="diplay-flex-row">
+ <RadioToggle
+ name="users-or-groups"
+ onCheck={props.onFilter}
+ options={filterOptions}
+ value={props.filter}
+ />
+
+ <div className="flex-1 spacer-left">
+ <SearchBox
+ minLength={3}
+ onChange={props.onSearch}
+ placeholder={translate('search.search_for_users_or_groups')}
+ value={props.query}
/>
-
- <form
- className="search-box display-inline-block text-middle big-spacer-left"
- onSubmit={this.handleSubmit}>
- <button className="search-box-submit button-clean">
- <i className="icon-search" />
- </button>
- <input
- ref="searchInput"
- value={query}
- className="search-box-input"
- style={{ width: 100 }}
- type="search"
- placeholder={translate('search_verb')}
- onChange={this.handleSearch.bind(this)}
- />
- {query.length > 0 &&
- query.length < 3 && (
- <div className="search-box-input-note tooltip bottom fade in">
- <div className="tooltip-inner">
- {translateWithParameters('select2.tooShort', 3)}
- </div>
- <div className="tooltip-arrow" style={{ left: 23 }} />
- </div>
- )}
- </form>
</div>
- );
- }
+ </div>
+ );
}
)}
<SearchFilterContainer
- className="projects-topbar-item projects-topbar-item-search"
isFavorite={props.isFavorite}
organization={props.organization}
query={props.query}
view="overall"
/>
<SearchFilterContainer
- className="projects-topbar-item projects-topbar-item-search"
query={
Object {
"search": "test",
view="overall"
/>
<SearchFilterContainer
- className="projects-topbar-item projects-topbar-item-search"
query={
Object {
"search": "test",
</div>
</Tooltip>
<SearchFilterContainer
- className="projects-topbar-item projects-topbar-item-search"
query={
Object {
"search": "test",
+++ /dev/null
-/*
- * 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 { translate, translateWithParameters } from '../../../helpers/l10n';
-
-interface Props {
- className?: string;
- handleSearch: (userString?: string) => void;
- query: { search?: string | undefined };
-}
-
-interface State {
- userQuery?: string;
-}
-
-export default class SearchFilter extends React.PureComponent<Props, State> {
- 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<HTMLInputElement>) => {
- 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 (
- <div className={this.props.className}>
- <input
- type="search"
- value={userQuery || ''}
- placeholder={translate('projects.search')}
- onChange={this.handleQueryChange}
- autoComplete="off"
- />
- {shortQuery && (
- <span className="note spacer-left">{translateWithParameters('select2.tooShort', 2)}</span>
- )}
- </div>
- );
- }
-}
*/
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;
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);
render() {
return (
- <SearchFilter
- className={this.props.className}
- query={this.props.query}
- handleSearch={this.handleSearch}
- />
+ <div className="projects-topbar-item projects-topbar-item-search">
+ <SearchBox
+ minLength={2}
+ onChange={this.handleSearch}
+ placeholder={translate('projects.search')}
+ />
+ </div>
);
}
}
+++ /dev/null
-/*
- * 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(<SearchFilter handleSearch={jest.fn()} query={{}} />);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should render with a search query', () => {
- const wrapper = shallow(<SearchFilter handleSearch={jest.fn()} query={{ search: 'foo' }} />);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('should display a help message when there is less than 2 characters', () => {
- const wrapper = shallow(<SearchFilter handleSearch={jest.fn()} query={{ search: 'a' }} />);
- expect(wrapper).toMatchSnapshot();
- wrapper.setState({ userQuery: 'foo' });
- expect(wrapper).toMatchSnapshot();
-});
-
-it('searches', () => {
- const handleSearch = jest.fn();
- const wrapper = shallow(<SearchFilter handleSearch={handleSearch} query={{}} />);
-
- 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(<SearchFilter handleSearch={jest.fn()} query={{ search: 'abc' }} />);
- expect(wrapper.state()).toEqual({ userQuery: 'abc' });
- wrapper.setProps({ query: { search: 'def' } });
- expect(wrapper.state()).toEqual({ userQuery: 'def' });
-});
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(<SearchFilterContainer query={{}} />, { context: { router: { push } } });
expect(wrapper).toMatchSnapshot();
- wrapper.prop('handleSearch')('foo');
+ wrapper.find('SearchBox').prop<Function>('onChange')('foo');
expect(push).toBeCalledWith({ pathname: '/projects', query: { search: 'foo' } });
});
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display a help message when there is less than 2 characters 1`] = `
-<div>
- <input
- autoComplete="off"
- onChange={[Function]}
- placeholder="projects.search"
- type="search"
- value="a"
- />
- <span
- className="note spacer-left"
- >
- select2.tooShort.2
- </span>
-</div>
-`;
-
-exports[`should display a help message when there is less than 2 characters 2`] = `
-<div>
- <input
- autoComplete="off"
- onChange={[Function]}
- placeholder="projects.search"
- type="search"
- value="foo"
- />
-</div>
-`;
-
-exports[`should render correctly without any search query 1`] = `
-<div>
- <input
- autoComplete="off"
- onChange={[Function]}
- placeholder="projects.search"
- type="search"
- value=""
- />
-</div>
-`;
-
-exports[`should render with a search query 1`] = `
-<div>
- <input
- autoComplete="off"
- onChange={[Function]}
- placeholder="projects.search"
- type="search"
- value="foo"
- />
-</div>
-`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`searches 1`] = `
-<SearchFilter
- handleSearch={[Function]}
- query={Object {}}
-/>
+<div
+ className="projects-topbar-item projects-topbar-item-search"
+>
+ <SearchBox
+ minLength={2}
+ onChange={[Function]}
+ placeholder="projects.search"
+ />
+</div>
`;
.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 {
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;
mounted: boolean;
state: State = { bulkApplyTemplateModal: false, deleteModal: false };
- onSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
- event.preventDefault();
- this.search();
- };
-
- search = (event?: React.SyntheticEvent<HTMLInputElement>) => {
- 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),
{this.renderDateFilter()}
{this.renderTypeFilter()}
<td className="text-middle">
- <form onSubmit={this.onSubmit} className="search-box">
- <button className="search-box-submit button-clean">
- <i className="icon-search" />
- </button>
- <input
- onChange={this.search}
- value={this.props.query}
- ref={node => (this.input = node!)}
- className="search-box-input input-medium"
- type="search"
- placeholder={translate('search_verb')}
- />
- </form>
+ <SearchBox
+ minLength={3}
+ onChange={this.props.onSearch}
+ placeholder={translate('search.search_by_name_or_key')}
+ value={this.props.query}
+ />
</td>
<td className="thin nowrap text-middle">
<button
import * as React from 'react';
import { shallow } from 'enzyme';
import Search, { Props } from '../Search';
-import { change, click } from '../../../helpers/testUtils';
+import { click } from '../../../helpers/testUtils';
const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
it('searches', () => {
const onSearch = jest.fn();
const wrapper = shallowRender({ onSearch });
- change(wrapper.find('input[type="search"]'), 'foo');
+ wrapper.find('SearchBox').prop<Function>('onChange')('foo');
expect(onSearch).toBeCalledWith('foo');
});
<td
className="text-middle"
>
- <form
- className="search-box"
- onSubmit={[Function]}
- >
- <button
- className="search-box-submit button-clean"
- >
- <i
- className="icon-search"
- />
- </button>
- <input
- className="search-box-input input-medium"
- onChange={[Function]}
- placeholder="search_verb"
- type="search"
- value=""
- />
- </form>
+ <SearchBox
+ minLength={3}
+ onChange={[Function]}
+ placeholder="search.search_by_name_or_key"
+ value=""
+ />
</td>
<td
className="thin nowrap text-middle"
<td
className="text-middle"
>
- <form
- className="search-box"
- onSubmit={[Function]}
- >
- <button
- className="search-box-submit button-clean"
- >
- <i
- className="icon-search"
- />
- </button>
- <input
- className="search-box-input input-medium"
- onChange={[Function]}
- placeholder="search_verb"
- type="search"
- value=""
- />
- </form>
+ <SearchBox
+ minLength={3}
+ onChange={[Function]}
+ placeholder="search.search_by_name_or_key"
+ value=""
+ />
</td>
<td
className="thin nowrap text-middle"
import { translate } from '../../../helpers/l10n';
// import styles to have the `.button-icon` styles
import '../../../components/ui/buttons.css';
+import '../../../components/controls/SearchBox.css';
class UsersAppContainer extends React.PureComponent {
static propTypes = {
+++ /dev/null
-/*
- * 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';
-import classNames from 'classnames';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-
-/*::
-type Props = {
- onSearch: (query?: string) => 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 (
- <div className={searchBoxClass}>
- <button className="search-box-submit button-clean">
- <i className="icon-search" />
- </button>
- <input
- type="search"
- value={query}
- className={inputClassName}
- placeholder={translate('search_verb')}
- onChange={this.handleInputChange}
- autoComplete="off"
- />
- <span className="note spacer-left text-middle">
- {translateWithParameters('select2.tooShort', 2)}
- </span>
- </div>
- );
- }
-}
+++ /dev/null
-/*
- * 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(<UsersSearch onSearch={jest.fn()} className="test" />);
- 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(<UsersSearch onSearch={jest.fn()} />);
- wrapper.setState({ query: 'f' });
- expect(wrapper).toMatchSnapshot();
-});
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display a help message when there is less than 2 characters 1`] = `
-<div
- className="search-box"
->
- <button
- className="search-box-submit button-clean"
- >
- <i
- className="icon-search"
- />
- </button>
- <input
- autoComplete="off"
- className="search-box-input touched"
- onChange={[Function]}
- placeholder="search_verb"
- type="search"
- value="f"
- />
- <span
- className="note spacer-left text-middle"
- >
- select2.tooShort.2
- </span>
-</div>
-`;
-
-exports[`should render correctly 1`] = `
-<div
- className="search-box test"
->
- <button
- className="search-box-submit button-clean"
- >
- <i
- className="icon-search"
- />
- </button>
- <input
- autoComplete="off"
- className="search-box-input"
- onChange={[Function]}
- placeholder="search_verb"
- type="search"
- value=""
- />
- <span
- className="note spacer-left text-middle"
- >
- select2.tooShort.2
- </span>
-</div>
-`;
-
-exports[`should render correctly 2`] = `
-<div
- className="search-box test"
->
- <button
- className="search-box-submit button-clean"
- >
- <i
- className="icon-search"
- />
- </button>
- <input
- autoComplete="off"
- className="search-box-input"
- onChange={[Function]}
- placeholder="search_verb"
- type="search"
- value="foo"
- />
- <span
- className="note spacer-left text-middle"
- >
- select2.tooShort.2
- </span>
-</div>
-`;
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() {
initialOnKeyUp() {
const q = this.getQuery();
this.ui.hint.toggleClass('hidden', q.length !== 1);
+ this.ui.reset.toggleClass('hidden', q.length === 0);
this.debouncedOnKeyUp();
},
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);
}
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();
}
});
<div class="panel panel-vertical bordered-bottom spacer-bottom">
<form id="users-search-form" class="search-box">
- <button id="users-search-submit" class="search-box-submit button-clean"><i class="icon-search"></i></button>
- <input id="users-search-query" class="search-box-input" type="search" name="q" placeholder="{{t 'search_verb'}}" maxlength="100">
- <span class="js-hint note spacer-left text-middle hidden">
+ <input id="users-search-query" class="search-box-input" type="text" name="q" placeholder="{{t 'search.search_by_login_or_name'}}" maxlength="100">
+ <svg class="search-box-magnifier" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
+ <g transform="matrix(0.0288462,0,0,0.0288462,2,1.07692)">
+ <path d="M288,208C288,177.167 277.042,150.792 255.125,128.875C233.208,106.958 206.833,96 176,96C145.167,96 118.792,106.958 96.875,128.875C74.958,150.792 64,177.167 64,208C64,238.833 74.958,265.208 96.875,287.125C118.792,309.042 145.167,320 176,320C206.833,320 233.208,309.042 255.125,287.125C277.042,265.208 288,238.833 288,208ZM416,416C416,424.667 412.833,432.167 406.5,438.5C400.167,444.833 392.667,448 384,448C375,448 367.5,444.833 361.5,438.5L275.75,353C245.917,373.667 212.667,384 176,384C152.167,384 129.375,379.375 107.625,370.125C85.875,360.875 67.125,348.375 51.375,332.625C35.625,316.875 23.125,298.125 13.875,276.375C4.625,254.625 0,231.833 0,208C0,184.167 4.625,161.375 13.875,139.625C23.125,117.875 35.625,99.125 51.375,83.375C67.125,67.625 85.875,55.125 107.625,45.875C129.375,36.625 152.167,32 176,32C199.833,32 222.625,36.625 244.375,45.875C266.125,55.125 284.875,67.625 300.625,83.375C316.375,99.125 328.875,117.875 338.125,139.625C347.375,161.375 352,184.167 352,208C352,244.667 341.667,277.917 321,307.75L406.75,393.5C412.917,399.667 416,407.167 416,416Z" style="fill:currentColor;fill-rule:nonzero;"/>
+ </g>
+ </svg>
+ <button class="js-reset hidden button-tiny search-box-clear button-icon" style="color: rgb(153, 153, 153);" type="reset">
+ <svg width="12" height="12" viewBox="0 0 16 16" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
+ <path d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z" style="fill: currentcolor;"/>
+ </svg>
+ </button>
+ <span class="js-hint search-box-note hidden" title="{{tp 'select2.tooShort' 2}}">
{{tp 'select2.tooShort' 2}}
</span>
</form>
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { debounce } from 'lodash';
import Checkbox from '../../../components/controls/Checkbox';
import HelpIcon from '../../../components/icons-components/HelpIcon';
import Tooltip from '../../../components/controls/Tooltip';
import { translate } from '../../../helpers/l10n';
+import SearchBox from '../../../components/controls/SearchBox';
interface Props {
showDeprecated: boolean;
onToggleDeprecated: () => void;
}
-interface State {
- query: string;
-}
-
-export default class Search extends React.PureComponent<Props, State> {
- constructor(props: Props) {
- super(props);
- this.state = { query: '' };
- this.actuallySearch = debounce(this.actuallySearch, 250);
- }
-
- handleSearch = (e: React.SyntheticEvent<HTMLInputElement>) => {
- this.setState({ query: e.currentTarget.value });
- this.actuallySearch();
- };
-
- actuallySearch = () => this.props.onSearch(this.state.query);
+export default function Search(props: Props) {
+ const { showInternal, showDeprecated, onToggleInternal, onToggleDeprecated } = props;
- render() {
- const { showInternal, showDeprecated, onToggleInternal, onToggleDeprecated } = this.props;
-
- return (
- <div className="web-api-search">
- <div>
- <i className="icon-search" />
- <input
- className="spacer-left input-large"
- type="search"
- value={this.state.query}
- placeholder={translate('search_verb')}
- onChange={this.handleSearch}
- />
- </div>
+ return (
+ <div className="web-api-search">
+ <div>
+ <SearchBox onChange={props.onSearch} placeholder={translate('api_documentation.search')} />
+ </div>
- <div className="big-spacer-top">
- <Checkbox checked={showInternal} onCheck={onToggleInternal}>
- <span className="little-spacer-left">
- {translate('api_documentation.show_internal')}
- </span>
- </Checkbox>
- <Tooltip overlay={translate('api_documentation.internal_tooltip')} placement="right">
- <span>
- <HelpIcon className="spacer-left text-info" />
- </span>
- </Tooltip>
- </div>
+ <div className="big-spacer-top">
+ <Checkbox checked={showInternal} onCheck={onToggleInternal}>
+ <span className="little-spacer-left">{translate('api_documentation.show_internal')}</span>
+ </Checkbox>
+ <Tooltip overlay={translate('api_documentation.internal_tooltip')} placement="right">
+ <span>
+ <HelpIcon className="spacer-left text-info" />
+ </span>
+ </Tooltip>
+ </div>
- <div className="spacer-top">
- <Checkbox checked={showDeprecated} onCheck={onToggleDeprecated}>
- <span className="little-spacer-left">
- {translate('api_documentation.show_deprecated')}
- </span>
- </Checkbox>
- <Tooltip overlay={translate('api_documentation.deprecation_tooltip')} placement="right">
- <span>
- <HelpIcon className="spacer-left text-info" />
- </span>
- </Tooltip>
- </div>
+ <div className="spacer-top">
+ <Checkbox checked={showDeprecated} onCheck={onToggleDeprecated}>
+ <span className="little-spacer-left">
+ {translate('api_documentation.show_deprecated')}
+ </span>
+ </Checkbox>
+ <Tooltip overlay={translate('api_documentation.deprecation_tooltip')} placement="right">
+ <span>
+ <HelpIcon className="spacer-left text-info" />
+ </span>
+ </Tooltip>
</div>
- );
- }
+ </div>
+ );
}
className="web-api-search"
>
<div>
- <i
- className="icon-search"
- />
- <input
- className="spacer-left input-large"
+ <SearchBox
onChange={[Function]}
- placeholder="search_verb"
- type="search"
- value=""
+ placeholder="api_documentation.search"
/>
</div>
<div
white-space: nowrap;
}
-.web-api-search .icon-search {
- color: var(--gray80);
-}
-
.web-api-domain-header,
.web-api-action-header {
display: flex;
import ItemTemplate from './templates/item.hbs';
import ListTemplate from './templates/list.hbs';
import './styles.css';
+import '../controls/SearchBox.css';
let showError = null;
events: {
'click .select-list-control-button[name=selected]': 'showSelected',
'click .select-list-control-button[name=deselected]': 'showDeselected',
- 'click .select-list-control-button[name=all]': 'showAll'
+ 'click .select-list-control-button[name=all]': 'showAll',
+ 'click .js-reset': 'onResetClick'
},
initialize(options) {
this.$('.select-list-check-control').toggleClass('disabled', hasQuery);
this.$('.select-list-search-control').toggleClass('disabled', !hasQuery);
+ this.$('.js-reset').toggleClass('hidden', !hasQuery);
if (hasQuery) {
this.showFetchSpinner();
}
},
+ onResetClick(e) {
+ e.preventDefault();
+ e.currentTarget.blur();
+ this.$('.select-list-search-control input')
+ .val('')
+ .focus()
+ .trigger('search');
+ },
+
searchByQuery(query) {
this.$('.select-list-search-control input').val(query);
this.search();
<a class="select-list-control-button" name="selected">{{this.selected}}</a><a class="select-list-control-button" name="deselected">{{this.deselected}}</a><a class="select-list-control-button" name="all">{{this.all}}</a>
</div>
<div class="select-list-search-control">
- <form class="search-box">
- <span class="search-box-submit button-clean"><i class="icon-search"></i></span>
- <input class="search-box-input" type="search" name="q" placeholder="{{t 'search_verb'}}" maxlength="100" autocomplete="off">
- </form>
+ <div class="search-box">
+ <input class="search-box-input" type="text" name="q" placeholder="{{t 'search_verb'}}" maxlength="100" autocomplete="off">
+ <svg class="search-box-magnifier" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
+ <g transform="matrix(0.0288462,0,0,0.0288462,2,1.07692)">
+ <path d="M288,208C288,177.167 277.042,150.792 255.125,128.875C233.208,106.958 206.833,96 176,96C145.167,96 118.792,106.958 96.875,128.875C74.958,150.792 64,177.167 64,208C64,238.833 74.958,265.208 96.875,287.125C118.792,309.042 145.167,320 176,320C206.833,320 233.208,309.042 255.125,287.125C277.042,265.208 288,238.833 288,208ZM416,416C416,424.667 412.833,432.167 406.5,438.5C400.167,444.833 392.667,448 384,448C375,448 367.5,444.833 361.5,438.5L275.75,353C245.917,373.667 212.667,384 176,384C152.167,384 129.375,379.375 107.625,370.125C85.875,360.875 67.125,348.375 51.375,332.625C35.625,316.875 23.125,298.125 13.875,276.375C4.625,254.625 0,231.833 0,208C0,184.167 4.625,161.375 13.875,139.625C23.125,117.875 35.625,99.125 51.375,83.375C67.125,67.625 85.875,55.125 107.625,45.875C129.375,36.625 152.167,32 176,32C199.833,32 222.625,36.625 244.375,45.875C266.125,55.125 284.875,67.625 300.625,83.375C316.375,99.125 328.875,117.875 338.125,139.625C347.375,161.375 352,184.167 352,208C352,244.667 341.667,277.917 321,307.75L406.75,393.5C412.917,399.667 416,407.167 416,416Z" style="fill:currentColor;fill-rule:nonzero;"/>
+ </g>
+ </svg>
+ <button class="js-reset hidden button-tiny search-box-clear button-icon" style="color: rgb(153, 153, 153);" type="reset">
+ <svg width="12" height="12" viewBox="0 0 16 16" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
+ <path d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z" style="fill: currentcolor;"/>
+ </svg>
+ </button>
+ </div>
</div>
</div>
<div class="select-list-list-container">
import React from 'react';
import { difference } from 'lodash';
import MultiSelectOption from './MultiSelectOption';
+import SearchBox from '../controls/SearchBox';
import { translate } from '../../helpers/l10n';
/*::
onSearch: string => void,
onSelect: string => void,
onUnselect: string => void,
- validateSearchInput: string => string
+ validateSearchInput: string => string,
+ placeholder: string
};
*/
}
};
- handleSearchChange = ({ target } /*: { target: HTMLInputElement } */) => {
- this.onSearchQuery(this.props.validateSearchInput(target.value));
+ handleSearchChange = (value /*: string */) => {
+ this.onSearchQuery(this.props.validateSearchInput(value));
};
handleElementHover = (element /*: string */) => {
return (
<div className="multi-select" ref={div => (this.container = div)}>
- <div className="search-box menu-search">
- <button className="search-box-submit button-clean">
- <i className="icon-search-new" />
- </button>
- <input
- type="search"
- value={query}
- className="search-box-input"
- placeholder={translate('search_verb')}
+ <div className="menu-search">
+ <SearchBox
+ autoFocus={true}
onChange={this.handleSearchChange}
- autoComplete="off"
- ref={input => (this.searchInput = input)}
+ placeholder={this.props.placeholder}
+ value={query}
/>
</div>
<ul className="menu">
elements: [],
onSearch: () => {},
onSelect: () => {},
- onUnselect: () => {}
+ onUnselect: () => {},
+ placeholder: ''
};
const elements = ['foo', 'bar', 'baz'];
className="multi-select"
>
<div
- className="search-box menu-search"
+ className="menu-search"
>
- <button
- className="search-box-submit button-clean"
- >
- <i
- className="icon-search-new"
- />
- </button>
- <input
- autoComplete="off"
- className="search-box-input"
+ <SearchBox
+ autoFocus={true}
onChange={[Function]}
- placeholder="search_verb"
- type="search"
+ placeholder=""
value=""
/>
</div>
className="multi-select"
>
<div
- className="search-box menu-search"
+ className="menu-search"
>
- <button
- className="search-box-submit button-clean"
- >
- <i
- className="icon-search-new"
- />
- </button>
- <input
- autoComplete="off"
- className="search-box-input"
+ <SearchBox
+ autoFocus={true}
onChange={[Function]}
- placeholder="search_verb"
- type="search"
+ placeholder=""
value=""
/>
</div>
className="multi-select"
>
<div
- className="search-box menu-search"
+ className="menu-search"
>
- <button
- className="search-box-submit button-clean"
- >
- <i
- className="icon-search-new"
- />
- </button>
- <input
- autoComplete="off"
- className="search-box-input"
+ <SearchBox
+ autoFocus={true}
onChange={[Function]}
- placeholder="search_verb"
- type="search"
+ placeholder=""
value=""
/>
</div>
className="multi-select"
>
<div
- className="search-box menu-search"
+ className="menu-search"
>
- <button
- className="search-box-submit button-clean"
- >
- <i
- className="icon-search-new"
- />
- </button>
- <input
- autoComplete="off"
- className="search-box-input"
+ <SearchBox
+ autoFocus={true}
onChange={[Function]}
- placeholder="search_verb"
- type="search"
+ placeholder=""
value="test"
/>
</div>
--- /dev/null
+/*
+ * 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;
+ display: inline-block;
+ vertical-align: middle;
+ font-size: 0;
+ white-space: nowrap;
+}
+
+.search-box,
+.search-box-input {
+ width: 100%;
+ max-width: 300px;
+}
+
+.search-box-input {
+ /* for magnifier icon */
+ padding-left: var(--controlHeight) !important;
+ /* for clear button */
+ padding-right: var(--controlHeight) !important;
+ font-size: var(--baseFontSize);
+}
+
+.search-box-input::placeholder {
+ color: var(--secondFontColor);
+ opacity: 1;
+}
+
+.search-box-input::-webkit-search-decoration,
+.search-box-input::-webkit-search-cancel-button,
+.search-box-input::-webkit-search-results-button,
+.search-box-input::-webkit-search-results-decoration {
+ -webkit-appearance: none;
+ display: none;
+}
+
+.search-box-input::-ms-clear,
+.search-box-input::-ms-reveal {
+ display: none;
+ width: 0;
+ height: 0;
+}
+
+.search-box-note {
+ position: absolute;
+ top: 1px;
+ left: 40px;
+ right: var(--controlHeight);
+ line-height: calc(var(--controlHeight));
+ color: var(--secondFontColor);
+ font-size: var(--smallFontSize);
+ text-align: right;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.search-box-input:focus ~ .search-box-magnifier {
+ color: var(--blue);
+}
+
+.search-box-magnifier {
+ position: absolute;
+ top: 4px;
+ left: 4px;
+ color: var(--gray60);
+ transition: color 0.3s ease;
+}
+
+.search-box-clear {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+}
+
+.search-box-input-note {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ line-height: 1;
+ color: var(--secondFontColor);
+ font-size: var(--smallFontSize);
+ white-space: nowrap;
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:contact 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 * as classNames from 'classnames';
+import { debounce, Cancelable } from 'lodash';
+import SearchIcon from '../icons-components/SearchIcon';
+import ClearIcon from '../icons-components/ClearIcon';
+import { ButtonIcon } from '../ui/buttons';
+import * as theme from '../../app/theme';
+import { translateWithParameters } from '../../helpers/l10n';
+import './SearchBox.css';
+
+interface Props {
+ autoFocus?: boolean;
+ innerRef?: (node: HTMLInputElement | null) => void;
+ minLength?: number;
+ onChange: (value: string) => void;
+ onClick?: React.MouseEventHandler<HTMLInputElement>;
+ onFocus?: React.FocusEventHandler<HTMLInputElement>;
+ onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
+ placeholder: string;
+ value?: string;
+}
+
+interface State {
+ value: string;
+}
+
+export default class SearchBox extends React.PureComponent<Props, State> {
+ debouncedOnChange: ((query: string) => void) & Cancelable;
+ input: HTMLInputElement | null;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = { value: props.value || '' };
+ this.debouncedOnChange = debounce(this.props.onChange, 250);
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ if (
+ // input is controlled
+ nextProps.value !== undefined &&
+ // parent is aware of last change
+ // can happen when previous value was less than min length
+ this.state.value === this.props.value &&
+ nextProps.value !== this.state.value
+ ) {
+ this.setState({ value: nextProps.value });
+ }
+ }
+
+ changeValue = (value: string, debounced = true) => {
+ const { minLength } = this.props;
+ if (value.length === 0) {
+ // immediately notify when value is empty
+ this.props.onChange('');
+ // and cancel scheduled callback
+ this.debouncedOnChange.cancel();
+ } else if (!minLength || minLength <= value.length) {
+ if (debounced) {
+ this.debouncedOnChange(value);
+ } else {
+ this.props.onChange(value);
+ }
+ }
+ };
+
+ handleInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+ const { value } = event.currentTarget;
+ this.setState({ value });
+ this.changeValue(value);
+ };
+
+ handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
+ if (event.keyCode === 27) {
+ // escape
+ event.preventDefault();
+ this.handleResetClick();
+ }
+ if (this.props.onKeyDown) {
+ this.props.onKeyDown(event);
+ }
+ };
+
+ handleResetClick = () => {
+ this.changeValue('', false);
+ if (this.props.value === undefined) {
+ this.setState({ value: '' });
+ }
+ if (this.input) {
+ this.input.focus();
+ }
+ };
+
+ ref = (node: HTMLInputElement | null) => {
+ this.input = node;
+ if (this.props.innerRef) {
+ this.props.innerRef(node);
+ }
+ };
+
+ render() {
+ const { minLength } = this.props;
+ const { value } = this.state;
+
+ const inputClassName = classNames('search-box-input', {
+ touched: value.length > 0 && (!minLength || minLength > value.length)
+ });
+
+ const tooShort = minLength !== undefined && value.length > 0 && value.length < minLength;
+
+ return (
+ <div className="search-box">
+ <input
+ autoComplete="off"
+ autoFocus={this.props.autoFocus}
+ className={inputClassName}
+ maxLength={100}
+ onChange={this.handleInputChange}
+ onClick={this.props.onClick}
+ onFocus={this.props.onFocus}
+ onKeyDown={this.handleInputKeyDown}
+ placeholder={this.props.placeholder}
+ ref={this.ref}
+ type="search"
+ value={value}
+ />
+
+ <SearchIcon className="search-box-magnifier" />
+
+ {value && (
+ <ButtonIcon
+ className="button-tiny search-box-clear"
+ color={theme.gray60}
+ onClick={this.handleResetClick}>
+ <ClearIcon size={12} />
+ </ButtonIcon>
+ )}
+
+ {tooShort && (
+ <span
+ className="search-box-note"
+ title={translateWithParameters('select2.tooShort', minLength!)}>
+ {translateWithParameters('select2.tooShort', minLength!)}
+ </span>
+ )}
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:contact 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, mount } from 'enzyme';
+import SearchBox from '../SearchBox';
+import { click, change } from '../../../helpers/testUtils';
+
+jest.mock('lodash', () => {
+ const lodash = require.requireActual('lodash');
+ const debounce = (fn: Function) => {
+ const debounced: any = (...args: any[]) => fn(...args);
+ debounced.cancel = jest.fn();
+ return debounced;
+ };
+ return Object.assign({}, lodash, { debounce });
+});
+
+it('renders', () => {
+ const wrapper = shallow(
+ <SearchBox minLength={2} onChange={jest.fn()} placeholder="placeholder" value="foo" />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('warns when input is too short', () => {
+ const wrapper = shallow(
+ <SearchBox minLength={2} onChange={jest.fn()} placeholder="placeholder" value="f" />
+ );
+ expect(wrapper.find('.search-box-note').exists()).toBeTruthy();
+});
+
+it('shows clear button only when there is a value', () => {
+ const wrapper = shallow(<SearchBox onChange={jest.fn()} placeholder="placeholder" value="f" />);
+ expect(wrapper.find('.search-box-clear').exists()).toBeTruthy();
+ wrapper.setProps({ value: '' });
+ expect(wrapper.find('.search-box-clear').exists()).toBeFalsy();
+});
+
+it('attaches ref', () => {
+ const ref = jest.fn();
+ mount(<SearchBox innerRef={ref} onChange={jest.fn()} placeholder="placeholder" value="f" />);
+ expect(ref).toBeCalled();
+ expect(ref.mock.calls[0][0]).toBeInstanceOf(HTMLInputElement);
+});
+
+it('resets', () => {
+ const onChange = jest.fn();
+ const wrapper = shallow(<SearchBox onChange={onChange} placeholder="placeholder" value="f" />);
+ click(wrapper.find('.search-box-clear'));
+ expect(onChange).toBeCalledWith('');
+});
+
+it('changes', () => {
+ const onChange = jest.fn();
+ const wrapper = shallow(<SearchBox onChange={onChange} placeholder="placeholder" value="f" />);
+ change(wrapper.find('.search-box-input'), 'foo');
+ expect(onChange).toBeCalledWith('foo');
+});
+
+it('does not change when value is too short', () => {
+ const onChange = jest.fn();
+ const wrapper = shallow(
+ <SearchBox minLength={3} onChange={onChange} placeholder="placeholder" value="" />
+ );
+ change(wrapper.find('.search-box-input'), 'fo');
+ expect(onChange).not.toBeCalled();
+});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<div
+ className="search-box"
+>
+ <input
+ autoComplete="off"
+ className="search-box-input"
+ maxLength={100}
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="placeholder"
+ type="search"
+ value="foo"
+ />
+ <SearchIcon
+ className="search-box-magnifier"
+ />
+ <ButtonIcon
+ className="button-tiny search-box-clear"
+ color="#999"
+ onClick={[Function]}
+ >
+ <ClearIcon
+ size={12}
+ />
+ </ButtonIcon>
+</div>
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:contact 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 { IconProps } from './types';
+
+export default function SearchIcon({ className, fill = 'currentColor', size = 16 }: IconProps) {
+ return (
+ <svg
+ className={className}
+ width={size}
+ height={size}
+ viewBox="0 0 16 16"
+ version="1.1"
+ xmlnsXlink="http://www.w3.org/1999/xlink"
+ xmlSpace="preserve">
+ <path
+ style={{ fill }}
+ d="M10.308 7.077c0-.89-.316-1.65-.949-2.283a3.111 3.111 0 0 0-2.282-.948c-.89 0-1.65.316-2.283.948a3.111 3.111 0 0 0-.948 2.283c0 .89.316 1.65.948 2.282a3.111 3.111 0 0 0 2.283.949c.89 0 1.65-.316 2.282-.949a3.111 3.111 0 0 0 .949-2.282zm3.692 6c0 .25-.091.466-.274.649a.887.887 0 0 1-.65.274.857.857 0 0 1-.648-.274L9.954 11.26c-.86.596-1.82.894-2.877.894a4.989 4.989 0 0 1-1.972-.4 5.076 5.076 0 0 1-1.623-1.082A5.076 5.076 0 0 1 2.4 9.049 4.989 4.989 0 0 1 2 7.077c0-.688.133-1.345.4-1.972a5.076 5.076 0 0 1 1.082-1.623A5.076 5.076 0 0 1 5.105 2.4 4.989 4.989 0 0 1 7.077 2c.687 0 1.345.133 1.972.4a5.076 5.076 0 0 1 1.623 1.082c.454.454.815.995 1.082 1.623.266.627.4 1.284.4 1.972a4.938 4.938 0 0 1-.894 2.877l2.473 2.474a.883.883 0 0 1 .267.649z"
+ />
+ </svg>
+ );
+}
*/
// @flow
import React from 'react';
-import { debounce, map } from 'lodash';
+import { map } from 'lodash';
import Avatar from '../../../components/ui/Avatar';
import BubblePopup from '../../../components/common/BubblePopup';
import SelectList from '../../../components/common/SelectList';
import SelectListItem from '../../../components/common/SelectListItem';
+import SearchBox from '../../../components/controls/SearchBox';
import getCurrentUserFromStore from '../../../app/utils/getCurrentUserFromStore';
import { areThereCustomOrganizations } from '../../../store/organizations/utils';
import { searchMembers } from '../../../api/organizations';
constructor(props /*: Props */) {
super(props);
this.organizationEnabled = areThereCustomOrganizations();
- this.searchUsers = debounce(this.searchUsers, 250);
- this.searchMembers = debounce(this.searchMembers, 250);
this.defaultUsersArray = [{ login: '', name: translate('unassigned') }];
const currentUser = getCurrentUserFromStore();
});
};
- handleSearchChange = (evt /*: SyntheticInputEvent */) => {
- const query = evt.target.value;
- if (query.length < 2) {
+ handleSearchChange = (query /*: string */) => {
+ if (query.length === 0) {
this.setState({
query,
users: this.defaultUsersArray,
position={this.props.popupPosition}
customClass="bubble-popup-menu bubble-popup-bottom">
<div className="multi-select">
- <div className="search-box menu-search">
- <button className="search-box-submit button-clean">
- <i className="icon-search-new" />
- </button>
- <input
- type="search"
- value={this.state.query}
- className="search-box-input"
- placeholder={translate('search_verb')}
- onChange={this.handleSearchChange}
- autoComplete="off"
+ <div className="menu-search">
+ <SearchBox
autoFocus={true}
+ minLength={2}
+ onChange={this.handleSearchChange}
+ placeholder={translate('search.search_for_users')}
+ value={this.state.query}
/>
</div>
<SelectList
*/
//@flow
import React from 'react';
-import { debounce, without } from 'lodash';
+import { without } from 'lodash';
import TagsSelector from '../../../components/tags/TagsSelector';
import { searchIssueTags } from '../../../api/issues';
export default class SetIssueTagsPopup extends React.PureComponent {
/*:: mounted: boolean; */
/*:: props: Props; */
- /*:: state: State; */
-
- constructor(props /*: Props */) {
- super(props);
- this.state = { searchResult: [] };
- this.onSearch = debounce(this.onSearch, 250);
- }
+ state /*: State */ = { searchResult: [] };
componentDidMount() {
this.mounted = true;
onSearch = (query /*: string */) => {
searchIssueTags({
- q: query || '',
+ q: query,
ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100),
organization: this.props.organization
}).then((tags /*: Array<string> */) => {
render() {
return (
+ // $FlowFixMe `this.props.popupPosition` is passed from `BabelPopupHelper`
<TagsSelector
position={this.props.popupPosition}
tags={this.state.searchResult}
import React from 'react';
import BubblePopup from '../common/BubblePopup';
import MultiSelect from '../common/MultiSelect';
+import { translate } from '../../helpers/l10n';
import './TagsList.css';
/*::
};
*/
-export default class TagsSelector extends React.PureComponent {
- /*:: validateTag: string => string; */
-
- /*:: props: Props; */
-
- validateTag(value /*: string */) {
- // Allow only a-z, 0-9, '+', '-', '#', '.'
- return value.toLowerCase().replace(/[^a-z0-9\+\-#.]/gi, '');
- }
+export default function TagsSelector(props /*: Props */) {
+ return (
+ <BubblePopup
+ position={props.position}
+ customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300">
+ <MultiSelect
+ elements={props.tags}
+ selectedElements={props.selectedTags}
+ listSize={props.listSize}
+ onSearch={props.onSearch}
+ onSelect={props.onSelect}
+ onUnselect={props.onUnselect}
+ validateSearchInput={validateTag}
+ placeholder={translate('search.search_for_tags')}
+ />
+ </BubblePopup>
+ );
+}
- render() {
- return (
- <BubblePopup
- position={this.props.position}
- customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300">
- <MultiSelect
- elements={this.props.tags}
- selectedElements={this.props.selectedTags}
- listSize={this.props.listSize}
- onSearch={this.props.onSearch}
- onSelect={this.props.onSelect}
- onUnselect={this.props.onUnselect}
- validateSearchInput={this.validateTag}
- />
- </BubblePopup>
- );
- }
+export function validateTag(value /*: string */) {
+ // Allow only a-z, 0-9, '+', '-', '#', '.'
+ return value.toLowerCase().replace(/[^a-z0-9\+\-#.]/gi, '');
}
*/
import { shallow } from 'enzyme';
import React from 'react';
-import TagsSelector from '../TagsSelector';
+import TagsSelector, { validateTag } from '../TagsSelector';
const props = {
position: { left: 0, top: 0 },
it('should validate tags correctly', () => {
const validChars = 'abcdefghijklmnopqrstuvwxyz0123456789+-#.';
- const tagsSelector = shallow(<TagsSelector {...props} />).instance();
- expect(tagsSelector.validateTag('test')).toBe('test');
- expect(tagsSelector.validateTag(validChars)).toBe(validChars);
- expect(tagsSelector.validateTag(validChars.toUpperCase())).toBe(validChars);
- expect(tagsSelector.validateTag('T E$ST')).toBe('test');
- expect(tagsSelector.validateTag('T E$st!^à éèing1')).toBe('testing1');
+ expect(validateTag('test')).toBe('test');
+ expect(validateTag(validChars)).toBe(validChars);
+ expect(validateTag(validChars.toUpperCase())).toBe(validChars);
+ expect(validateTag('T E$ST')).toBe('test');
+ expect(validateTag('T E$st!^à éèing1')).toBe('testing1');
});
onSearch={[Function]}
onSelect={[Function]}
onUnselect={[Function]}
+ placeholder="search.search_for_tags"
selectedElements={
Array [
"bar",
onSearch={[Function]}
onSelect={[Function]}
onUnselect={[Function]}
+ placeholder="search.search_for_tags"
selectedElements={Array []}
validateSearchInput={[Function]}
/>
};
render() {
- const { children, className, color = theme.darkBlue, ...props } = this.props;
+ const { children, className, color = theme.darkBlue, onClick, ...props } = this.props;
return (
<button
className={classNames(className, 'button-icon')}
}
export function elementKeydown(element: ShallowWrapper, keyCode: number): void {
- element.simulate('keydown', {
+ const event = {
currentTarget: { element },
keyCode,
preventDefault() {}
- });
+ };
+
+ if (typeof element.type() === 'string') {
+ // `type()` is string for native dom elements
+ element.simulate('keydown', event);
+ } else {
+ element.prop<Function>('onKeyDown')(event);
+ }
}
export function doAsync(fn?: Function): Promise<void> {
# SEARCH ENGINE FOR RESOURCES
#
#------------------------------------------------------------------------------
-search.shortcut_hint=Hint: Press {0} from anywhere to open this search bar.
+search.shortcut_hint=Hint: Press {shortcut} from anywhere to open this search bar.
search.show_more.hint=Press {0} to display
search.placeholder=Search for projects, sub-projects and files...
+search.search_for_projects=Search for projects...
+search.search_for_users=Search for users...
+search.search_for_users_or_groups=Search for users or groups...
+search.search_by_login_or_name=Search by login or name...
+search.search_by_name=Search by name...
+search.search_by_name_or_key=Search by name or key...
+search.search_for_tags=Search for tags...
+search.search_for_rules=Search for rules...
+search.search_for_languages=Search for languages...
#------------------------------------------------------------------------------
marketplace.wrong_license_type_x=Your license is not compatible with the selected edition. Please provide a valid license for {0}.
marketplace.i_need_a_license=I need a license key
marketplace.download_package=Download package
+marketplace.search=Search by features or categories...
#------------------------------------------------------------------------------
api_documentation.parameters=Parameters
api_documentation.response_example=Response Example
api_documentation.changelog=Changelog
+api_documentation.search=Search by name...
#------------------------------------------------------------------------------
#
#------------------------------------------------------------------------------
code.open_component_page=Open Component's Page
+code.search_placeholder=Search for files and sub-projects...
+code.search_placeholder.portfolio=Search for projects and sub-portfolios...
#------------------------------------------------------------------------------
branches.last_analysis_date=Last Analysis Date
branches.no_support.header=Get the most out of SonarQube with branches analysis
branches.no_support.header.text=Analyze each branch of your project separately with the Developer Edition.
+branches.search_for_branches=Search for branches...
#------------------------------------------------------------------------------
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.html5.WebStorage;
-import org.sonarqube.pageobjects.measures.MeasuresPage;
-import org.sonarqube.tests.Tester;
import org.sonarqube.pageobjects.issues.IssuesPage;
+import org.sonarqube.pageobjects.measures.MeasuresPage;
import org.sonarqube.pageobjects.organization.MembersPage;
import org.sonarqube.pageobjects.projects.ProjectsPage;
import org.sonarqube.pageobjects.settings.SettingsPage;
+import org.sonarqube.tests.Tester;
import static com.codeborne.selenide.Condition.visible;
import static com.codeborne.selenide.Selenide.$;
return open(url, MeasuresPage.class);
}
+ public ProjectCodePage openCode(String projectKey) {
+ // TODO encode projectKey
+ String url = "/code?id=" + projectKey;
+ return open(url, ProjectCodePage.class);
+ }
+
+ public ProjectCodePage openCode(String projectKey, String selected) {
+ // TODO encode projectKey and selected
+ String url = "/code?id=" + projectKey + "&selected=" + selected;
+ return open(url, ProjectCodePage.class);
+ }
+
public MembersPage openOrganizationMembers(String orgKey) {
String url = "/organizations/" + orgKey + "/members";
return open(url, MembersPage.class);
--- /dev/null
+/*
+ * 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.
+ */
+package org.sonarqube.pageobjects;
+
+import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Selenide.$;
+import static com.codeborne.selenide.Selenide.$$;
+
+public class ProjectCodePage {
+
+ public ProjectCodePage() {}
+
+ public ProjectCodePage openFirstComponent() {
+ $$(".code-name-cell a").first().click();
+ return this;
+ }
+
+ public ProjectCodePage search(String query) {
+ $(".code-search .search-box-input").val(query);
+ return this;
+ }
+
+ public ProjectCodePage shouldHaveComponent(String name) {
+ $(".code-components").shouldHave(text(name));
+ return this;
+ }
+
+ public ProjectCodePage shouldHaveCode(String code) {
+ $(".code-components .source-viewer").shouldHave(text(code));
+ return this;
+ }
+
+ public ProjectCodePage shouldHaveBreadcrumbs(String... breadcrumbs) {
+ for (String breadcrumb : breadcrumbs) {
+ $(".code-breadcrumbs").shouldHave(text(breadcrumb));
+ }
+ return this;
+ }
+
+ public ProjectCodePage shouldSearchResult(String name) {
+ $(".code-search-with-results").shouldHave(text(name));
+ return this;
+ }
+}
}
public MembersPage searchForMember(String query) {
- $("input.search-box-input").shouldBe(visible).val("").sendKeys(query);
+ $(".page .search-box-input").shouldBe(visible).val("").sendKeys(query);
return this;
}
package org.sonarqube.tests.organization;
import com.sonar.orchestrator.Orchestrator;
-import org.sonarqube.tests.Category6Suite;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
-import org.sonarqube.tests.OrganizationTester;
+import org.sonarqube.pageobjects.organization.MembersPage;
+import org.sonarqube.tests.Category6Suite;
import org.sonarqube.tests.Tester;
import org.sonarqube.ws.Organizations.Organization;
import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
-import org.sonarqube.pageobjects.organization.MembersPage;
-
-import static util.ItUtils.setServerProperty;
public class OrganizationMembershipUiTest {
@Before
public void setUp() {
- setServerProperty(orchestrator, "sonar.organizations.anyoneCanCreate", "true");
+ tester.settings().setGlobalSetting("sonar.organizations.anyoneCanCreate", "true");
root = tester.users().generate();
tester.wsClient().roots().setRoot(root.getLogin());
}
@After
public void tearDown() {
- setServerProperty(orchestrator, "sonar.organizations.anyoneCanCreate", null);
+ tester.settings().resetSettings("sonar.organizations.anyoneCanCreate");
}
@Test
addMember(organization, member1);
User member2 = tester.users().generate(p -> p.setName("bar"));
addMember(organization, member2);
- User nonMember = tester.users().generate();
+ tester.users().generate();
MembersPage page = tester.openBrowser().openOrganizationMembers(organization.getKey());
page
User member2 = tester.users().generate(p -> p.setName("sameprefixuser1"));
addMember(organization, member2);
// Created to verify that only the user part of the org is returned
- User userWithSameNamePrefix = tester.users().generate(p -> p.setName(member2.getName() + "sameprefixuser2"));
+ tester.users().generate(p -> p.setName(member2.getName() + "sameprefixuser2"));
MembersPage page = tester.openBrowser().openOrganizationMembers(organization.getKey());
page
public void admin_can_add_members() {
Organization organization = tester.organizations().generate();
User user1 = tester.users().generate(u -> u.setLogin("foo"));
- User user2 = tester.users().generate();
+ tester.users().generate();
MembersPage page = tester.openBrowser()
.logIn().submitCredentials(root.getLogin())
.shouldHaveGroups(2);
}
- private OrganizationTester addMember(Organization organization, User member1) {
- return tester.organizations().addMember(organization, member1);
+ private void addMember(Organization organization, User member1) {
+ tester.organizations().addMember(organization, member1);
}
}
package org.sonarqube.tests.projectAdministration;
import com.sonar.orchestrator.Orchestrator;
-import com.sonar.orchestrator.build.SonarScanner;
-import org.sonarqube.tests.Category1Suite;
-import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
-import util.user.UserRule;
+import org.sonarqube.tests.Category1Suite;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.WsProjects.CreateWsResponse.Project;
+import org.sonarqube.ws.client.component.SearchProjectsRequest;
-import static util.ItUtils.projectDir;
-import static util.selenium.Selenese.runSelenese;
+import static com.codeborne.selenide.Condition.text;
+import static com.codeborne.selenide.Condition.visible;
+import static com.codeborne.selenide.Selenide.$;
+import static org.assertj.core.api.Assertions.assertThat;
public class ProjectBulkDeletionPageTest {
- private static final String ADMIN_USER_LOGIN = "admin-user";
+ private String adminUser;
@ClassRule
public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;
@Rule
- public UserRule userRule = UserRule.from(orchestrator);
+ public Tester tester = new Tester(orchestrator);
@Before
public void deleteData() {
orchestrator.resetData();
- userRule.createAdminUser(ADMIN_USER_LOGIN, ADMIN_USER_LOGIN);
- }
-
- @After
- public void deleteAdminUser() {
- userRule.resetUsers();
+ adminUser = tester.users().generateAdministrator().getLogin();
}
/**
*/
@Test
public void test_bulk_deletion_on_selected_projects() throws Exception {
- // we must have several projects to test the bulk deletion
- executeBuild("cameleon-1", "Sample-Project");
- executeBuild("cameleon-2", "Foo-Application");
- executeBuild("cameleon-3", "Bar-Sonar-Plugin");
+ Project project1 = tester.projects().generate(null, t -> t.setName("Foo"));
+ Project project2 = tester.projects().generate(null, t -> t.setName("Bar"));
+ Project project3 = tester.projects().generate(null, t -> t.setName("FooQux"));
- runSelenese(orchestrator, "/projectAdministration/ProjectBulkDeletionPageTest/bulk-delete-filter-projects.html");
- }
+ tester.openBrowser().logIn().submitCredentials(adminUser).open("/admin/projects_management");
+ $("#projects-management-page").shouldHave(text(project1.getName())).shouldHave(text(project2.getName())).shouldHave(text(project3.getName()));
- private void executeBuild(String projectKey, String projectName) {
- orchestrator.executeBuild(
- SonarScanner.create(projectDir("shared/xoo-sample"))
- .setProjectKey(projectKey)
- .setProjectName(projectName));
- }
+ $("#projects-management-page .search-box-input").val("foo").pressEnter();
+ $("#projects-management-page").shouldNotHave(text(project2.getName())).shouldHave(text(project1.getName())).shouldHave(text(project3.getName()));
+ $("#projects-management-page .js-delete").click();
+ $(".modal").shouldBe(visible);
+ $(".modal button").click();
+ $("#projects-management-page").shouldNotHave(text(project1.getName())).shouldNotHave(text(project3.getName()));
+
+ assertThat(tester.wsClient().components().searchProjects(SearchProjectsRequest.builder().build())
+ .getComponentsCount()).isEqualTo(1);
+ }
}
import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.build.SonarScanner;
-import org.sonarqube.tests.Category1Suite;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
+import org.sonarqube.tests.Category1Suite;
import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.WsProjects.CreateWsResponse.Project;
import static util.ItUtils.projectDir;
public Tester tester = new Tester(orchestrator).disableOrganizations();
@Test
- public void test_project_code_page() {
- executeBuild("shared/xoo-sample", "project-for-code", "Project For Code");
+ public void browse() {
+ Project project = tester.projects().generate(null);
+ executeAnalysis(project);
+
+ tester.openBrowser().openCode(project.getKey())
+ .shouldHaveComponent("src/main/xoo/sample")
+ .openFirstComponent()
+ .shouldHaveComponent("Sample.xoo")
+ .openFirstComponent()
+ .shouldHaveCode("public class Sample")
+ .shouldHaveBreadcrumbs(project.getName(), "src/main/xoo/sample", "Sample.xoo");
+ }
+
+ @Test
+ public void search() {
+ Project project = tester.projects().generate(null);
+ executeAnalysis(project);
- tester.runHtmlTests(
- "/sourceCode/ProjectCodeTest/test_project_code_page.html",
- "/sourceCode/ProjectCodeTest/search.html",
- "/sourceCode/ProjectCodeTest/permalink.html");
+ tester.openBrowser().openCode(project.getKey())
+ .shouldHaveComponent(project.getName())
+ .search("xoo")
+ .shouldSearchResult("Sample.xoo");
}
@Test
- public void code_page_should_expand_root_dir() {
- executeBuild("shared/xoo-sample-with-root-dir", "project-for-code-root-dir", "Project For Code");
+ public void permalink() {
+ Project project = tester.projects().generate(null);
+ executeAnalysis(project);
- tester.runHtmlTests("/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html");
+ tester.openBrowser().openCode(project.getKey(), project.getKey() + "%3Asrc%2Fmain%2Fxoo%2Fsample%2FSample.xoo")
+ .shouldHaveCode("public class Sample")
+ .shouldHaveBreadcrumbs(project.getName(), "src/main/xoo/sample", "Sample.xoo");
}
- private void executeBuild(String projectLocation, String projectKey, String projectName) {
+ @Test
+ public void expand_root_dir() {
+ Project project = tester.projects().generate(null);
+ executeAnalysis(project, "shared/xoo-sample-with-root-dir");
+
+ tester.openBrowser().openCode(project.getKey())
+ .shouldHaveComponent("Hello.xoo")
+ .shouldHaveComponent("src/main/xoo/sample");
+ }
+
+ private void executeAnalysis(Project project, String path) {
orchestrator.executeBuild(
- SonarScanner.create(projectDir(projectLocation))
- .setProjectKey(projectKey)
- .setProjectName(projectName));
+ SonarScanner.create(projectDir(path))
+ .setProjectKey(project.getKey())
+ .setProjectName(project.getName()));
}
+ private void executeAnalysis(Project project) {
+ executeAnalysis(project, "shared/xoo-sample");
+ }
}
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
- <title>bulk-delete-filter-projects</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
- <tbody>
-<tr>
- <td>open</td>
- <td>/sessions/login</td>
- <td></td>
-</tr>
-<tr>
- <td>type</td>
- <td>login</td>
- <td>admin-user</td>
-</tr>
-<tr>
- <td>type</td>
- <td>password</td>
- <td>admin-user</td>
-</tr>
-<tr>
- <td>clickAndWait</td>
- <td>commit</td>
- <td></td>
-</tr>
-<tr>
- <td>waitForElementPresent</td>
- <td>css=.js-user-authenticated</td>
- <td></td>
-</tr>
-<tr>
- <td>open</td>
- <td>/projects_admin</td>
- <td></td>
-</tr>
-<tr>
- <td>waitForText</td>
- <td>content</td>
- <td>*Bar-Sonar-Plugin*Foo-Application*Sample-Project*</td>
-</tr>
-<tr>
- <td>type</td>
- <td>css=.search-box-input</td>
- <td>s</td>
-</tr>
-<tr>
- <td>click</td>
- <td>css=.search-box-submit</td>
- <td></td>
-</tr>
-<tr>
- <td>waitForText</td>
- <td>content</td>
- <td>*Bar-Sonar-Plugin*Sample-Project*</td>
-</tr>
-<tr>
- <td>waitForText</td>
- <td>content</td>
- <td>*cameleon-3*cameleon-1*</td>
-</tr>
-<tr>
- <td>assertTextNotPresent</td>
- <td>content</td>
- <td>*Foo-Application*</td>
-</tr>
-</tbody>
-</table>
-</body>
-</html>
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
- <link rel="selenium.base" href="http://localhost:49506"/>
- <title>code_page_should_expand_root_dir</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
- <thead>
- <tr>
- <td rowspan="1" colspan="3">code_page_should_expand_root_dir</td>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>open</td>
- <td>/code?id=project-for-code-root-dir</td>
- <td></td>
-</tr>
-<tr>
- <td>waitForText</td>
- <td>css=#content</td>
- <td>*Hello.xoo*src/main/xoo/sample*</td>
-</tr>
-</tbody>
-</table>
-</body>
-</html>
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
- <link rel="selenium.base" href="http://localhost:49506"/>
- <title>test_project_code_page</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
- <thead>
- <tr>
- <td rowspan="1" colspan="3">test_project_code_page</td>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>open</td>
- <td>/code?id=project-for-code&selected=project-for-code%3Asrc%2Fmain%2Fxoo%2Fsample%2FSample.xoo</td>
- <td></td>
-</tr>
-<tr>
- <td>waitForText</td>
- <td>css=#content</td>
- <td>*public class Sample*</td>
-</tr>
-<tr>
- <td>waitForText</td>
- <td>css=.code-breadcrumbs</td>
- <td>*Project For Code*src/main/xoo/sample*Sample.xoo*</td>
-</tr>
-</tbody>
-</table>
-</body>
-</html>
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
- <link rel="selenium.base" href="http://localhost:49506"/>
- <title>test_project_code_page</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
- <thead>
- <tr>
- <td rowspan="1" colspan="3">test_project_code_page</td>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>open</td>
- <td>/code?id=project-for-code</td>
- <td></td>
-</tr>
-<tr>
- <td>waitForText</td>
- <td>css=#content</td>
- <td>*Project For Code*13*0*0*0.0%*</td>
-</tr>
-<tr>
- <td>type</td>
- <td>css=.search-box-input</td>
- <td>xoo</td>
-</tr>
-<tr>
- <td>click</td>
- <td>css=.search-box-submit</td>
- <td></td>
-</tr>
-<tr>
- <td>waitForText</td>
- <td>css=#content</td>
- <td>*Sample.xoo*</td>
-</tr>
-<tr>
- <td>click</td>
- <td>css=.code-name-cell a</td>
- <td></td>
-</tr>
-<tr>
- <td>waitForText</td>
- <td>css=#content</td>
- <td>*public class Sample*</td>
-</tr>
-<tr>
- <td>waitForText</td>
- <td>css=.code-breadcrumbs</td>
- <td>*Project For Code*src/main/xoo/sample*Sample.xoo*</td>
-</tr>
-</tbody>
-</table>
-</body>
-</html>
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head profile="http://selenium-ide.openqa.org/profiles/test-case">
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
- <link rel="selenium.base" href="http://localhost:49506"/>
- <title>test_project_code_page</title>
-</head>
-<body>
-<table cellpadding="1" cellspacing="1" border="1">
- <thead>
- <tr>
- <td rowspan="1" colspan="3">test_project_code_page</td>
- </tr>
- </thead>
- <tbody>
- <tr>
- <td>open</td>
- <td>/code?id=project-for-code</td>
- <td></td>
-</tr>
-<tr>
- <td>waitForText</td>
- <td>css=#content</td>
- <td>*Project For Code*13*0*0*0.0%*</td>
-</tr>
-<tr>
- <td>waitForText</td>
- <td>css=#content</td>
- <td>*src/main/xoo/sample*</td>
-</tr>
-<tr>
- <td>click</td>
- <td>css=.code-name-cell a</td>
- <td></td>
-</tr>
-<tr>
- <td>waitForText</td>
- <td>css=#content</td>
- <td>*Sample.xoo*</td>
-</tr>
-<tr>
- <td>click</td>
- <td>css=.code-breadcrumbs a</td>
- <td></td>
-</tr>
-<tr>
- <td>waitForNotText</td>
- <td>css=#content</td>
- <td>*Sample.xoo*</td>
-</tr>
-</tbody>
-</table>
-</body>
-</html>