aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/app/components/search/Search.js80
-rw-r--r--server/sonar-web/src/main/js/app/components/search/SearchResult.js43
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.js16
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap8
-rw-r--r--server/sonar-web/src/main/js/helpers/l10n.js72
-rw-r--r--server/sonar-web/src/main/less/components/navbar.less1
-rw-r--r--server/sonar-web/yarn.lock16
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties2
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