diff options
8 files changed, 151 insertions, 87 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/js/helpers/l10n.js b/server/sonar-web/src/main/js/helpers/l10n.js index 40f3da8a7ad..e4eb1f44d17 100644 --- a/server/sonar-web/src/main/js/helpers/l10n.js +++ b/server/sonar-web/src/main/js/helpers/l10n.js @@ -19,7 +19,7 @@ */ /* @flow */ import moment from 'moment'; -import { request } from './request'; +import { getJSON } from './request'; let messages = {}; @@ -52,31 +52,6 @@ function getPreferredLanguage() { return window.navigator.languages ? window.navigator.languages[0] : window.navigator.language; } -function makeRequest(params) { - const url = '/api/l10n/index'; - - return request(url).setData(params).submit().then(response => { - switch (response.status) { - case 200: - return response.json(); - case 304: - return JSON.parse(localStorage.getItem('l10n.bundle') || '{}'); - case 401: - window.location = - window.baseUrl + - '/sessions/new?return_to=' + - encodeURIComponent( - window.location.pathname + window.location.search + window.location.hash - ); - // return unresolved promise to stop the promise chain - // anyway the page will be reloaded - return new Promise(() => {}); - default: - throw new Error('Unexpected status code: ' + response.status); - } - }); -} - function checkCachedBundle() { const cached = localStorage.getItem('l10n.bundle'); @@ -92,34 +67,49 @@ function checkCachedBundle() { } } +function getL10nBundle(params) { + const url = '/api/l10n/index'; + return getJSON(url, params); +} + export function requestMessages() { - const currentLocale = getPreferredLanguage(); + const browserLocale = getPreferredLanguage(); const cachedLocale = localStorage.getItem('l10n.locale'); const params = {}; - if (currentLocale) { - params.locale = currentLocale; + if (browserLocale) { + params.locale = browserLocale; } - if (cachedLocale === currentLocale) { + if (browserLocale.startsWith(cachedLocale)) { const bundleTimestamp = localStorage.getItem('l10n.timestamp'); if (bundleTimestamp !== null && checkCachedBundle()) { params.ts = bundleTimestamp; } } - return makeRequest(params).then(response => { - try { - const currentTimestamp = moment().format('YYYY-MM-DDTHH:mm:ssZZ'); - localStorage.setItem('l10n.timestamp', currentTimestamp); - localStorage.setItem('l10n.locale', response.effectiveLocale); - localStorage.setItem('l10n.bundle', JSON.stringify(response.messages)); - } catch (e) { - // do nothing + return getL10nBundle(params).then( + ({ effectiveLocale, messages }) => { + try { + const currentTimestamp = moment().format('YYYY-MM-DDTHH:mm:ssZZ'); + localStorage.setItem('l10n.timestamp', currentTimestamp); + localStorage.setItem('l10n.locale', effectiveLocale); + localStorage.setItem('l10n.bundle', JSON.stringify(messages)); + } catch (e) { + // do nothing + } + configureMoment(effectiveLocale); + resetBundle(messages); + }, + ({ response }) => { + if (response && response.status === 304) { + configureMoment(cachedLocale || browserLocale); + resetBundle(JSON.parse(localStorage.getItem('l10n.bundle') || '{}')); + } else { + throw new Error('Unexpected status code: ' + response.status); + } } - configureMoment(response.effectiveLocale); - resetBundle(response.messages); - }); + ); } export function resetBundle(bundle: Object) { 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; diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index 3936a65ab63..f424d809212 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -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" diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 890acc19a6b..4b45a83585e 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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 |