@@ -29,6 +29,7 @@ import { | |||
} from '../../../../helpers/branches'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { getProjectBranchUrl } from '../../../../helpers/urls'; | |||
import SearchBox from '../../../../components/controls/SearchBox'; | |||
import Tooltip from '../../../../components/controls/Tooltip'; | |||
interface Props { | |||
@@ -75,8 +76,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<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) { | |||
@@ -84,10 +84,6 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, | |||
event.preventDefault(); | |||
this.openSelected(); | |||
return; | |||
case 27: | |||
event.preventDefault(); | |||
this.props.onClose(); | |||
return; | |||
case 38: | |||
event.preventDefault(); | |||
this.selectPrevious(); | |||
@@ -151,17 +147,12 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, | |||
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> |
@@ -66,13 +66,13 @@ it('selects next & previous', () => { | |||
onClose={jest.fn()} | |||
/> | |||
); | |||
elementKeydown(wrapper.find('input'), 40); | |||
elementKeydown(wrapper.find('SearchBox'), 40); | |||
wrapper.update(); | |||
expect(wrapper.state().selected).toBe('foo'); | |||
elementKeydown(wrapper.find('input'), 40); | |||
elementKeydown(wrapper.find('SearchBox'), 40); | |||
wrapper.update(); | |||
expect(wrapper.state().selected).toBe('foobar'); | |||
elementKeydown(wrapper.find('input'), 38); | |||
elementKeydown(wrapper.find('SearchBox'), 38); | |||
wrapper.update(); | |||
expect(wrapper.state().selected).toBe('foo'); | |||
}); |
@@ -5,22 +5,13 @@ exports[`renders list 1`] = ` | |||
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> | |||
@@ -181,22 +172,13 @@ exports[`searches 1`] = ` | |||
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> |
@@ -3,6 +3,12 @@ | |||
padding-right: 3px; | |||
} | |||
.navbar-search .search-box, | |||
.navbar-search .search-box-input { | |||
width: 310px; | |||
max-width: none; | |||
} | |||
.navbar-search-input { | |||
vertical-align: middle; | |||
width: 310px; | |||
@@ -13,22 +19,20 @@ | |||
.navbar-search-input-hint { | |||
position: absolute; | |||
top: 4px; | |||
right: 30px; | |||
top: 1px; | |||
right: 27px; | |||
line-height: var(--controlHeight); | |||
font-size: var(--smallFontSize); | |||
color: var(--secondFontColor); | |||
} | |||
.navbar-search-input-hint.is-shifted { | |||
z-index: 7501; | |||
top: 32px; | |||
} | |||
.navbar-search-icon { | |||
position: relative; | |||
z-index: var(--aboveNormalZIndex); | |||
vertical-align: middle; | |||
width: 16px; | |||
margin-right: -20px; | |||
background-color: #fff; | |||
color: var(--secondFontColor); | |||
} | |||
@@ -78,9 +82,10 @@ | |||
} | |||
.global-navbar-search-dropdown { | |||
top: calc(100% + 3px) !important; | |||
max-height: 80vh; | |||
width: 440px; | |||
padding: 0; | |||
padding: 0 !important; | |||
overflow-y: auto; | |||
overflow-x: hidden; | |||
} |
@@ -23,6 +23,7 @@ import PropTypes from 'prop-types'; | |||
import classNames from 'classnames'; | |||
import key from 'keymaster'; | |||
import { debounce, keyBy, uniqBy } from 'lodash'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import SearchResults from './SearchResults'; | |||
import SearchResult from './SearchResult'; | |||
import { sortQualifiers } from './utils'; | |||
@@ -30,6 +31,7 @@ import { sortQualifiers } from './utils'; | |||
import RecentHistory from '../../components/RecentHistory'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import ClockIcon from '../../../components/common/ClockIcon'; | |||
import SearchBox from '../../../components/controls/SearchBox'; | |||
import { getSuggestions } from '../../../api/components'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { scrollToElement } from '../../../helpers/scrolling'; | |||
@@ -59,7 +61,7 @@ type State = { | |||
*/ | |||
export default class Search extends React.PureComponent { | |||
/*:: input: HTMLElement; */ | |||
/*:: input: HTMLInputElement | null; */ | |||
/*:: mounted: boolean; */ | |||
/*:: node: HTMLElement; */ | |||
/*:: nodes: { [string]: HTMLElement }; | |||
@@ -92,7 +94,9 @@ export default class Search extends React.PureComponent { | |||
componentDidMount() { | |||
this.mounted = true; | |||
key('s', () => { | |||
this.input.focus(); | |||
if (this.input) { | |||
this.input.focus(); | |||
} | |||
this.openSearch(); | |||
return false; | |||
}); | |||
@@ -169,6 +173,12 @@ export default class Search extends React.PureComponent { | |||
return uniqBy([...components, ...recentlyBrowsed], 'key'); | |||
}; | |||
stopLoading = () => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
}; | |||
search = (query /*: string */) => { | |||
if (query.length === 0 || query.length >= 2) { | |||
this.setState({ loading: true }); | |||
@@ -191,10 +201,10 @@ export default class Search extends React.PureComponent { | |||
projects: { ...state.projects, ...keyBy(response.projects, 'key') }, | |||
results, | |||
selected: list.length > 0 ? list[0] : null, | |||
shortQuery: response.warning === 'short_input' | |||
shortQuery: query.length > 2 && response.warning === 'short_input' | |||
})); | |||
} | |||
}); | |||
}, this.stopLoading); | |||
} else { | |||
this.setState({ loading: false }); | |||
} | |||
@@ -221,12 +231,11 @@ export default class Search extends React.PureComponent { | |||
selected: moreResults.length > 0 ? moreResults[0].key : state.selected | |||
})); | |||
} | |||
}); | |||
}, this.stopLoading); | |||
} | |||
}; | |||
handleQueryChange = (event /*: { currentTarget: HTMLInputElement } */) => { | |||
const query = event.currentTarget.value; | |||
handleQueryChange = (query /*: string */) => { | |||
this.setState({ query, shortQuery: query.length === 1 }); | |||
this.search(query); | |||
}; | |||
@@ -278,10 +287,6 @@ export default class Search extends React.PureComponent { | |||
event.preventDefault(); | |||
this.openSelected(); | |||
return; | |||
case 27: | |||
event.preventDefault(); | |||
this.closeSearch(); | |||
return; | |||
case 38: | |||
event.preventDefault(); | |||
this.selectPrevious(); | |||
@@ -297,10 +302,18 @@ export default class Search extends React.PureComponent { | |||
this.setState({ selected }); | |||
}; | |||
handleClick = (event /*: Event */) => { | |||
event.stopPropagation(); | |||
}; | |||
innerRef = (component /*: string */, node /*: HTMLElement */) => { | |||
this.nodes[component] = node; | |||
}; | |||
searchInputRef = (node /*: HTMLInputElement | null */) => { | |||
this.input = node; | |||
}; | |||
renderResult = (component /*: Component */) => ( | |||
<SearchResult | |||
appState={this.props.appState} | |||
@@ -326,30 +339,21 @@ export default class Search extends React.PureComponent { | |||
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> | |||
)} | |||
@@ -375,12 +379,11 @@ export default class Search extends React.PureComponent { | |||
<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> |
@@ -38,12 +38,12 @@ function component(key /*: string */, qualifier /*: string */ = 'TRK') { | |||
} | |||
function next(form /*: ShallowWrapper */, expected /*: string */) { | |||
elementKeydown(form.find('input'), 40); | |||
elementKeydown(form.find('SearchBox'), 40); | |||
expect(form.state().selected).toBe(expected); | |||
} | |||
function prev(form /*: ShallowWrapper */, expected /*: string */) { | |||
elementKeydown(form.find('input'), 38); | |||
elementKeydown(form.find('SearchBox'), 38); | |||
expect(form.state().selected).toBe(expected); | |||
} | |||
@@ -83,7 +83,7 @@ it('opens selected on enter', () => { | |||
}); | |||
const openSelected = jest.fn(); | |||
form.instance().openSelected = openSelected; | |||
elementKeydown(form.find('input'), 13); | |||
elementKeydown(form.find('SearchBox'), 13); | |||
expect(openSelected).toBeCalled(); | |||
}); | |||
@@ -95,14 +95,6 @@ it('shows warning about short input', () => { | |||
expect(form.find('.navbar-search-input-hint')).toMatchSnapshot(); | |||
}); | |||
it('closes on escape', () => { | |||
const form = render(); | |||
form.instance().openSearch(); | |||
expect(form.state().open).toBe(true); | |||
elementKeydown(form.find('input'), 27); | |||
expect(form.state().open).toBe(false); | |||
}); | |||
it('closes on click outside', () => { | |||
const form = mount( | |||
<Search appState={{ organizationsEnabled: false }} currentUser={{ isLoggedIn: false }} /> |
@@ -10,7 +10,7 @@ exports[`shows warning about short input 1`] = ` | |||
exports[`shows warning about short input 2`] = ` | |||
<span | |||
className="navbar-search-input-hint is-shifted" | |||
className="navbar-search-input-hint" | |||
> | |||
select2.tooShort.2 | |||
</span> |
@@ -99,22 +99,10 @@ | |||
padding: 4px 16px 0; | |||
} | |||
.menu-search .search-box, | |||
.menu-search .search-box-input { | |||
font-size: var(--smallFontSize); | |||
} | |||
.menu-search .search-box-submit { | |||
vertical-align: baseline; | |||
} | |||
.menu-search-full-width { | |||
display: flex; | |||
align-items: center; | |||
} | |||
.menu-search-full-width .search-box-input { | |||
flex-grow: 1; | |||
width: auto; | |||
max-width: none; | |||
min-width: 240px; | |||
} | |||
.menu-search ~ .menu > li > a:hover, |
@@ -534,11 +534,7 @@ a.search-navigator-facet:focus .facet-stat { | |||
} | |||
.search-navigator-facet-query { | |||
padding: 7px 10px 27px; | |||
} | |||
.search-navigator-facet-query input { | |||
width: 100%; | |||
padding: 7px 0 27px; | |||
} | |||
.search-navigator-facet-custom-value { |
@@ -709,21 +709,6 @@ a:hover > .icon-radio { | |||
font-size: var(--bigFontSize); | |||
} | |||
.icon-search:before { | |||
content: '\f002'; | |||
font-size: var(--bigFontSize); | |||
} | |||
.icon-search-new { | |||
display: inline-block; | |||
vertical-align: top; | |||
width: 16px; | |||
height: 16px; | |||
background-size: 13px 14px; | |||
background: no-repeat center center; | |||
background-image: url('data:image/svg+xml,%3Csvg%20width%3D%2213%22%20height%3D%2214%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cpath%20d%3D%22M9%206.5c0-.964-.342-1.788-1.027-2.473C7.288%203.342%206.463%203%205.5%203c-.964%200-1.788.342-2.473%201.027C2.342%204.712%202%205.537%202%206.5c0%20.964.342%201.788%201.027%202.473C3.712%209.658%204.537%2010%205.5%2010c.964%200%201.788-.342%202.473-1.027C8.658%208.288%209%207.463%209%206.5zm4%206.5c0%20.27-.1.505-.297.703-.198.198-.432.297-.703.297-.28%200-.516-.1-.703-.297l-2.68-2.672c-.932.647-1.97.97-3.117.97-.745%200-1.457-.145-2.137-.434-.68-.29-1.265-.68-1.758-1.171-.492-.493-.882-1.08-1.17-1.758C.144%207.957%200%207.245%200%206.5c0-.745.145-1.457.434-2.137.29-.68.68-1.265%201.17-1.758.494-.492%201.08-.882%201.76-1.17C4.043%201.144%204.753%201%205.5%201c.745%200%201.457.145%202.137.434.68.29%201.265.68%201.758%201.17.492.494.882%201.08%201.17%201.76.29.68.435%201.39.435%202.136%200%201.146-.323%202.185-.97%203.117l2.68%202.68c.194.193.29.427.29.703z%22%20fill%3D%22%23777%22%20fill-rule%3D%22nonzero%22%2F%3E%3C%2Fsvg%3E'); | |||
} | |||
.icon-edit:before { | |||
content: '\f040'; | |||
font-size: var(--mediumFontSize); |
@@ -47,7 +47,6 @@ | |||
@import './components/panels.css'; | |||
@import './components/badges.css'; | |||
@import './components/columns.css'; | |||
@import './components/search.css'; | |||
@import './components/side-tabs.css'; | |||
@import './components/boxed-group.css'; | |||
@@ -48,19 +48,19 @@ describe('Search', () => { | |||
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(); |
@@ -4,7 +4,7 @@ | |||
} | |||
.bt-search-form > li + li { | |||
margin-left: 40px; | |||
margin-left: 16px; | |||
} | |||
.bt-search-form-label { | |||
@@ -15,8 +15,8 @@ | |||
padding: 4px 0; | |||
} | |||
.bt-search-form-right { | |||
margin-left: auto !important; | |||
.bt-search-form-large { | |||
flex: 1; | |||
} | |||
.bt-workers-warning-icon { |
@@ -25,6 +25,7 @@ import TypesFilter from './TypesFilter'; | |||
import CurrentsFilter from './CurrentsFilter'; | |||
import DateFilter from './DateFilter'; | |||
import { DEFAULT_FILTERS } from './../constants'; | |||
import SearchBox from '../../../components/controls/SearchBox'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default class Search extends React.PureComponent { | |||
@@ -54,9 +55,9 @@ export default class Search extends React.PureComponent { | |||
this.props.onFilterUpdate(date); | |||
} | |||
handleQueryChange(query /*: string */) { | |||
handleQueryChange = (query /*: string */) => { | |||
this.props.onFilterUpdate({ query }); | |||
} | |||
}; | |||
handleReload(e /*: Object */) { | |||
e.target.blur(); | |||
@@ -78,18 +79,11 @@ export default class Search extends React.PureComponent { | |||
} | |||
return ( | |||
<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> | |||
); | |||
@@ -143,7 +137,7 @@ export default class Search extends React.PureComponent { | |||
{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>{' '} |
@@ -55,20 +55,6 @@ | |||
display: none; | |||
} | |||
.code-search .search-box { | |||
padding-right: 10px; | |||
} | |||
.code-search .search-box .note { | |||
vertical-align: middle; | |||
opacity: 0; | |||
transition: opacity 0.3s ease; | |||
} | |||
.code-search .search-box input.touched ~ .note { | |||
opacity: 1; | |||
} | |||
.code-components-header { | |||
position: sticky; | |||
top: 95px; |
@@ -20,13 +20,13 @@ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import * as classNames from 'classnames'; | |||
import { debounce } from 'lodash'; | |||
import Components from './Components'; | |||
import { getTree } from '../../../api/components'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { parseError } from '../utils'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { Component } from '../types'; | |||
import SearchBox from '../../../components/controls/SearchBox'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
branch?: string; | |||
@@ -43,7 +43,6 @@ interface State { | |||
} | |||
export default class Search extends React.PureComponent<Props, State> { | |||
input: HTMLInputElement; | |||
mounted: boolean; | |||
static contextTypes = { | |||
@@ -55,10 +54,6 @@ export default class Search extends React.PureComponent<Props, State> { | |||
loading: false | |||
}; | |||
componentWillMount() { | |||
this.handleSearch = debounce(this.handleSearch, 250); | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
@@ -79,10 +74,6 @@ export default class Search extends React.PureComponent<Props, State> { | |||
this.mounted = false; | |||
} | |||
checkInputValue(query: string) { | |||
return this.input.value === query; | |||
} | |||
handleSelectNext() { | |||
const { selectedIndex, results } = this.state; | |||
if (results != null && selectedIndex != null && selectedIndex < results.length - 1) { | |||
@@ -114,27 +105,26 @@ export default class Search extends React.PureComponent<Props, State> { | |||
} | |||
} | |||
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 }); | |||
@@ -143,8 +133,7 @@ export default class Search extends React.PureComponent<Props, State> { | |||
getTree(component.key, { branch, q: query, s: 'qualifier,name', qualifiers }) | |||
.then(r => { | |||
// second time check if value has change due to api request | |||
if (this.mounted && this.checkInputValue(query)) { | |||
if (this.mounted) { | |||
this.setState({ | |||
results: r.components, | |||
selectedIndex: r.components.length > 0 ? 0 : undefined, | |||
@@ -153,8 +142,7 @@ export default class Search extends React.PureComponent<Props, State> { | |||
} | |||
}) | |||
.catch(e => { | |||
// second time check if value has change due to api request | |||
if (this.mounted && this.checkInputValue(query)) { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
parseError(e).then(onError); | |||
} | |||
@@ -162,61 +150,36 @@ export default class Search extends React.PureComponent<Props, State> { | |||
} | |||
}; | |||
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 |
@@ -17,17 +17,20 @@ | |||
* 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' | |||
}; | |||
}, | |||
@@ -37,6 +40,8 @@ export default BaseFacet.extend({ | |||
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); | |||
} | |||
}, | |||
@@ -45,8 +50,24 @@ export default BaseFacet.extend({ | |||
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() { |
@@ -1,5 +1,18 @@ | |||
<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> |
@@ -21,6 +21,7 @@ import React from 'react'; | |||
import Helmet from 'react-helmet'; | |||
import init from '../init'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import '../../../components/controls/SearchBox.css'; | |||
export default class GroupsAppContainer extends React.PureComponent { | |||
componentDidMount() { |
@@ -24,10 +24,15 @@ import Template from './templates/groups-search.hbs'; | |||
export default Marionette.ItemView.extend({ | |||
template: Template, | |||
ui: { | |||
reset: '.js-reset' | |||
}, | |||
events: { | |||
'submit #groups-search-form': 'onFormSubmit', | |||
'search #groups-search-query': 'debouncedOnKeyUp', | |||
'keyup #groups-search-query': 'debouncedOnKeyUp' | |||
'search #groups-search-query': 'initialOnKeyUp', | |||
'keyup #groups-search-query': 'initialOnKeyUp', | |||
'click .js-reset': 'onResetClick' | |||
}, | |||
initialize() { | |||
@@ -44,6 +49,12 @@ export default Marionette.ItemView.extend({ | |||
this.debouncedOnKeyUp(); | |||
}, | |||
initialOnKeyUp() { | |||
const q = this.getQuery(); | |||
this.ui.reset.toggleClass('hidden', q.length === 0); | |||
this.debouncedOnKeyUp(); | |||
}, | |||
onKeyUp() { | |||
const q = this.getQuery(); | |||
if (q === this._bufferedValue) { | |||
@@ -62,5 +73,14 @@ export default Marionette.ItemView.extend({ | |||
search(q) { | |||
return this.collection.fetch({ reset: true, data: { q } }); | |||
}, | |||
onResetClick(e) { | |||
e.preventDefault(); | |||
e.currentTarget.blur(); | |||
this.$('#groups-search-query') | |||
.val('') | |||
.focus(); | |||
this.onKeyUp(); | |||
} | |||
}); |
@@ -1,6 +1,15 @@ | |||
<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> |
@@ -56,7 +56,7 @@ class LanguageFacetFooter extends React.PureComponent { | |||
noResultsText={translate('select2.noMatches')} | |||
onChange={this.handleChange} | |||
options={options} | |||
placeholder={translate('search_verb')} | |||
placeholder={translate('search.search_for_languages')} | |||
searchable={true} | |||
/> | |||
</div> |
@@ -18,9 +18,9 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { debounce } from 'lodash'; | |||
import RadioToggle from '../../components/controls/RadioToggle'; | |||
import { Query } from './utils'; | |||
import RadioToggle from '../../components/controls/RadioToggle'; | |||
import SearchBox from '../../components/controls/SearchBox'; | |||
import { translate } from '../../helpers/l10n'; | |||
interface Props { | |||
@@ -29,33 +29,13 @@ interface Props { | |||
updateQuery: (newQuery: Partial<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 = [ | |||
@@ -77,21 +57,11 @@ export default class Search extends React.PureComponent<Props, State> { | |||
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> | |||
); | |||
} |
@@ -19,7 +19,7 @@ | |||
*/ | |||
//@flow | |||
import React from 'react'; | |||
import UsersSearch from '../../users/components/UsersSearch'; | |||
import SearchBox from '../../../components/controls/SearchBox'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -30,21 +30,19 @@ type Props = { | |||
}; | |||
*/ | |||
export default class MembersListHeader extends React.PureComponent { | |||
/*:: props: Props; */ | |||
render() { | |||
const { total } = this.props; | |||
return ( | |||
<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> | |||
); | |||
} |
@@ -4,9 +4,10 @@ exports[`should render with the total 1`] = ` | |||
<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" | |||
@@ -24,9 +25,10 @@ exports[`should render without the total 1`] = ` | |||
<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> | |||
`; |
@@ -19,7 +19,7 @@ | |||
*/ | |||
//@flow | |||
import React from 'react'; | |||
import { debounce, without } from 'lodash'; | |||
import { without } from 'lodash'; | |||
import TagsSelector from '../../../components/tags/TagsSelector'; | |||
import { searchProjectTags } from '../../../api/components'; | |||
@@ -42,13 +42,7 @@ const LIST_SIZE = 10; | |||
export default class MetaTagsSelector extends React.PureComponent { | |||
/*:: props: Props; */ | |||
/*:: state: State; */ | |||
constructor(props /*: Props */) { | |||
super(props); | |||
this.state = { searchResult: [] }; | |||
this.onSearch = debounce(this.onSearch, 250); | |||
} | |||
state /*: State */ = { searchResult: [] }; | |||
componentDidMount() { | |||
this.onSearch(''); | |||
@@ -56,12 +50,10 @@ export default class MetaTagsSelector extends React.PureComponent { | |||
onSearch = (query /*: string */) => { | |||
searchProjectTags({ | |||
q: query || '', | |||
q: query, | |||
ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100) | |||
}).then(result => { | |||
this.setState({ | |||
searchResult: result.tags | |||
}); | |||
this.setState({ searchResult: result.tags }); | |||
}); | |||
}; | |||
@@ -37,18 +37,14 @@ export default class Template extends React.PureComponent { | |||
topQualifiers: PropTypes.array.isRequired | |||
}; | |||
constructor(props) { | |||
super(props); | |||
this.state = { | |||
loading: false, | |||
users: [], | |||
groups: [], | |||
query: '', | |||
filter: 'all', | |||
selectedPermission: null | |||
}; | |||
this.requestHoldersDebounced = debounce(this.requestHolders, 250); | |||
} | |||
state = { | |||
loading: false, | |||
users: [], | |||
groups: [], | |||
query: '', | |||
filter: 'all', | |||
selectedPermission: null | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
@@ -140,9 +136,7 @@ export default class Template extends React.PureComponent { | |||
handleSearch = query => { | |||
this.setState({ query }); | |||
if (query.length === 0 || query.length > 2) { | |||
this.requestHoldersDebounced(query); | |||
} | |||
this.requestHolders(query); | |||
}; | |||
handleFilter = filter => { |
@@ -87,9 +87,7 @@ export const updateQuery = (query /*: string */ = '', organization /*: ?string * | |||
dispatch /*: Dispatch */ | |||
) => { | |||
dispatch({ type: UPDATE_QUERY, query }); | |||
if (query.length === 0 || query.length > 2) { | |||
dispatch(loadHolders(organization)); | |||
} | |||
dispatch(loadHolders(organization)); | |||
}; | |||
export const updateFilter = (filter /*: string */, organization /*: ?string */) => ( |
@@ -151,11 +151,7 @@ export default class App extends React.PureComponent { | |||
handleQueryChange = (query /*: string */) => { | |||
if (this.mounted) { | |||
this.setState({ query }, () => { | |||
if (query.length === 0 || query.length > 2) { | |||
this.loadHolders(); | |||
} | |||
}); | |||
this.setState({ query }, this.loadHolders); | |||
} | |||
}; | |||
@@ -20,79 +20,33 @@ | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import RadioToggle from '../../../../components/controls/RadioToggle'; | |||
import SearchBox from '../../../../components/controls/SearchBox'; | |||
import { translate, translateWithParameters } from '../../../../helpers/l10n'; | |||
export default class SearchForm extends React.PureComponent { | |||
static propTypes = { | |||
query: PropTypes.string, | |||
filter: PropTypes.oneOf(['all', 'users', 'groups']), | |||
onSearch: PropTypes.func, | |||
onFilter: PropTypes.func | |||
}; | |||
componentWillMount() { | |||
this.handleSubmit = this.handleSubmit.bind(this); | |||
this.handleSearch = this.handleSearch.bind(this); | |||
} | |||
handleSubmit(e) { | |||
e.preventDefault(); | |||
this.handleSearch(); | |||
} | |||
handleSearch() { | |||
const { value } = this.refs.searchInput; | |||
this.props.onSearch(value); | |||
} | |||
handleFilter(filter) { | |||
this.props.onFilter(filter); | |||
} | |||
render() { | |||
const { query, filter } = this.props; | |||
const filterOptions = [ | |||
{ value: 'all', label: translate('all') }, | |||
{ value: 'users', label: translate('users.page') }, | |||
{ value: 'groups', label: translate('user_groups.page') } | |||
]; | |||
return ( | |||
<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> | |||
); | |||
} |
@@ -79,7 +79,6 @@ export default function PageHeader(props: Props) { | |||
)} | |||
<SearchFilterContainer | |||
className="projects-topbar-item projects-topbar-item-search" | |||
isFavorite={props.isFavorite} | |||
organization={props.organization} | |||
query={props.query} |
@@ -17,7 +17,6 @@ exports[`should render correctly 1`] = ` | |||
view="overall" | |||
/> | |||
<SearchFilterContainer | |||
className="projects-topbar-item projects-topbar-item-search" | |||
query={ | |||
Object { | |||
"search": "test", | |||
@@ -57,7 +56,6 @@ exports[`should render correctly while loading 1`] = ` | |||
view="overall" | |||
/> | |||
<SearchFilterContainer | |||
className="projects-topbar-item projects-topbar-item-search" | |||
query={ | |||
Object { | |||
"search": "test", | |||
@@ -110,7 +108,6 @@ exports[`should render disabled sorting options for visualizations 1`] = ` | |||
</div> | |||
</Tooltip> | |||
<SearchFilterContainer | |||
className="projects-topbar-item projects-topbar-item-search" | |||
query={ | |||
Object { | |||
"search": "test", |
@@ -1,74 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { 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> | |||
); | |||
} | |||
} |
@@ -19,9 +19,9 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import { debounce } from 'lodash'; | |||
import { getFilterUrl } from './utils'; | |||
import SearchFilter from './SearchFilter'; | |||
import SearchBox from '../../../components/controls/SearchBox'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
className?: string; | |||
@@ -35,11 +35,6 @@ export default class SearchFilterContainer extends React.PureComponent<Props> { | |||
router: PropTypes.object.isRequired | |||
}; | |||
constructor(props: Props) { | |||
super(props); | |||
this.handleSearch = debounce(this.handleSearch, 250); | |||
} | |||
handleSearch = (userQuery?: string) => { | |||
const path = getFilterUrl(this.props, { search: userQuery }); | |||
this.context.router.push(path); | |||
@@ -47,11 +42,13 @@ export default class SearchFilterContainer extends React.PureComponent<Props> { | |||
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> | |||
); | |||
} | |||
} |
@@ -1,58 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import SearchFilter from '../SearchFilter'; | |||
import { change } from '../../../../helpers/testUtils'; | |||
it('should render correctly without any search query', () => { | |||
const wrapper = shallow(<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' }); | |||
}); |
@@ -21,17 +21,10 @@ import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import SearchFilterContainer from '../SearchFilterContainer'; | |||
// mocking lodash, because mocking timers is now working for some reason :'( | |||
jest.mock('lodash', () => { | |||
const lodash = require.requireActual('lodash'); | |||
lodash.debounce = (fn: Function) => (...args: any[]) => fn(args); | |||
return lodash; | |||
}); | |||
it('searches', () => { | |||
const push = jest.fn(); | |||
const wrapper = shallow(<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' } }); | |||
}); |
@@ -1,54 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display a help message when there is less than 2 characters 1`] = ` | |||
<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> | |||
`; |
@@ -1,8 +1,13 @@ | |||
// 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> | |||
`; |
@@ -41,19 +41,7 @@ | |||
.projects-topbar-item-search { | |||
position: relative; | |||
flex: 1; | |||
} | |||
.projects-topbar-item-search input { | |||
width: 100%; | |||
max-width: 300px; | |||
} | |||
.projects-topbar-item-search .note { | |||
position: absolute; | |||
top: 1px; | |||
left: 80px; | |||
line-height: var(--controlHeight); | |||
pointer-events: none; | |||
height: var(--controlHeight); | |||
} | |||
.projects-list .page-actions { |
@@ -29,6 +29,7 @@ import QualifierIcon from '../../components/shared/QualifierIcon'; | |||
import Tooltip from '../../components/controls/Tooltip'; | |||
import DateInput from '../../components/controls/DateInput'; | |||
import Select from '../../components/controls/Select'; | |||
import SearchBox from '../../components/controls/SearchBox'; | |||
export interface Props { | |||
analyzedBefore?: string; | |||
@@ -60,16 +61,6 @@ export default class Search extends React.PureComponent<Props, State> { | |||
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), | |||
@@ -206,19 +197,12 @@ export default class Search extends React.PureComponent<Props, State> { | |||
{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 |
@@ -20,7 +20,7 @@ | |||
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' }; | |||
@@ -67,7 +67,7 @@ it('updates analysis date', () => { | |||
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'); | |||
}); | |||
@@ -120,25 +120,12 @@ exports[`render qualifiers filter 1`] = ` | |||
<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" | |||
@@ -223,25 +210,12 @@ exports[`renders 1`] = ` | |||
<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" |
@@ -26,6 +26,7 @@ import { getCurrentUser } from '../../../store/rootReducer'; | |||
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 = { |
@@ -1,87 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
//@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> | |||
); | |||
} | |||
} |
@@ -1,35 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import UsersSearch from '../UsersSearch'; | |||
it('should render correctly', () => { | |||
const wrapper = shallow(<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(); | |||
}); |
@@ -1,82 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display a help message when there is less than 2 characters 1`] = ` | |||
<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> | |||
`; |
@@ -25,13 +25,15 @@ export default Marionette.ItemView.extend({ | |||
template: Template, | |||
ui: { | |||
hint: '.js-hint' | |||
hint: '.js-hint', | |||
reset: '.js-reset' | |||
}, | |||
events: { | |||
'submit #users-search-form': 'onFormSubmit', | |||
'search #users-search-query': 'initialOnKeyUp', | |||
'keyup #users-search-query': 'initialOnKeyUp' | |||
'keyup #users-search-query': 'initialOnKeyUp', | |||
'click .js-reset': 'onResetClick' | |||
}, | |||
initialize() { | |||
@@ -51,6 +53,7 @@ export default Marionette.ItemView.extend({ | |||
initialOnKeyUp() { | |||
const q = this.getQuery(); | |||
this.ui.hint.toggleClass('hidden', q.length !== 1); | |||
this.ui.reset.toggleClass('hidden', q.length === 0); | |||
this.debouncedOnKeyUp(); | |||
}, | |||
@@ -64,6 +67,7 @@ export default Marionette.ItemView.extend({ | |||
this.searchRequest.abort(); | |||
} | |||
this.ui.hint.toggleClass('hidden', q.length !== 1); | |||
this.ui.reset.toggleClass('hidden', q.length === 0); | |||
if (q.length !== 1) { | |||
this.searchRequest = this.search(q); | |||
} | |||
@@ -75,5 +79,14 @@ export default Marionette.ItemView.extend({ | |||
search(q) { | |||
return this.collection.fetch({ reset: true, data: { q } }); | |||
}, | |||
onResetClick(e) { | |||
e.preventDefault(); | |||
e.currentTarget.blur(); | |||
this.$('#users-search-query') | |||
.val('') | |||
.focus(); | |||
this.onKeyUp(); | |||
} | |||
}); |
@@ -1,8 +1,17 @@ | |||
<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> |
@@ -18,11 +18,11 @@ | |||
* 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; | |||
@@ -32,66 +32,38 @@ interface Props { | |||
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> | |||
); | |||
} |
@@ -5,15 +5,9 @@ exports[`should render correctly 1`] = ` | |||
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 |
@@ -9,10 +9,6 @@ | |||
white-space: nowrap; | |||
} | |||
.web-api-search .icon-search { | |||
color: var(--gray80); | |||
} | |||
.web-api-domain-header, | |||
.web-api-action-header { | |||
display: flex; |
@@ -25,6 +25,7 @@ import { translate } from '../../helpers/l10n'; | |||
import ItemTemplate from './templates/item.hbs'; | |||
import ListTemplate from './templates/list.hbs'; | |||
import './styles.css'; | |||
import '../controls/SearchBox.css'; | |||
let showError = null; | |||
@@ -160,7 +161,8 @@ const SelectListView = Backbone.View.extend({ | |||
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) { | |||
@@ -331,6 +333,7 @@ const SelectListView = Backbone.View.extend({ | |||
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(); | |||
@@ -352,6 +355,15 @@ const SelectListView = Backbone.View.extend({ | |||
} | |||
}, | |||
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(); |
@@ -4,10 +4,19 @@ | |||
<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"> |
@@ -21,6 +21,7 @@ | |||
import React from 'react'; | |||
import { difference } from 'lodash'; | |||
import MultiSelectOption from './MultiSelectOption'; | |||
import SearchBox from '../controls/SearchBox'; | |||
import { translate } from '../../helpers/l10n'; | |||
/*:: | |||
@@ -31,7 +32,8 @@ type Props = { | |||
onSearch: string => void, | |||
onSelect: string => void, | |||
onUnselect: string => void, | |||
validateSearchInput: string => string | |||
validateSearchInput: string => string, | |||
placeholder: string | |||
}; | |||
*/ | |||
@@ -104,8 +106,8 @@ export default class MultiSelect extends React.PureComponent { | |||
} | |||
}; | |||
handleSearchChange = ({ target } /*: { target: HTMLInputElement } */) => { | |||
this.onSearchQuery(this.props.validateSearchInput(target.value)); | |||
handleSearchChange = (value /*: string */) => { | |||
this.onSearchQuery(this.props.validateSearchInput(value)); | |||
}; | |||
handleElementHover = (element /*: string */) => { | |||
@@ -232,18 +234,12 @@ export default class MultiSelect extends React.PureComponent { | |||
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"> |
@@ -26,7 +26,8 @@ const props = { | |||
elements: [], | |||
onSearch: () => {}, | |||
onSelect: () => {}, | |||
onUnselect: () => {} | |||
onUnselect: () => {}, | |||
placeholder: '' | |||
}; | |||
const elements = ['foo', 'bar', 'baz']; |
@@ -5,21 +5,12 @@ exports[`should render multiselect with selected elements 1`] = ` | |||
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> | |||
@@ -44,21 +35,12 @@ exports[`should render multiselect with selected elements 2`] = ` | |||
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> | |||
@@ -101,21 +83,12 @@ exports[`should render multiselect with selected elements 3`] = ` | |||
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> | |||
@@ -158,21 +131,12 @@ exports[`should render multiselect with selected elements 4`] = ` | |||
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> |
@@ -19,39 +19,76 @@ | |||
*/ | |||
.search-box { | |||
position: relative; | |||
display: inline-block; | |||
vertical-align: middle; | |||
font-size: 0; | |||
white-space: nowrap; | |||
} | |||
.search-box, | |||
.search-box-input { | |||
vertical-align: middle; | |||
width: 250px; | |||
border: none !important; | |||
font-size: var(--baseFontSize); | |||
width: 100%; | |||
max-width: 300px; | |||
} | |||
.search-box-input ~ .note { | |||
opacity: 0; | |||
transition: opacity 0.3s ease; | |||
.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.touched ~ .note { | |||
.search-box-input::placeholder { | |||
color: var(--secondFontColor); | |||
opacity: 1; | |||
} | |||
.search-box-submit { | |||
display: inline-block; | |||
vertical-align: middle; | |||
.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-submit .icon-search:before { | |||
color: var(--secondFontColor); | |||
font-size: var(--mediumFontSize); | |||
.search-box-input::-ms-clear, | |||
.search-box-input::-ms-reveal { | |||
display: none; | |||
width: 0; | |||
height: 0; | |||
} | |||
.search-box-submit .icon-search-new { | |||
position: relative; | |||
.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 { |
@@ -0,0 +1,167 @@ | |||
/* | |||
* 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> | |||
); | |||
} | |||
} |
@@ -0,0 +1,84 @@ | |||
/* | |||
* 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(); | |||
}); |
@@ -0,0 +1,30 @@ | |||
// 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> | |||
`; |
@@ -0,0 +1,39 @@ | |||
/* | |||
* 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> | |||
); | |||
} |
@@ -19,11 +19,12 @@ | |||
*/ | |||
// @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'; | |||
@@ -68,8 +69,6 @@ export default class SetAssigneePopup extends React.PureComponent { | |||
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(); | |||
@@ -103,9 +102,8 @@ export default class SetAssigneePopup extends React.PureComponent { | |||
}); | |||
}; | |||
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, | |||
@@ -127,18 +125,13 @@ export default class SetAssigneePopup extends React.PureComponent { | |||
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 |
@@ -19,7 +19,7 @@ | |||
*/ | |||
//@flow | |||
import React from 'react'; | |||
import { debounce, without } from 'lodash'; | |||
import { without } from 'lodash'; | |||
import TagsSelector from '../../../components/tags/TagsSelector'; | |||
import { searchIssueTags } from '../../../api/issues'; | |||
@@ -44,13 +44,7 @@ const LIST_SIZE = 10; | |||
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; | |||
@@ -63,7 +57,7 @@ export default class SetIssueTagsPopup extends React.PureComponent { | |||
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> */) => { | |||
@@ -83,6 +77,7 @@ export default class SetIssueTagsPopup extends React.PureComponent { | |||
render() { | |||
return ( | |||
// $FlowFixMe `this.props.popupPosition` is passed from `BabelPopupHelper` | |||
<TagsSelector | |||
position={this.props.popupPosition} | |||
tags={this.state.searchResult} |
@@ -21,6 +21,7 @@ | |||
import React from 'react'; | |||
import BubblePopup from '../common/BubblePopup'; | |||
import MultiSelect from '../common/MultiSelect'; | |||
import { translate } from '../../helpers/l10n'; | |||
import './TagsList.css'; | |||
/*:: | |||
@@ -35,31 +36,26 @@ type Props = { | |||
}; | |||
*/ | |||
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, ''); | |||
} |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import TagsSelector from '../TagsSelector'; | |||
import TagsSelector, { validateTag } from '../TagsSelector'; | |||
const props = { | |||
position: { left: 0, top: 0 }, | |||
@@ -41,10 +41,9 @@ it('should render without tags at all', () => { | |||
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'); | |||
}); |
@@ -22,6 +22,7 @@ exports[`should render with selected tags 1`] = ` | |||
onSearch={[Function]} | |||
onSelect={[Function]} | |||
onUnselect={[Function]} | |||
placeholder="search.search_for_tags" | |||
selectedElements={ | |||
Array [ | |||
"bar", | |||
@@ -48,6 +49,7 @@ exports[`should render without tags at all 1`] = ` | |||
onSearch={[Function]} | |||
onSelect={[Function]} | |||
onUnselect={[Function]} | |||
placeholder="search.search_for_tags" | |||
selectedElements={Array []} | |||
validateSearchInput={[Function]} | |||
/> |
@@ -43,7 +43,7 @@ export class ButtonIcon extends React.PureComponent<ButtonIconProps> { | |||
}; | |||
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')} |
@@ -56,11 +56,18 @@ export function keydown(keyCode: number): void { | |||
} | |||
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> { |
@@ -834,9 +834,18 @@ property.sonar.branch.longLivedBranches.regex.description=Regular expression use | |||
# 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... | |||
#------------------------------------------------------------------------------ | |||
@@ -2125,6 +2134,7 @@ marketplace.enter_license_for_x=Enter your license key for {0} | |||
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... | |||
#------------------------------------------------------------------------------ | |||
@@ -2310,6 +2320,7 @@ api_documentation.deprecated_since_x=deprecated since {0} | |||
api_documentation.parameters=Parameters | |||
api_documentation.response_example=Response Example | |||
api_documentation.changelog=Changelog | |||
api_documentation.search=Search by name... | |||
#------------------------------------------------------------------------------ | |||
@@ -2318,6 +2329,8 @@ api_documentation.changelog=Changelog | |||
# | |||
#------------------------------------------------------------------------------ | |||
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... | |||
#------------------------------------------------------------------------------ | |||
@@ -2567,6 +2580,7 @@ branches.set_leak_period=Set Leak Period | |||
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... | |||
#------------------------------------------------------------------------------ |
@@ -31,12 +31,12 @@ import javax.annotation.Nullable; | |||
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.$; | |||
@@ -142,6 +142,18 @@ public class Navigation { | |||
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); |
@@ -0,0 +1,61 @@ | |||
/* | |||
* 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; | |||
} | |||
} |
@@ -49,7 +49,7 @@ public class MembersPage { | |||
} | |||
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; | |||
} | |||
@@ -21,19 +21,16 @@ | |||
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 { | |||
@@ -47,14 +44,14 @@ 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 | |||
@@ -64,7 +61,7 @@ public class OrganizationMembershipUiTest { | |||
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 | |||
@@ -86,7 +83,7 @@ public class OrganizationMembershipUiTest { | |||
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 | |||
@@ -103,7 +100,7 @@ public class OrganizationMembershipUiTest { | |||
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()) | |||
@@ -173,7 +170,7 @@ public class OrganizationMembershipUiTest { | |||
.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); | |||
} | |||
} |
@@ -20,37 +20,34 @@ | |||
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(); | |||
} | |||
/** | |||
@@ -58,19 +55,22 @@ public class ProjectBulkDeletionPageTest { | |||
*/ | |||
@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); | |||
} | |||
} |
@@ -21,11 +21,12 @@ package org.sonarqube.tests.sourceCode; | |||
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; | |||
@@ -38,27 +39,58 @@ public class ProjectCodeTest { | |||
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"); | |||
} | |||
} |
@@ -1,74 +0,0 @@ | |||
<?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> |
@@ -1,30 +0,0 @@ | |||
<?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> |
@@ -1,35 +0,0 @@ | |||
<?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> |
@@ -1,60 +0,0 @@ | |||
<?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> |
@@ -1,55 +0,0 @@ | |||
<?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> |