aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorStas Vilchik <stas-vilchik@users.noreply.github.com>2017-05-11 18:17:15 +0200
committerGitHub <noreply@github.com>2017-05-11 18:17:15 +0200
commitf6554b56731131f5d97b6d2f22801237250d8920 (patch)
tree722cfe62f2a6cb0e09b12a08aacf112162583ecf /server
parentc1a942976f2ba145f99c4256f7e36c8917554bcb (diff)
downloadsonarqube-f6554b56731131f5d97b6d2f22801237250d8920.tar.gz
sonarqube-f6554b56731131f5d97b6d2f22801237250d8920.zip
apply search feedback (#2054)
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/api/components.js7
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js4
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js10
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavSearchForm-test.js.snap262
-rw-r--r--server/sonar-web/src/main/js/app/components/search/Search.js (renamed from server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearchForm.js)268
-rw-r--r--server/sonar-web/src/main/js/app/components/search/SearchResult.js (renamed from server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearchFormComponent.js)46
-rw-r--r--server/sonar-web/src/main/js/app/components/search/SearchResults.js83
-rw-r--r--server/sonar-web/src/main/js/app/components/search/SearchShowMore.js78
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js (renamed from server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavSearchForm-test.js)40
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.js (renamed from server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavSearchFormComponent-test.js)4
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/SearchResults-test.js70
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.js.snap17
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap (renamed from server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavSearchFormComponent-test.js.snap)71
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap84
-rw-r--r--server/sonar-web/src/main/js/app/components/search/utils.js42
-rw-r--r--server/sonar-web/src/main/js/components/controls/FavoriteBase.js6
-rw-r--r--server/sonar-web/src/main/less/components/dropdowns.less4
-rw-r--r--server/sonar-web/src/main/less/components/menu.less27
-rw-r--r--server/sonar-web/src/main/less/components/navbar.less36
-rw-r--r--server/sonar-web/src/main/less/components/ui.less8
20 files changed, 643 insertions, 524 deletions
diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js
index 9552a20dfce..867100c4fae 100644
--- a/server/sonar-web/src/main/js/api/components.js
+++ b/server/sonar-web/src/main/js/api/components.js
@@ -192,11 +192,14 @@ export type SuggestionsResponse = {
};
export const getSuggestions = (
- query: string,
+ query?: string,
recentlyBrowsed?: Array<string>,
more?: string
): Promise<SuggestionsResponse> => {
- const data: Object = { s: query };
+ const data: Object = {};
+ if (query) {
+ data.s = query;
+ }
if (recentlyBrowsed) {
data.recentlyBrowsed = recentlyBrowsed.join();
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js
index 033bd787f7c..fca0171d9ec 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNav.js
@@ -22,7 +22,7 @@ import { connect } from 'react-redux';
import GlobalNavBranding from './GlobalNavBranding';
import GlobalNavMenu from './GlobalNavMenu';
import GlobalNavUser from './GlobalNavUser';
-import GlobalNavSearchForm from './GlobalNavSearchForm';
+import Search from '../../search/Search';
import ShortcutsHelpView from './ShortcutsHelpView';
import { getCurrentUser, getAppState } from '../../../../store/rootReducer';
@@ -63,7 +63,7 @@ class GlobalNav extends React.PureComponent {
<GlobalNavMenu {...this.props} />
<ul className="nav navbar-nav navbar-right">
- <GlobalNavSearchForm {...this.props} />
+ <Search {...this.props} />
<li>
<a className="navbar-help" onClick={this.openHelp} href="#">
<svg width="16" height="16">
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js
index 8fd6fc4b8e9..6d60902e1c8 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js
+++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavUser.js
@@ -58,6 +58,16 @@ class GlobalNavUser extends React.PureComponent {
<Avatar email={currentUser.email} name={currentUser.name} size={24} />
</a>
<ul className="dropdown-menu dropdown-menu-right">
+ <li className="dropdown-item">
+ <div className="text-ellipsis text-muted" title={currentUser.name}>
+ <strong>{currentUser.name}</strong>
+ </div>
+ {currentUser.email != null &&
+ <div className="little-spacer-top text-ellipsis text-muted" title={currentUser.email}>
+ {currentUser.email}
+ </div>}
+ </li>
+ <li className="divider" />
<li>
<Link to="/account">{translate('my_account.page')}</Link>
</li>
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavSearchForm-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavSearchForm-test.js.snap
deleted file mode 100644
index faf3d85dffe..00000000000
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavSearchForm-test.js.snap
+++ /dev/null
@@ -1,262 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders "Show More" link 1`] = `
-<ul
- className="menu"
->
- <li
- className="dropdown-header"
- >
- qualifiers.TRK
- </li>
- <GlobalNavSearchFormComponent
- appState={
- Object {
- "organizationsEnabled": false,
- }
- }
- component={
- Object {
- "key": "foo",
- "name": "foo",
- "qualifier": "TRK",
- }
- }
- innerRef={[Function]}
- onClose={[Function]}
- onSelect={[Function]}
- organizations={Object {}}
- projects={Object {}}
- selected={false}
- />
- <GlobalNavSearchFormComponent
- appState={
- Object {
- "organizationsEnabled": false,
- }
- }
- component={
- Object {
- "key": "bar",
- "name": "bar",
- "qualifier": "TRK",
- }
- }
- innerRef={[Function]}
- onClose={[Function]}
- onSelect={[Function]}
- organizations={Object {}}
- projects={Object {}}
- selected={false}
- />
- <li
- className="menu-footer"
- >
- <DeferredSpinner
- className="navbar-search-icon"
- loading={false}
- timeout={100}
- >
- <a
- data-qualifier="TRK"
- href="#"
- onClick={[Function]}
- >
- show_more
- </a>
- </DeferredSpinner>
- </li>
- <li
- className="divider"
- />
- <li
- className="dropdown-header"
- >
- qualifiers.BRC
- </li>
- <GlobalNavSearchFormComponent
- appState={
- Object {
- "organizationsEnabled": false,
- }
- }
- component={
- Object {
- "key": "qwe",
- "name": "qwe",
- "qualifier": "BRC",
- }
- }
- innerRef={[Function]}
- onClose={[Function]}
- onSelect={[Function]}
- organizations={Object {}}
- projects={Object {}}
- selected={false}
- />
- <GlobalNavSearchFormComponent
- appState={
- Object {
- "organizationsEnabled": false,
- }
- }
- component={
- Object {
- "key": "qux",
- "name": "qux",
- "qualifier": "BRC",
- }
- }
- innerRef={[Function]}
- onClose={[Function]}
- onSelect={[Function]}
- organizations={Object {}}
- projects={Object {}}
- selected={false}
- />
-</ul>
-`;
-
-exports[`renders different components and dividers between them 1`] = `
-<ul
- className="menu"
->
- <li
- className="dropdown-header"
- >
- qualifiers.TRK
- </li>
- <GlobalNavSearchFormComponent
- appState={
- Object {
- "organizationsEnabled": false,
- }
- }
- component={
- Object {
- "key": "foo",
- "name": "foo",
- "qualifier": "TRK",
- }
- }
- innerRef={[Function]}
- onClose={[Function]}
- onSelect={[Function]}
- organizations={Object {}}
- projects={Object {}}
- selected={false}
- />
- <GlobalNavSearchFormComponent
- appState={
- Object {
- "organizationsEnabled": false,
- }
- }
- component={
- Object {
- "key": "bar",
- "name": "bar",
- "qualifier": "TRK",
- }
- }
- innerRef={[Function]}
- onClose={[Function]}
- onSelect={[Function]}
- organizations={Object {}}
- projects={Object {}}
- selected={false}
- />
- <li
- className="divider"
- />
- <li
- className="dropdown-header"
- >
- qualifiers.BRC
- </li>
- <GlobalNavSearchFormComponent
- appState={
- Object {
- "organizationsEnabled": false,
- }
- }
- component={
- Object {
- "key": "qwe",
- "name": "qwe",
- "qualifier": "BRC",
- }
- }
- innerRef={[Function]}
- onClose={[Function]}
- onSelect={[Function]}
- organizations={Object {}}
- projects={Object {}}
- selected={false}
- />
- <GlobalNavSearchFormComponent
- appState={
- Object {
- "organizationsEnabled": false,
- }
- }
- component={
- Object {
- "key": "qux",
- "name": "qux",
- "qualifier": "BRC",
- }
- }
- innerRef={[Function]}
- onClose={[Function]}
- onSelect={[Function]}
- organizations={Object {}}
- projects={Object {}}
- selected={false}
- />
- <li
- className="divider"
- />
- <li
- className="dropdown-header"
- >
- qualifiers.FIL
- </li>
- <GlobalNavSearchFormComponent
- appState={
- Object {
- "organizationsEnabled": false,
- }
- }
- component={
- Object {
- "key": "zux",
- "name": "zux",
- "qualifier": "FIL",
- }
- }
- innerRef={[Function]}
- onClose={[Function]}
- onSelect={[Function]}
- organizations={Object {}}
- projects={Object {}}
- selected={false}
- />
-</ul>
-`;
-
-exports[`shows warning about short input 1`] = `
-<span
- className="navbar-search-input-hint"
->
- select2.tooShort.2
-</span>
-`;
-
-exports[`shows warning about short input 2`] = `
-<span
- className="navbar-search-input-hint is-shifted"
->
- select2.tooShort.2
-</span>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearchForm.js b/server/sonar-web/src/main/js/app/components/search/Search.js
index 469a0e673fd..a138b2a1b35 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearchForm.js
+++ b/server/sonar-web/src/main/js/app/components/search/Search.js
@@ -21,16 +21,17 @@
import React from 'react';
import classNames from 'classnames';
import key from 'keymaster';
-import { debounce, groupBy, keyBy, sortBy, uniqBy } from 'lodash';
-import GlobalNavSearchFormComponent from './GlobalNavSearchFormComponent';
-import type { Component } from './GlobalNavSearchFormComponent';
-import RecentHistory from '../../RecentHistory';
-import DeferredSpinner from '../../../../components/common/DeferredSpinner';
-import { getSuggestions } from '../../../../api/components';
-import { getFavorites } from '../../../../api/favorites';
-import { translate, translateWithParameters } from '../../../../helpers/l10n';
-import { scrollToElement } from '../../../../helpers/scrolling';
-import { getProjectUrl } from '../../../../helpers/urls';
+import { debounce, keyBy, uniqBy } from 'lodash';
+import SearchResults from './SearchResults';
+import SearchResult from './SearchResult';
+import { sortQualifiers } from './utils';
+import type { Component, More, Results } from './utils';
+import RecentHistory from '../../components/RecentHistory';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { getSuggestions } from '../../../api/components';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { scrollToElement } from '../../../helpers/scrolling';
+import { getProjectUrl } from '../../../helpers/urls';
type Props = {|
appState: { organizationsEnabled: boolean },
@@ -40,19 +41,17 @@ type Props = {|
type State = {
loading: boolean,
loadingMore: ?string,
- more: { [string]: number },
+ more: More,
open: boolean,
organizations: { [string]: { name: string } },
projects: { [string]: { name: string } },
query: string,
- results: { [qualifier: string]: Array<Component> },
+ results: Results,
selected: ?string,
shortQuery: boolean
};
-const ORDER = ['DEV', 'VW', 'SVW', 'TRK', 'BRC', 'FIL', 'UTS'];
-
-export default class GlobalNavSearchForm extends React.PureComponent {
+export default class Search extends React.PureComponent {
input: HTMLElement;
mounted: boolean;
node: HTMLElement;
@@ -68,9 +67,6 @@ export default class GlobalNavSearchForm extends React.PureComponent {
super(props);
this.nodes = {};
this.search = debounce(this.search, 250);
- this.fetchFavoritesAndRecentlyBrowsed = debounce(this.fetchFavoritesAndRecentlyBrowsed, 250, {
- leading: true
- });
this.state = {
loading: false,
loadingMore: null,
@@ -92,7 +88,6 @@ export default class GlobalNavSearchForm extends React.PureComponent {
this.openSearch();
return false;
});
- this.fetchFavoritesAndRecentlyBrowsed();
}
componentWillUpdate() {
@@ -113,40 +108,49 @@ export default class GlobalNavSearchForm extends React.PureComponent {
handleClickOutside = (event: { target: HTMLElement }) => {
if (!this.node || !this.node.contains(event.target)) {
- this.closeSearch();
+ this.closeSearch(false);
}
};
openSearch = () => {
window.addEventListener('click', this.handleClickOutside);
- if (!this.state.open) {
- this.fetchFavoritesAndRecentlyBrowsed();
+ if (!this.state.open && !this.state.query) {
+ this.search('');
}
this.setState({ open: true });
};
- closeSearch = () => {
+ closeSearch = (clear: boolean = true) => {
if (this.input) {
this.input.blur();
}
window.removeEventListener('click', this.handleClickOutside);
- this.setState({
- more: {},
- open: false,
- organizations: {},
- projects: {},
- query: '',
- results: {},
- selected: null,
- shortQuery: false
- });
+ this.setState(
+ clear
+ ? {
+ more: {},
+ open: false,
+ organizations: {},
+ projects: {},
+ query: '',
+ results: {},
+ selected: null,
+ shortQuery: false
+ }
+ : {
+ open: false
+ }
+ );
};
- getPlainComponentsList = (results: { [qualifier: string]: Array<Component> }): Array<Component> =>
- this.sortQualifiers(Object.keys(results)).reduce(
- (components, qualifier) => [...components, ...results[qualifier]],
- []
- );
+ getPlainComponentsList = (results: Results, more: More): Array<string> =>
+ sortQualifiers(Object.keys(results)).reduce((components, qualifier) => {
+ const next = [...components, ...results[qualifier].map(component => component.key)];
+ if (more[qualifier]) {
+ next.push('qualifier###' + qualifier);
+ }
+ return next;
+ }, []);
mergeWithRecentlyBrowsed = (components: Array<Component>) => {
const recentlyBrowsed = RecentHistory.get().map(component => ({
@@ -157,49 +161,27 @@ export default class GlobalNavSearchForm extends React.PureComponent {
return uniqBy([...components, ...recentlyBrowsed], 'key');
};
- fetchFavoritesAndRecentlyBrowsed = () => {
- const done = (components: Array<Component>) => {
- const results = groupBy(this.mergeWithRecentlyBrowsed(components), 'qualifier');
- const list = this.getPlainComponentsList(results);
- this.setState({
- loading: false,
- more: {},
- results,
- selected: list.length > 0 ? list[0].key : null
- });
- };
-
- if (this.props.currentUser.isLoggedIn) {
- this.setState({ loading: true });
- getFavorites().then(response => {
- if (this.mounted) {
- done(response.favorites.map(component => ({ ...component, isFavorite: true })));
- }
- });
- } else {
- done([]);
- }
- };
-
search = (query: string) => {
this.setState({ loading: true });
const recentlyBrowsed = RecentHistory.get().map(component => component.key);
getSuggestions(query, recentlyBrowsed).then(response => {
- if (this.mounted) {
+ // 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);
+ 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].key : null,
+ selected: list.length > 0 ? list[0] : null,
shortQuery: response.warning === 'short_input'
}));
}
@@ -207,57 +189,67 @@ export default class GlobalNavSearchForm extends React.PureComponent {
};
searchMore = (qualifier: string) => {
- this.setState({ loading: true, loadingMore: qualifier });
- const recentlyBrowsed = RecentHistory.get().map(component => component.key);
- getSuggestions(this.state.query, recentlyBrowsed, qualifier).then(response => {
- if (this.mounted) {
- const group = response.results.find(group => group.q === qualifier);
- const moreResults = (group ? group.items : []).map(item => ({ ...item, qualifier }));
- this.setState(state => ({
- loading: false,
- loadingMore: null,
- more: { ...state.more, [qualifier]: 0 },
- organizations: { ...state.organizations, ...keyBy(response.organizations, 'key') },
- projects: { ...state.projects, ...keyBy(response.projects, 'key') },
- results: {
- ...state.results,
- [qualifier]: uniqBy([...state.results[qualifier], ...moreResults], 'key')
- }
- }));
- }
- });
+ if (this.state.query.length !== 1) {
+ this.setState({ loading: true, loadingMore: qualifier });
+ const recentlyBrowsed = RecentHistory.get().map(component => component.key);
+ getSuggestions(this.state.query, recentlyBrowsed, qualifier).then(response => {
+ if (this.mounted) {
+ const group = response.results.find(group => group.q === qualifier);
+ const moreResults = (group ? group.items : []).map(item => ({ ...item, qualifier }));
+ this.setState(state => ({
+ loading: false,
+ loadingMore: null,
+ more: { ...state.more, [qualifier]: 0 },
+ organizations: { ...state.organizations, ...keyBy(response.organizations, 'key') },
+ projects: { ...state.projects, ...keyBy(response.projects, 'key') },
+ results: {
+ ...state.results,
+ [qualifier]: uniqBy([...state.results[qualifier], ...moreResults], 'key')
+ },
+ selected: moreResults.length > 0 ? moreResults[0].key : state.selected
+ }));
+ }
+ });
+ }
};
handleQueryChange = (event: { currentTarget: HTMLInputElement }) => {
const query = event.currentTarget.value;
this.setState({ query, shortQuery: query.length === 1 });
- if (query.length === 0) {
- this.fetchFavoritesAndRecentlyBrowsed();
- } else if (query.length >= 2) {
+ if (query.length === 0 || query.length >= 2) {
this.search(query);
}
};
selectPrevious = () => {
- this.setState((state: State) => {
- const list = this.getPlainComponentsList(state.results);
- const index = list.findIndex(component => component.key === state.selected);
- return index > 0 ? { selected: list[index - 1].key } : undefined;
+ this.setState(({ more, results, selected }: State) => {
+ if (selected) {
+ const list = this.getPlainComponentsList(results, more);
+ const index = list.indexOf(selected);
+ return index > 0 ? { selected: list[index - 1] } : undefined;
+ }
});
};
selectNext = () => {
- this.setState((state: State) => {
- const list = this.getPlainComponentsList(state.results);
- const index = list.findIndex(component => component.key === state.selected);
- return index >= 0 && index < list.length - 1 ? { selected: list[index + 1].key } : undefined;
+ this.setState(({ more, results, selected }: State) => {
+ if (selected) {
+ const list = this.getPlainComponentsList(results, more);
+ const index = list.indexOf(selected);
+ return index >= 0 && index < list.length - 1 ? { selected: list[index + 1] } : undefined;
+ }
});
};
openSelected = () => {
- if (this.state.selected) {
- this.context.router.push(getProjectUrl(this.state.selected));
- this.closeSearch();
+ const { selected } = this.state;
+ if (selected) {
+ if (selected.startsWith('qualifier###')) {
+ this.searchMore(selected.substr(12));
+ } else {
+ this.context.router.push(getProjectUrl(selected));
+ this.closeSearch();
+ }
}
};
@@ -295,23 +287,12 @@ export default class GlobalNavSearchForm extends React.PureComponent {
this.setState({ selected });
};
- handleMoreClick = (event: MouseEvent & { currentTarget: HTMLElement }) => {
- event.preventDefault();
- event.stopPropagation();
- event.currentTarget.blur();
- const { qualifier } = event.currentTarget.dataset;
- this.searchMore(qualifier);
- };
-
- sortQualifiers = (qualifiers: Array<string>) =>
- sortBy(qualifiers, qualifier => ORDER.indexOf(qualifier));
-
innerRef = (component: string, node: HTMLElement) => {
this.nodes[component] = node;
};
- renderComponent = (component: Component) => (
- <GlobalNavSearchFormComponent
+ renderResult = (component: Component) => (
+ <SearchResult
appState={this.props.appState}
component={component}
innerRef={this.innerRef}
@@ -324,47 +305,11 @@ export default class GlobalNavSearchForm extends React.PureComponent {
/>
);
- renderComponents = () => {
- const qualifiers = Object.keys(this.state.results);
- const renderedComponents = [];
-
- this.sortQualifiers(qualifiers).forEach(qualifier => {
- const components = this.state.results[qualifier];
-
- if (components.length > 0 && renderedComponents.length > 0) {
- renderedComponents.push(<li key={`divider-${qualifier}`} className="divider" />);
- }
-
- if (components.length > 0) {
- renderedComponents.push(
- <li key={`header-${qualifier}`} className="dropdown-header">
- {translate('qualifiers', qualifier)}
- </li>
- );
- }
-
- components.forEach(component => {
- renderedComponents.push(this.renderComponent(component));
- });
-
- const more = this.state.more[qualifier];
- if (more != null && more > 0) {
- renderedComponents.push(
- <li key={`more-${qualifier}`} className="menu-footer">
- <DeferredSpinner
- className="navbar-search-icon"
- loading={this.state.loadingMore === qualifier}>
- <a data-qualifier={qualifier} href="#" onClick={this.handleMoreClick}>
- {translate('show_more')}
- </a>
- </DeferredSpinner>
- </li>
- );
- }
- });
-
- return renderedComponents;
- };
+ renderNoResults = () => (
+ <div className="navbar-search-no-results">
+ {translateWithParameters('no_results_for_x', this.state.query)}
+ </div>
+ );
render() {
const dropdownClassName = classNames('dropdown', 'navbar-search', { open: this.state.open });
@@ -403,13 +348,24 @@ export default class GlobalNavSearchForm extends React.PureComponent {
<div
className="dropdown-menu dropdown-menu-right global-navbar-search-dropdown"
ref={node => (this.node = node)}>
- <ul className="menu">
- {this.renderComponents()}
- </ul>
+ <SearchResults
+ allowMore={this.state.query.length !== 1}
+ loadingMore={this.state.loadingMore}
+ more={this.state.more}
+ onMoreClick={this.searchMore}
+ onSelect={this.handleSelect}
+ renderNoResults={this.renderNoResults}
+ renderResult={this.renderResult}
+ results={this.state.results}
+ selected={this.state.selected}
+ />
<div
className="navbar-search-shortcut-hint"
dangerouslySetInnerHTML={{
- __html: translateWithParameters('search.shortcut_hint', 's')
+ __html: translateWithParameters(
+ 'search.shortcut_hint',
+ '<span class="shortcut-button shortcut-button-small">s</span>'
+ )
}}
/>
</div>}
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearchFormComponent.js b/server/sonar-web/src/main/js/app/components/search/SearchResult.js
index 9bf2aa3dfc5..2765fa3f0f6 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavSearchFormComponent.js
+++ b/server/sonar-web/src/main/js/app/components/search/SearchResult.js
@@ -20,22 +20,12 @@
// @flow
import React from 'react';
import { Link } from 'react-router';
-import FavoriteIcon from '../../../../components/common/FavoriteIcon';
-import QualifierIcon from '../../../../components/shared/QualifierIcon';
-import ClockIcon from '../../../../components/common/ClockIcon';
-import Tooltip from '../../../../components/controls/Tooltip';
-import { getProjectUrl } from '../../../../helpers/urls';
-
-export type Component = {
- isFavorite?: boolean,
- isRecentlyBrowsed?: boolean,
- key: string,
- match?: string,
- name: string,
- organization?: string,
- project?: string,
- qualifier: string
-};
+import type { Component } from './utils';
+import FavoriteIcon from '../../../components/common/FavoriteIcon';
+import QualifierIcon from '../../../components/shared/QualifierIcon';
+import ClockIcon from '../../../components/common/ClockIcon';
+import Tooltip from '../../../components/controls/Tooltip';
+import { getProjectUrl } from '../../../helpers/urls';
type Props = {|
appState: { organizationsEnabled: boolean },
@@ -48,7 +38,7 @@ type Props = {|
selected: boolean
|};
-export default class GlobalNavSearchFormComponent extends React.PureComponent {
+export default class SearchResult extends React.PureComponent {
props: Props;
handleMouseEnter = () => {
@@ -65,7 +55,9 @@ export default class GlobalNavSearchFormComponent extends React.PureComponent {
}
const organization = this.props.organizations[component.organization];
- return organization ? <div className="pull-right text-muted-2">{organization.name}</div> : null;
+ return organization
+ ? <div className="navbar-search-item-right text-muted-2">{organization.name}</div>
+ : null;
};
renderProject = (component: Component) => {
@@ -74,7 +66,9 @@ export default class GlobalNavSearchFormComponent extends React.PureComponent {
}
const project = this.props.projects[component.project];
- return project ? <div className="pull-right text-muted-2">{project.name}</div> : null;
+ return project
+ ? <div className="navbar-search-item-right text-muted-2">{project.name}</div>
+ : null;
};
render() {
@@ -87,14 +81,12 @@ export default class GlobalNavSearchFormComponent extends React.PureComponent {
ref={node => this.props.innerRef(component.key, node)}>
<Tooltip mouseEnterDelay={1.0} overlay={component.key} placement="left">
<Link
+ className="navbar-search-item-link"
data-key={component.key}
onClick={this.props.onClose}
onMouseEnter={this.handleMouseEnter}
to={getProjectUrl(component.key)}>
- {this.renderOrganization(component)}
- {this.renderProject(component)}
-
<span className="navbar-search-item-icons little-spacer-right">
{component.isFavorite && <FavoriteIcon favorite={true} size={12} />}
{!component.isFavorite && component.isRecentlyBrowsed && <ClockIcon size={12} />}
@@ -102,8 +94,14 @@ export default class GlobalNavSearchFormComponent extends React.PureComponent {
</span>
{component.match
- ? <span dangerouslySetInnerHTML={{ __html: component.match }} />
- : component.name}
+ ? <span
+ className="navbar-search-item-match"
+ dangerouslySetInnerHTML={{ __html: component.match }}
+ />
+ : <span className="navbar-search-item-match">{component.name}</span>}
+
+ {this.renderOrganization(component)}
+ {this.renderProject(component)}
</Link>
</Tooltip>
diff --git a/server/sonar-web/src/main/js/app/components/search/SearchResults.js b/server/sonar-web/src/main/js/app/components/search/SearchResults.js
new file mode 100644
index 00000000000..96be176aea6
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/search/SearchResults.js
@@ -0,0 +1,83 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import SearchShowMore from './SearchShowMore';
+import { sortQualifiers } from './utils';
+import type { Component, More, Results } from './utils';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+ allowMore: boolean,
+ loadingMore: ?string,
+ more: More,
+ onMoreClick: string => void,
+ onSelect: string => void,
+ renderNoResults: () => React.Element<*>,
+ renderResult: Component => React.Element<*>,
+ results: Results,
+ selected: ?string
+|};
+
+export default class SearchResults extends React.PureComponent {
+ props: Props;
+
+ render() {
+ const qualifiers = Object.keys(this.props.results);
+ const renderedComponents = [];
+
+ sortQualifiers(qualifiers).forEach(qualifier => {
+ const components = this.props.results[qualifier];
+
+ if (components.length > 0 && renderedComponents.length > 0) {
+ renderedComponents.push(<li key={`divider-${qualifier}`} className="divider" />);
+ }
+
+ if (components.length > 0) {
+ renderedComponents.push(
+ <li key={`header-${qualifier}`} className="dropdown-header">
+ {translate('qualifiers', qualifier)}
+ </li>
+ );
+ }
+
+ components.forEach(component => renderedComponents.push(this.props.renderResult(component)));
+
+ const more = this.props.more[qualifier];
+ if (more != null && more > 0) {
+ renderedComponents.push(
+ <SearchShowMore
+ allowMore={this.props.allowMore}
+ key={`more-${qualifier}`}
+ loadingMore={this.props.loadingMore}
+ onMoreClick={this.props.onMoreClick}
+ onSelect={this.props.onSelect}
+ qualifier={qualifier}
+ selected={this.props.selected === `qualifier###${qualifier}`}
+ />
+ );
+ }
+ });
+
+ return renderedComponents.length > 0
+ ? <ul className="menu">{renderedComponents}</ul>
+ : this.props.renderNoResults();
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/components/search/SearchShowMore.js b/server/sonar-web/src/main/js/app/components/search/SearchShowMore.js
new file mode 100644
index 00000000000..d3a0282849f
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/search/SearchShowMore.js
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+type Props = {|
+ allowMore: boolean,
+ loadingMore: ?string,
+ onMoreClick: string => void,
+ onSelect: string => void,
+ qualifier: string,
+ selected: boolean
+|};
+
+export default class SearchShowMore extends React.PureComponent {
+ props: Props;
+
+ handleMoreClick = (event: MouseEvent & { currentTarget: HTMLElement }) => {
+ event.preventDefault();
+ event.stopPropagation();
+ event.currentTarget.blur();
+ const { qualifier } = event.currentTarget.dataset;
+ this.props.onMoreClick(qualifier);
+ };
+
+ handleMoreMouseEnter = (event: { currentTarget: HTMLElement }) => {
+ const { qualifier } = event.currentTarget.dataset;
+ this.props.onSelect(`qualifier###${qualifier}`);
+ };
+
+ render() {
+ const { loadingMore, qualifier, selected } = this.props;
+
+ return (
+ <li key={`more-${qualifier}`} className={classNames('menu-footer', { active: selected })}>
+ <DeferredSpinner className="navbar-search-icon" loading={loadingMore === qualifier}>
+ <a
+ className={classNames({ 'cursor-not-allowed': !this.props.allowMore })}
+ data-qualifier={qualifier}
+ href="#"
+ onClick={this.handleMoreClick}
+ onMouseEnter={this.handleMoreMouseEnter}>
+ <div
+ className="pull-right text-muted-2 menu-footer-note"
+ dangerouslySetInnerHTML={{
+ __html: translateWithParameters(
+ 'search.show_more.hint',
+ '<span class="shortcut-button shortcut-button-small">Enter</span>'
+ )
+ }}
+ />
+ <span>{translate('show_more')}</span>
+ </a>
+ </DeferredSpinner>
+ </li>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavSearchForm-test.js b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js
index 9ab6beaa056..07a2e347e3a 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavSearchForm-test.js
+++ b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js
@@ -20,12 +20,12 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
import type { ShallowWrapper } from 'enzyme';
-import GlobalNavSearchForm from '../GlobalNavSearchForm';
-import { elementKeydown, clickOutside } from '../../../../../helpers/testUtils';
+import Search from '../Search';
+import { elementKeydown, clickOutside } from '../../../../helpers/testUtils';
function render(props?: Object) {
return shallow(
- <GlobalNavSearchForm
+ <Search
appState={{ organizationsEnabled: false }}
currentUser={{ isLoggedIn: false }}
{...props}
@@ -52,35 +52,10 @@ function select(form: ShallowWrapper, expected: string) {
expect(form.state().selected).toBe(expected);
}
-it('renders different components and dividers between them', () => {
- const form = render();
- form.setState({
- open: true,
- results: {
- TRK: [component('foo'), component('bar')],
- BRC: [component('qwe', 'BRC'), component('qux', 'BRC')],
- FIL: [component('zux', 'FIL')]
- }
- });
- expect(form.find('.menu')).toMatchSnapshot();
-});
-
-it('renders "Show More" link', () => {
- const form = render();
- form.setState({
- more: { TRK: 175, BRC: 0 },
- open: true,
- results: {
- TRK: [component('foo'), component('bar')],
- BRC: [component('qwe', 'BRC'), component('qux', 'BRC')]
- }
- });
- expect(form.find('.menu')).toMatchSnapshot();
-});
-
it('selects results', () => {
const form = render();
form.setState({
+ more: { TRK: 15, BRC: 0 },
open: true,
results: {
TRK: [component('foo'), component('bar')],
@@ -90,8 +65,10 @@ it('selects results', () => {
});
expect(form.state().selected).toBe('foo');
next(form, 'bar');
+ next(form, 'qualifier###TRK');
next(form, 'qwe');
next(form, 'qwe');
+ prev(form, 'qualifier###TRK');
prev(form, 'bar');
select(form, 'foo');
prev(form, 'foo');
@@ -128,10 +105,7 @@ it('closes on escape', () => {
it('closes on click outside', () => {
const form = mount(
- <GlobalNavSearchForm
- appState={{ organizationsEnabled: false }}
- currentUser={{ isLoggedIn: false }}
- />
+ <Search appState={{ organizationsEnabled: false }} currentUser={{ isLoggedIn: false }} />
);
form.instance().openSearch();
expect(form.state().open).toBe(true);
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavSearchFormComponent-test.js b/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.js
index 73bb358d7a6..ef4aa10dafb 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/GlobalNavSearchFormComponent-test.js
+++ b/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.js
@@ -20,12 +20,12 @@
// @flow
import React from 'react';
import { shallow } from 'enzyme';
-import GlobalNavSearchFormComponent from '../GlobalNavSearchFormComponent';
+import SearchResult from '../SearchResult';
function render(props?: Object) {
return shallow(
// $FlowFixMe
- <GlobalNavSearchFormComponent
+ <SearchResult
appState={{ organizationsEnabled: false }}
component={{ key: 'foo', name: 'foo', qualifier: 'TRK', organization: 'bar' }}
innerRef={jest.fn()}
diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResults-test.js b/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResults-test.js
new file mode 100644
index 00000000000..fbba231e38f
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResults-test.js
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import { shallow } from 'enzyme';
+import SearchResults from '../SearchResults';
+
+it('renders different components and dividers between them', () => {
+ expect(
+ shallow(
+ <SearchResults
+ allowMore={true}
+ loadingMore={null}
+ more={{}}
+ onMoreClick={jest.fn()}
+ onSelect={jest.fn()}
+ renderNoResults={() => <div />}
+ renderResult={component => <span key={component.key}>{component.name}</span>}
+ results={{
+ TRK: [component('foo'), component('bar')],
+ BRC: [component('qwe', 'BRC'), component('qux', 'BRC')],
+ FIL: [component('zux', 'FIL')]
+ }}
+ selected={null}
+ />
+ )
+ ).toMatchSnapshot();
+});
+
+it('renders "Show More" link', () => {
+ expect(
+ shallow(
+ <SearchResults
+ allowMore={true}
+ loadingMore={null}
+ more={{ TRK: 175, BRC: 0 }}
+ onMoreClick={jest.fn()}
+ onSelect={jest.fn()}
+ renderNoResults={() => <div />}
+ renderResult={component => <span key={component.key}>{component.name}</span>}
+ results={{
+ TRK: [component('foo'), component('bar')],
+ BRC: [component('qwe', 'BRC'), component('qux', 'BRC')]
+ }}
+ selected={null}
+ />
+ )
+ ).toMatchSnapshot();
+});
+
+function component(key: string, qualifier: string = 'TRK') {
+ return { key, name: key, qualifier };
+}
diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.js.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.js.snap
new file mode 100644
index 00000000000..86b9f83f770
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.js.snap
@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`shows warning about short input 1`] = `
+<span
+ className="navbar-search-input-hint"
+>
+ select2.tooShort.2
+</span>
+`;
+
+exports[`shows warning about short input 2`] = `
+<span
+ className="navbar-search-input-hint is-shifted"
+>
+ select2.tooShort.2
+</span>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavSearchFormComponent-test.js.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap
index 039ebbd70c1..ce33642ae2c 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNavSearchFormComponent-test.js.snap
+++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap
@@ -8,6 +8,7 @@ exports[`renders favorite 1`] = `
placement="left"
>
<Link
+ className="navbar-search-item-link"
data-key="foo"
onClick={[Function]}
onMouseEnter={[Function]}
@@ -34,7 +35,11 @@ exports[`renders favorite 1`] = `
qualifier="TRK"
/>
</span>
- foo
+ <span
+ className="navbar-search-item-match"
+ >
+ foo
+ </span>
</Link>
</Tooltip>
</li>
@@ -48,6 +53,7 @@ exports[`renders match 1`] = `
placement="left"
>
<Link
+ className="navbar-search-item-link"
data-key="foo"
onClick={[Function]}
onMouseEnter={[Function]}
@@ -71,6 +77,7 @@ exports[`renders match 1`] = `
/>
</span>
<span
+ className="navbar-search-item-match"
dangerouslySetInnerHTML={
Object {
"__html": "f<mark>o</mark>o",
@@ -90,6 +97,7 @@ exports[`renders organizations 1`] = `
placement="left"
>
<Link
+ className="navbar-search-item-link"
data-key="foo"
onClick={[Function]}
onMouseEnter={[Function]}
@@ -104,11 +112,6 @@ exports[`renders organizations 1`] = `
}
}
>
- <div
- className="pull-right text-muted-2"
- >
- bar
- </div>
<span
className="navbar-search-item-icons little-spacer-right"
>
@@ -120,7 +123,16 @@ exports[`renders organizations 1`] = `
qualifier="TRK"
/>
</span>
- foo
+ <span
+ className="navbar-search-item-match"
+ >
+ foo
+ </span>
+ <div
+ className="navbar-search-item-right text-muted-2"
+ >
+ bar
+ </div>
</Link>
</Tooltip>
</li>
@@ -134,6 +146,7 @@ exports[`renders organizations 2`] = `
placement="left"
>
<Link
+ className="navbar-search-item-link"
data-key="foo"
onClick={[Function]}
onMouseEnter={[Function]}
@@ -159,7 +172,11 @@ exports[`renders organizations 2`] = `
qualifier="TRK"
/>
</span>
- foo
+ <span
+ className="navbar-search-item-match"
+ >
+ foo
+ </span>
</Link>
</Tooltip>
</li>
@@ -173,6 +190,7 @@ exports[`renders projects 1`] = `
placement="left"
>
<Link
+ className="navbar-search-item-link"
data-key="qwe"
onClick={[Function]}
onMouseEnter={[Function]}
@@ -187,11 +205,6 @@ exports[`renders projects 1`] = `
}
}
>
- <div
- className="pull-right text-muted-2"
- >
- foo
- </div>
<span
className="navbar-search-item-icons little-spacer-right"
>
@@ -203,7 +216,16 @@ exports[`renders projects 1`] = `
qualifier="BRC"
/>
</span>
- qwe
+ <span
+ className="navbar-search-item-match"
+ >
+ qwe
+ </span>
+ <div
+ className="navbar-search-item-right text-muted-2"
+ >
+ foo
+ </div>
</Link>
</Tooltip>
</li>
@@ -217,6 +239,7 @@ exports[`renders recently browsed 1`] = `
placement="left"
>
<Link
+ className="navbar-search-item-link"
data-key="foo"
onClick={[Function]}
onMouseEnter={[Function]}
@@ -242,7 +265,11 @@ exports[`renders recently browsed 1`] = `
qualifier="TRK"
/>
</span>
- foo
+ <span
+ className="navbar-search-item-match"
+ >
+ foo
+ </span>
</Link>
</Tooltip>
</li>
@@ -256,6 +283,7 @@ exports[`renders selected 1`] = `
placement="left"
>
<Link
+ className="navbar-search-item-link"
data-key="foo"
onClick={[Function]}
onMouseEnter={[Function]}
@@ -278,7 +306,11 @@ exports[`renders selected 1`] = `
qualifier="TRK"
/>
</span>
- foo
+ <span
+ className="navbar-search-item-match"
+ >
+ foo
+ </span>
</Link>
</Tooltip>
</li>
@@ -294,6 +326,7 @@ exports[`renders selected 2`] = `
placement="left"
>
<Link
+ className="navbar-search-item-link"
data-key="foo"
onClick={[Function]}
onMouseEnter={[Function]}
@@ -316,7 +349,11 @@ exports[`renders selected 2`] = `
qualifier="TRK"
/>
</span>
- foo
+ <span
+ className="navbar-search-item-match"
+ >
+ foo
+ </span>
</Link>
</Tooltip>
</li>
diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap
new file mode 100644
index 00000000000..b93d9ef64b5
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap
@@ -0,0 +1,84 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders "Show More" link 1`] = `
+<ul
+ className="menu"
+>
+ <li
+ className="dropdown-header"
+ >
+ qualifiers.TRK
+ </li>
+ <span>
+ foo
+ </span>
+ <span>
+ bar
+ </span>
+ <SearchShowMore
+ allowMore={true}
+ loadingMore={null}
+ onMoreClick={[Function]}
+ onSelect={[Function]}
+ qualifier="TRK"
+ selected={false}
+ />
+ <li
+ className="divider"
+ />
+ <li
+ className="dropdown-header"
+ >
+ qualifiers.BRC
+ </li>
+ <span>
+ qwe
+ </span>
+ <span>
+ qux
+ </span>
+</ul>
+`;
+
+exports[`renders different components and dividers between them 1`] = `
+<ul
+ className="menu"
+>
+ <li
+ className="dropdown-header"
+ >
+ qualifiers.TRK
+ </li>
+ <span>
+ foo
+ </span>
+ <span>
+ bar
+ </span>
+ <li
+ className="divider"
+ />
+ <li
+ className="dropdown-header"
+ >
+ qualifiers.BRC
+ </li>
+ <span>
+ qwe
+ </span>
+ <span>
+ qux
+ </span>
+ <li
+ className="divider"
+ />
+ <li
+ className="dropdown-header"
+ >
+ qualifiers.FIL
+ </li>
+ <span>
+ zux
+ </span>
+</ul>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/search/utils.js b/server/sonar-web/src/main/js/app/components/search/utils.js
new file mode 100644
index 00000000000..5ed66863da2
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/search/utils.js
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import { sortBy } from 'lodash';
+
+const ORDER = ['DEV', 'VW', 'SVW', 'TRK', 'BRC', 'FIL', 'UTS'];
+
+export function sortQualifiers(qualifiers: Array<string>) {
+ return sortBy(qualifiers, qualifier => ORDER.indexOf(qualifier));
+}
+
+export type Component = {
+ isFavorite?: boolean,
+ isRecentlyBrowsed?: boolean,
+ key: string,
+ match?: string,
+ name: string,
+ organization?: string,
+ project?: string,
+ qualifier: string
+};
+
+export type Results = { [qualifier: string]: Array<Component> };
+
+export type More = { [string]: number };
diff --git a/server/sonar-web/src/main/js/components/controls/FavoriteBase.js b/server/sonar-web/src/main/js/components/controls/FavoriteBase.js
index b417b22e48a..13ec0372fe1 100644
--- a/server/sonar-web/src/main/js/components/controls/FavoriteBase.js
+++ b/server/sonar-web/src/main/js/components/controls/FavoriteBase.js
@@ -39,6 +39,12 @@ export default class FavoriteBase extends React.PureComponent {
this.toggleFavorite = this.toggleFavorite.bind(this);
}
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.favorite !== this.props.favorite && nextProps.favorite !== this.state.favorite) {
+ this.setState({ favorite: nextProps.favorite });
+ }
+ }
+
componentWillUnmount() {
this.mounted = false;
}
diff --git a/server/sonar-web/src/main/less/components/dropdowns.less b/server/sonar-web/src/main/less/components/dropdowns.less
index 394380271cd..382b487d163 100644
--- a/server/sonar-web/src/main/less/components/dropdowns.less
+++ b/server/sonar-web/src/main/less/components/dropdowns.less
@@ -77,6 +77,10 @@
white-space: nowrap; // as with > li > a
}
+.dropdown-item {
+ padding: 5px 16px;
+}
+
.dropdown-menu .small-divider {
height: 1px;
margin: 4px 20px;
diff --git a/server/sonar-web/src/main/less/components/menu.less b/server/sonar-web/src/main/less/components/menu.less
index b8b7a6d7161..226ce16e989 100644
--- a/server/sonar-web/src/main/less/components/menu.less
+++ b/server/sonar-web/src/main/less/components/menu.less
@@ -77,21 +77,18 @@
}
}
- .menu-footer {
- display: block;
- padding: 8px 16px 4px;
- white-space: nowrap;
-
- & > a {
- display: inline;
- padding: 0;
- border-bottom: 1px solid @darkGrey;
- color: @secondFontColor;
-
- &:hover {
- background: none;
- }
- }
+ .menu-footer > a > span {
+ border-bottom: 1px solid @darkGrey;
+ color: @secondFontColor;
+ }
+
+ .menu-footer-note {
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ .menu-footer.active .menu-footer-note {
+ opacity: 1;
}
}
diff --git a/server/sonar-web/src/main/less/components/navbar.less b/server/sonar-web/src/main/less/components/navbar.less
index 0190ea4163f..6e3878b0c82 100644
--- a/server/sonar-web/src/main/less/components/navbar.less
+++ b/server/sonar-web/src/main/less/components/navbar.less
@@ -138,7 +138,8 @@
}
.navbar-search-input {
- width: 280px;
+ vertical-align: middle;
+ width: 310px;
margin-top: 3px;
margin-bottom: 3px;
padding-left: 26px !important;
@@ -160,6 +161,7 @@
.navbar-search-icon {
position: relative;
+ vertical-align: middle;
width: 16px;
margin-right: -20px;
color: @secondFontColor;
@@ -169,10 +171,25 @@
}
}
+.navbar-search-item-link {
+ display: flex !important;
+}
+
+.navbar-search-item-match {
+ flex-grow: 5;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.navbar-search-item-right {
+ flex-grow: 1;
+ padding-left: 10px;
+ text-align: right;
+}
+
.navbar-search-item-icons {
position: relative;
- display: inline-block;
- vertical-align: middle;
+ flex-shrink: 0;
width: 16px;
height: 16px;
@@ -186,7 +203,7 @@
> .icon-star,
> .icon-clock {
z-index: 6;
- top: -5px;
+ top: -4px;
left: -5px;
}
}
@@ -198,14 +215,11 @@
background-color: #f3f3f3;
color: #777;
font-size: 11px;
+}
- .shortcut-button {
- min-width: 16px;
- height: 16px;
- line-height: 12px;
- margin-left: 4px;
- margin-right: 4px;
- }
+.navbar-search-no-results {
+ margin-top: 4px;
+ padding: 5px 10px;
}
.navbar-global {
diff --git a/server/sonar-web/src/main/less/components/ui.less b/server/sonar-web/src/main/less/components/ui.less
index 357e37ebb9f..cfdd43a1bde 100644
--- a/server/sonar-web/src/main/less/components/ui.less
+++ b/server/sonar-web/src/main/less/components/ui.less
@@ -107,6 +107,14 @@
text-align: center;
}
+.shortcut-button-small {
+ min-width: 16px;
+ height: 16px;
+ line-height: 14px;
+ margin-left: 4px;
+ margin-right: 4px;
+}
+
.nav {
margin: 0;