]> source.dussan.org Git - sonarqube.git/commitdiff
apply search feedback (#2083)
authorStas Vilchik <stas-vilchik@users.noreply.github.com>
Mon, 22 May 2017 07:23:07 +0000 (09:23 +0200)
committerGitHub <noreply@github.com>
Mon, 22 May 2017 07:23:07 +0000 (09:23 +0200)
server/sonar-web/src/main/js/app/components/search/Search.js
server/sonar-web/src/main/js/app/components/search/SearchResult.js
server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.js
server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap
server/sonar-web/src/main/less/components/navbar.less
server/sonar-web/yarn.lock
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index a138b2a1b35b2a76251b8ea28be8120e690c845d..adfd90a8b833b0172d456506299c86a6e824923d 100644 (file)
@@ -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>
     );
index 2765fa3f0f629e82ea901be21c653f38c2b8092d..252c82fc44175709e8049472c395fffeec52de5c 100644 (file)
@@ -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}
index ef4aa10dafb8f786780e84e21fedddd5fb4cda8f..70d38fd2ad519d4a103aa46864d41a0071aa6ec8 100644 (file)
@@ -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);
+});
index ce33642ae2cf9a5a8fef3736e86e45105e14e741..f09e0116d7513ed09b6eccfd7f01952b7bcf1e48 100644 (file)
@@ -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"
index 6e3878b0c822153ef5e6df1ca934e3ba0ab4187e..fd7ac938e48f3cccdf2202a45179e92b23b15742 100644 (file)
 }
 
 .navbar-search-shortcut-hint {
+  line-height: 16px;
   margin-top: 5px;
   padding: 5px 10px;
   border-top: 1px solid #e6e6e6;
index 3936a65ab63dee97d7e45fd340181f9ebd59dd5b..f424d809212cc57830b443e10cea47b950eb1371 100644 (file)
@@ -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"
index 890acc19a6bd0d5aa7e3a73d37618ce4bcf3d5bf..4b45a83585ea05f5ba7d17fefb40552145721aff 100644 (file)
@@ -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