@@ -28,6 +28,7 @@ import { sortQualifiers } from './utils'; | |||
import type { Component, More, Results } from './utils'; | |||
import RecentHistory from '../../components/RecentHistory'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import ClockIcon from '../../../components/common/ClockIcon'; | |||
import { getSuggestions } from '../../../api/components'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { scrollToElement } from '../../../helpers/scrolling'; | |||
@@ -162,30 +163,34 @@ export default class Search extends React.PureComponent { | |||
}; | |||
search = (query: string) => { | |||
this.setState({ loading: true }); | |||
const recentlyBrowsed = RecentHistory.get().map(component => component.key); | |||
getSuggestions(query, recentlyBrowsed).then(response => { | |||
// compare `this.state.query` and `query` to handle two request done almost at the same time | |||
// in this case only the request that matches the current query should be taken | |||
if (this.mounted && this.state.query === query) { | |||
const results = {}; | |||
const more = {}; | |||
response.results.forEach(group => { | |||
results[group.q] = group.items.map(item => ({ ...item, qualifier: group.q })); | |||
more[group.q] = group.more; | |||
}); | |||
const list = this.getPlainComponentsList(results, more); | |||
this.setState(state => ({ | |||
loading: false, | |||
more, | |||
organizations: { ...state.organizations, ...keyBy(response.organizations, 'key') }, | |||
projects: { ...state.projects, ...keyBy(response.projects, 'key') }, | |||
results, | |||
selected: list.length > 0 ? list[0] : null, | |||
shortQuery: response.warning === 'short_input' | |||
})); | |||
} | |||
}); | |||
if (query.length === 0 || query.length >= 2) { | |||
this.setState({ loading: true }); | |||
const recentlyBrowsed = RecentHistory.get().map(component => component.key); | |||
getSuggestions(query, recentlyBrowsed).then(response => { | |||
// compare `this.state.query` and `query` to handle two request done almost at the same time | |||
// in this case only the request that matches the current query should be taken | |||
if (this.mounted && this.state.query === query) { | |||
const results = {}; | |||
const more = {}; | |||
response.results.forEach(group => { | |||
results[group.q] = group.items.map(item => ({ ...item, qualifier: group.q })); | |||
more[group.q] = group.more; | |||
}); | |||
const list = this.getPlainComponentsList(results, more); | |||
this.setState(state => ({ | |||
loading: false, | |||
more, | |||
organizations: { ...state.organizations, ...keyBy(response.organizations, 'key') }, | |||
projects: { ...state.projects, ...keyBy(response.projects, 'key') }, | |||
results, | |||
selected: list.length > 0 ? list[0] : null, | |||
shortQuery: response.warning === 'short_input' | |||
})); | |||
} | |||
}); | |||
} else { | |||
this.setState({ loading: false }); | |||
} | |||
}; | |||
searchMore = (qualifier: string) => { | |||
@@ -216,9 +221,7 @@ export default class Search extends React.PureComponent { | |||
handleQueryChange = (event: { currentTarget: HTMLInputElement }) => { | |||
const query = event.currentTarget.value; | |||
this.setState({ query, shortQuery: query.length === 1 }); | |||
if (query.length === 0 || query.length >= 2) { | |||
this.search(query); | |||
} | |||
this.search(query); | |||
}; | |||
selectPrevious = () => { | |||
@@ -359,15 +362,20 @@ export default class Search extends React.PureComponent { | |||
results={this.state.results} | |||
selected={this.state.selected} | |||
/> | |||
<div | |||
className="navbar-search-shortcut-hint" | |||
dangerouslySetInnerHTML={{ | |||
__html: translateWithParameters( | |||
'search.shortcut_hint', | |||
'<span class="shortcut-button shortcut-button-small">s</span>' | |||
) | |||
}} | |||
/> | |||
<div className="navbar-search-shortcut-hint"> | |||
<div className="pull-right"> | |||
<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>' | |||
) | |||
}} | |||
/> | |||
</div> | |||
</div>} | |||
</li> | |||
); |
@@ -38,8 +38,45 @@ type Props = {| | |||
selected: boolean | |||
|}; | |||
type State = { | |||
tooltipVisible: boolean | |||
}; | |||
const TOOLTIP_DELAY = 1000; | |||
export default class SearchResult extends React.PureComponent { | |||
interval: ?number; | |||
props: Props; | |||
state: State = { tooltipVisible: false }; | |||
componentDidMount() { | |||
if (this.props.selected) { | |||
this.scheduleTooltip(); | |||
} | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
if (!this.props.selected && nextProps.selected) { | |||
this.scheduleTooltip(); | |||
} else if (this.props.selected && !nextProps.selected) { | |||
this.unscheduleTooltip(); | |||
this.setState({ tooltipVisible: false }); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.unscheduleTooltip(); | |||
} | |||
scheduleTooltip = () => { | |||
this.interval = setTimeout(() => this.setState({ tooltipVisible: true }), TOOLTIP_DELAY); | |||
}; | |||
unscheduleTooltip = () => { | |||
if (this.interval) { | |||
clearInterval(this.interval); | |||
} | |||
}; | |||
handleMouseEnter = () => { | |||
this.props.onSelect(this.props.component.key); | |||
@@ -79,7 +116,11 @@ export default class SearchResult extends React.PureComponent { | |||
className={this.props.selected ? 'active' : undefined} | |||
key={component.key} | |||
ref={node => this.props.innerRef(component.key, node)}> | |||
<Tooltip mouseEnterDelay={1.0} overlay={component.key} placement="left"> | |||
<Tooltip | |||
mouseEnterDelay={TOOLTIP_DELAY / 1000} | |||
overlay={component.key} | |||
placement="left" | |||
visible={this.state.tooltipVisible}> | |||
<Link | |||
className="navbar-search-item-link" | |||
data-key={component.key} |
@@ -39,6 +39,8 @@ function render(props?: Object) { | |||
); | |||
} | |||
jest.useFakeTimers(); | |||
it('renders selected', () => { | |||
const wrapper = render(); | |||
expect(wrapper).toMatchSnapshot(); | |||
@@ -107,3 +109,17 @@ it('renders organizations', () => { | |||
wrapper.setProps({ appState: { organizationsEnabled: false } }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('shows tooltip after delay', () => { | |||
const wrapper = render(); | |||
expect(wrapper.find('Tooltip').prop('visible')).toBe(false); | |||
wrapper.setProps({ selected: true }); | |||
expect(wrapper.find('Tooltip').prop('visible')).toBe(false); | |||
jest.runAllTimers(); | |||
expect(wrapper.find('Tooltip').prop('visible')).toBe(true); | |||
wrapper.setProps({ selected: false }); | |||
expect(wrapper.find('Tooltip').prop('visible')).toBe(false); | |||
}); |
@@ -6,6 +6,7 @@ exports[`renders favorite 1`] = ` | |||
mouseEnterDelay={1} | |||
overlay="foo" | |||
placement="left" | |||
visible={false} | |||
> | |||
<Link | |||
className="navbar-search-item-link" | |||
@@ -51,6 +52,7 @@ exports[`renders match 1`] = ` | |||
mouseEnterDelay={1} | |||
overlay="foo" | |||
placement="left" | |||
visible={false} | |||
> | |||
<Link | |||
className="navbar-search-item-link" | |||
@@ -95,6 +97,7 @@ exports[`renders organizations 1`] = ` | |||
mouseEnterDelay={1} | |||
overlay="foo" | |||
placement="left" | |||
visible={false} | |||
> | |||
<Link | |||
className="navbar-search-item-link" | |||
@@ -144,6 +147,7 @@ exports[`renders organizations 2`] = ` | |||
mouseEnterDelay={1} | |||
overlay="foo" | |||
placement="left" | |||
visible={false} | |||
> | |||
<Link | |||
className="navbar-search-item-link" | |||
@@ -188,6 +192,7 @@ exports[`renders projects 1`] = ` | |||
mouseEnterDelay={1} | |||
overlay="qwe" | |||
placement="left" | |||
visible={false} | |||
> | |||
<Link | |||
className="navbar-search-item-link" | |||
@@ -237,6 +242,7 @@ exports[`renders recently browsed 1`] = ` | |||
mouseEnterDelay={1} | |||
overlay="foo" | |||
placement="left" | |||
visible={false} | |||
> | |||
<Link | |||
className="navbar-search-item-link" | |||
@@ -281,6 +287,7 @@ exports[`renders selected 1`] = ` | |||
mouseEnterDelay={1} | |||
overlay="foo" | |||
placement="left" | |||
visible={false} | |||
> | |||
<Link | |||
className="navbar-search-item-link" | |||
@@ -324,6 +331,7 @@ exports[`renders selected 2`] = ` | |||
mouseEnterDelay={1} | |||
overlay="foo" | |||
placement="left" | |||
visible={false} | |||
> | |||
<Link | |||
className="navbar-search-item-link" |
@@ -209,6 +209,7 @@ | |||
} | |||
.navbar-search-shortcut-hint { | |||
line-height: 16px; | |||
margin-top: 5px; | |||
padding: 5px 10px; | |||
border-top: 1px solid #e6e6e6; |
@@ -4716,23 +4716,23 @@ postcss-zindex@^2.0.1: | |||
postcss "^5.0.4" | |||
uniqs "^2.0.0" | |||
postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.1.2: | |||
version "5.2.8" | |||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.8.tgz#05720c49df23c79bda51fd01daeb1e9222e94390" | |||
postcss@^5.0.10, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.8, postcss@^5.2.17: | |||
version "5.2.17" | |||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.17.tgz#cf4f597b864d65c8a492b2eabe9d706c879c388b" | |||
dependencies: | |||
chalk "^1.1.3" | |||
js-base64 "^2.1.9" | |||
source-map "^0.5.6" | |||
supports-color "^3.1.2" | |||
supports-color "^3.2.3" | |||
postcss@^5.2.17: | |||
version "5.2.17" | |||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.17.tgz#cf4f597b864d65c8a492b2eabe9d706c879c388b" | |||
postcss@^5.0.11, postcss@^5.0.6, postcss@^5.1.2: | |||
version "5.2.8" | |||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.8.tgz#05720c49df23c79bda51fd01daeb1e9222e94390" | |||
dependencies: | |||
chalk "^1.1.3" | |||
js-base64 "^2.1.9" | |||
source-map "^0.5.6" | |||
supports-color "^3.2.3" | |||
supports-color "^3.1.2" | |||
prelude-ls@~1.1.2: | |||
version "1.1.2" |
@@ -29,7 +29,6 @@ biggest=Biggest | |||
blocker=Blocker | |||
bold=Bold | |||
branch=Branch | |||
browsed_recently=Browsed Recently | |||
build_date=Build date | |||
build_time=Build time | |||
calendar=Calendar | |||
@@ -135,6 +134,7 @@ projects_management=Projects Management | |||
quality_profile=Quality Profile | |||
raw=Raw | |||
recent_history=Recent History | |||
recently_browsed=Recently Browsed | |||
refresh=Refresh | |||
reload=Reload | |||
remove=Remove |