diff options
author | Stas Vilchik <stas-vilchik@users.noreply.github.com> | 2017-05-22 09:23:07 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-05-22 09:23:07 +0200 |
commit | c8727c5ee3660384f0a29f6da7c8ad3d60fd7932 (patch) | |
tree | ec21b66c30c45dd3859c003cecdc66fb7683c558 /server/sonar-web/src/main | |
parent | e1c241af1858b92bb01fbec1a0c609429499fff2 (diff) | |
download | sonarqube-c8727c5ee3660384f0a29f6da7c8ad3d60fd7932.tar.gz sonarqube-c8727c5ee3660384f0a29f6da7c8ad3d60fd7932.zip |
apply search feedback (#2083)
Diffstat (limited to 'server/sonar-web/src/main')
5 files changed, 111 insertions, 37 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} /> - <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> ); 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)}> - <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} diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.js b/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.js index ef4aa10dafb..70d38fd2ad5 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.js +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.js @@ -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); +}); 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} > <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" diff --git a/server/sonar-web/src/main/less/components/navbar.less b/server/sonar-web/src/main/less/components/navbar.less index 6e3878b0c82..fd7ac938e48 100644 --- a/server/sonar-web/src/main/less/components/navbar.less +++ b/server/sonar-web/src/main/less/components/navbar.less @@ -209,6 +209,7 @@ } .navbar-search-shortcut-hint { + line-height: 16px; margin-top: 5px; padding: 5px 10px; border-top: 1px solid #e6e6e6; |