From c8727c5ee3660384f0a29f6da7c8ad3d60fd7932 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Mon, 22 May 2017 09:23:07 +0200 Subject: [PATCH] apply search feedback (#2083) --- .../main/js/app/components/search/Search.js | 80 ++++++++++--------- .../js/app/components/search/SearchResult.js | 43 +++++++++- .../search/__tests__/SearchResult-test.js | 16 ++++ .../__snapshots__/SearchResult-test.js.snap | 8 ++ .../src/main/less/components/navbar.less | 1 + server/sonar-web/yarn.lock | 16 ++-- .../resources/org/sonar/l10n/core.properties | 2 +- 7 files changed, 120 insertions(+), 46 deletions(-) diff --git a/server/sonar-web/src/main/js/app/components/search/Search.js b/server/sonar-web/src/main/js/app/components/search/Search.js index a138b2a1b35..adfd90a8b83 100644 --- a/server/sonar-web/src/main/js/app/components/search/Search.js +++ b/server/sonar-web/src/main/js/app/components/search/Search.js @@ -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} /> -
s' - ) - }} - /> +
+
+ + {translate('recently_browsed')} +
+
s' + ) + }} + /> +
} ); diff --git a/server/sonar-web/src/main/js/app/components/search/SearchResult.js b/server/sonar-web/src/main/js/app/components/search/SearchResult.js index 2765fa3f0f6..252c82fc441 100644 --- a/server/sonar-web/src/main/js/app/components/search/SearchResult.js +++ b/server/sonar-web/src/main/js/app/components/search/SearchResult.js @@ -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)}> - + { 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); +}); diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap index ce33642ae2c..f09e0116d75 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap @@ -6,6 +6,7 @@ exports[`renders favorite 1`] = ` mouseEnterDelay={1} overlay="foo" placement="left" + visible={false} >