+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { get, remove, save } from '../../helpers/storage';
-
-const RECENT_HISTORY = 'sonar_recent_history';
-const HISTORY_LIMIT = 10;
-
-/*::
-type History = Array<{
- key: string,
- name: string,
- icon: string,
- organization?: string
-}>;
-*/
-
-export default class RecentHistory {
- static get() /*: History */ {
- const history = get(RECENT_HISTORY);
- if (history == null) {
- return [];
- } else {
- try {
- return JSON.parse(history);
- } catch (e) {
- remove(RECENT_HISTORY);
- return [];
- }
- }
- }
-
- static set(newHistory /*: History */) /*: void */ {
- save(RECENT_HISTORY, JSON.stringify(newHistory));
- }
-
- static clear() /*: void */ {
- remove(RECENT_HISTORY);
- }
-
- static add(
- componentKey /*: string */,
- componentName /*: string */,
- icon /*: string */,
- organization /*: string | void */
- ) /*: void */ {
- const sonarHistory = RecentHistory.get();
- const newEntry = { key: componentKey, name: componentName, icon, organization };
- let newHistory = sonarHistory.filter(entry => entry.key !== newEntry.key);
- newHistory.unshift(newEntry);
- newHistory = newHistory.slice(0, HISTORY_LIMIT);
- RecentHistory.set(newHistory);
- }
-
- static remove(componentKey /*: string */) /*: void */ {
- const history = RecentHistory.get();
- const newHistory = history.filter(entry => entry.key !== componentKey);
- RecentHistory.set(newHistory);
- }
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+import { get, remove, save } from '../../helpers/storage';
+
+const RECENT_HISTORY = 'sonar_recent_history';
+const HISTORY_LIMIT = 10;
+
+export type History = Array<{
+ key: string;
+ name: string;
+ icon: string;
+ organization?: string;
+}>;
+
+export default class RecentHistory {
+ static get(): History {
+ const history = get(RECENT_HISTORY);
+ if (history == null) {
+ return [];
+ } else {
+ try {
+ return JSON.parse(history);
+ } catch {
+ remove(RECENT_HISTORY);
+ return [];
+ }
+ }
+ }
+
+ static set(newHistory: History) {
+ save(RECENT_HISTORY, JSON.stringify(newHistory));
+ }
+
+ static clear() {
+ remove(RECENT_HISTORY);
+ }
+
+ static add(
+ componentKey: string,
+ componentName: string,
+ icon: string,
+ organization: string | undefined
+ ) {
+ const sonarHistory = RecentHistory.get();
+ const newEntry = { key: componentKey, name: componentName, icon, organization };
+ let newHistory = sonarHistory.filter(entry => entry.key !== newEntry.key);
+ newHistory.unshift(newEntry);
+ newHistory = newHistory.slice(0, HISTORY_LIMIT);
+ RecentHistory.set(newHistory);
+ }
+
+ static remove(componentKey: string) {
+ const history = RecentHistory.get();
+ const newHistory = history.filter(entry => entry.key !== componentKey);
+ RecentHistory.set(newHistory);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+import RecentHistory, { History } from '../RecentHistory';
+import { get, remove, save } from '../../../helpers/storage';
+
+jest.mock('../../../helpers/storage', () => ({
+ get: jest.fn(),
+ remove: jest.fn(),
+ save: jest.fn()
+}));
+
+beforeEach(() => {
+ (get as jest.Mock).mockClear();
+ (remove as jest.Mock).mockClear();
+ (save as jest.Mock).mockClear();
+});
+
+it('should get existing history', () => {
+ const history = [{ key: 'foo', name: 'Foo', icon: 'TRK' }];
+ (get as jest.Mock).mockReturnValueOnce(JSON.stringify(history));
+ expect(RecentHistory.get()).toEqual(history);
+ expect(get).toBeCalledWith('sonar_recent_history');
+});
+
+it('should get empty history', () => {
+ (get as jest.Mock).mockReturnValueOnce(null);
+ expect(RecentHistory.get()).toEqual([]);
+ expect(get).toBeCalledWith('sonar_recent_history');
+});
+
+it('should return [] and clear history in case of failure', () => {
+ (get as jest.Mock).mockReturnValueOnce('not a json');
+ expect(RecentHistory.get()).toEqual([]);
+ expect(get).toBeCalledWith('sonar_recent_history');
+ expect(remove).toBeCalledWith('sonar_recent_history');
+});
+
+it('should save history', () => {
+ const history = [{ key: 'foo', name: 'Foo', icon: 'TRK' }];
+ RecentHistory.set(history);
+ expect(save).toBeCalledWith('sonar_recent_history', JSON.stringify(history));
+});
+
+it('should clear history', () => {
+ RecentHistory.clear();
+ expect(remove).toBeCalledWith('sonar_recent_history');
+});
+
+it('should add item to history', () => {
+ const history = [{ key: 'foo', name: 'Foo', icon: 'TRK' }];
+ (get as jest.Mock).mockReturnValueOnce(JSON.stringify(history));
+ RecentHistory.add('bar', 'Bar', 'VW', 'org');
+ expect(save).toBeCalledWith(
+ 'sonar_recent_history',
+ JSON.stringify([{ key: 'bar', name: 'Bar', icon: 'VW', organization: 'org' }, ...history])
+ );
+});
+
+it('should keep 10 items maximum', () => {
+ const history: History = [];
+ for (let i = 0; i < 10; i++) {
+ history.push({ key: `key-${i}`, name: `name-${i}`, icon: 'TRK' });
+ }
+ (get as jest.Mock).mockReturnValueOnce(JSON.stringify(history));
+ RecentHistory.add('bar', 'Bar', 'VW', 'org');
+ expect(save).toBeCalledWith(
+ 'sonar_recent_history',
+ JSON.stringify([
+ { key: 'bar', name: 'Bar', icon: 'VW', organization: 'org' },
+ ...history.slice(0, 9)
+ ])
+ );
+});
+
+it('should remove component from history', () => {
+ const history: History = [];
+ for (let i = 0; i < 10; i++) {
+ history.push({ key: `key-${i}`, name: `name-${i}`, icon: 'TRK' });
+ }
+ (get as jest.Mock).mockReturnValueOnce(JSON.stringify(history));
+ RecentHistory.remove('key-5');
+ expect(save).toBeCalledWith(
+ 'sonar_recent_history',
+ JSON.stringify([...history.slice(0, 5), ...history.slice(6)])
+ );
+});
suggestions={Array []}
tooltip={false}
/>
- <Search
+ <withRouter(Search)
appState={
Object {
"canAdmin": false,
suggestions={Array []}
tooltip={true}
/>
- <Search
+ <withRouter(Search)
appState={
Object {
"canAdmin": false,
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-import * as React from 'react';
-import { CurrentUser, AppState } from '../../types';
-
-export interface Props {
- appState: Pick<AppState, 'organizationsEnabled'>;
- currentUser: CurrentUser;
-}
-
-export default class Search extends React.PureComponent<Props> {}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 PropTypes from 'prop-types';
-import key from 'keymaster';
-import { debounce, keyBy, uniqBy } from 'lodash';
-import { FormattedMessage } from 'react-intl';
-import { sortQualifiers } from './utils';
-/*:: import type { Component, More, Results } from './utils'; */
-import RecentHistory from '../RecentHistory';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
-import { DropdownOverlay } from '../../../components/controls/Dropdown';
-import ClockIcon from '../../../components/icons-components/ClockIcon';
-import OutsideClickHandler from '../../../components/controls/OutsideClickHandler';
-import SearchBox from '../../../components/controls/SearchBox';
-import { lazyLoad } from '../../../components/lazyLoad';
-import { getSuggestions } from '../../../api/components';
-import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { scrollToElement } from '../../../helpers/scrolling';
-import { getProjectUrl } from '../../../helpers/urls';
-import './Search.css';
-
-const SearchResults = lazyLoad(() => import('./SearchResults'));
-const SearchResult = lazyLoad(() => import('./SearchResult'));
-
-/*::
-type Props = {|
- appState: { organizationsEnabled: boolean },
- currentUser: { isLoggedIn: boolean }
-|};
-*/
-
-/*::
-type State = {
- loading: boolean,
- loadingMore: ?string,
- more: More,
- open: boolean,
- organizations: { [string]: { name: string } },
- projects: { [string]: { name: string } },
- query: string,
- results: Results,
- selected: ?string,
- shortQuery: boolean
-};
-*/
-
-export default class Search extends React.PureComponent {
- /*:: input: HTMLInputElement | null; */
- /*:: mounted: boolean; */
- /*:: node: HTMLElement; */
- /*:: nodes: { [string]: HTMLElement };
-*/
- /*:: props: Props; */
- /*:: state: State; */
-
- static contextTypes = {
- router: PropTypes.object
- };
-
- constructor(props /*: Props */) {
- super(props);
- this.nodes = {};
- this.search = debounce(this.search, 250);
- this.state = {
- loading: false,
- loadingMore: null,
- more: {},
- open: false,
- organizations: {},
- projects: {},
- query: '',
- results: {},
- selected: null,
- shortQuery: false
- };
- }
-
- componentDidMount() {
- this.mounted = true;
- key('s', () => {
- if (this.input) {
- this.input.focus();
- }
- this.openSearch();
- return false;
- });
- }
-
- componentWillUpdate() {
- this.nodes = {};
- }
-
- componentDidUpdate(prevProps /*: Props */, prevState /*: State */) {
- if (prevState.selected !== this.state.selected) {
- this.scrollToSelected();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- key.unbind('s');
- }
-
- handleClickOutside = () => {
- this.closeSearch(false);
- };
-
- handleFocus = () => {
- if (!this.state.open) {
- // simulate click to close any other dropdowns
- const body = document.documentElement;
- if (body) {
- body.click();
- }
- }
- this.openSearch();
- };
-
- openSearch = () => {
- if (!this.state.open && !this.state.query) {
- this.search('');
- }
- this.setState({ open: true });
- };
-
- closeSearch = (clear /*: boolean */ = true) => {
- if (this.input) {
- this.input.blur();
- }
- this.setState(
- clear
- ? {
- more: {},
- open: false,
- organizations: {},
- projects: {},
- query: '',
- results: {},
- selected: null,
- shortQuery: false
- }
- : {
- open: false
- }
- );
- };
-
- getPlainComponentsList = (results /*: Results */, more /*: More */) =>
- 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 => ({
- ...component,
- isRecentlyBrowsed: true,
- qualifier: component.icon.toUpperCase()
- }));
- return uniqBy([...components, ...recentlyBrowsed], 'key');
- };
-
- stopLoading = () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- };
-
- search = (query /*: string */) => {
- 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: query.length > 2 && response.warning === 'short_input'
- }));
- }
- }, this.stopLoading);
- } else {
- this.setState({ loading: false });
- }
- };
-
- searchMore = (qualifier /*: string */) => {
- 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
- }));
- if (this.input) {
- this.input.focus();
- }
- }
- }, this.stopLoading);
- }
- };
-
- handleQueryChange = (query /*: string */) => {
- this.setState({ query, shortQuery: query.length === 1 });
- this.search(query);
- };
-
- selectPrevious = () => {
- 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(({ 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 = () => {
- const { selected } = this.state;
- if (selected) {
- if (selected.startsWith('qualifier###')) {
- this.searchMore(selected.substr(12));
- } else {
- this.context.router.push(getProjectUrl(selected));
- this.closeSearch();
- }
- }
- };
-
- scrollToSelected = () => {
- if (this.state.selected) {
- const node = this.nodes[this.state.selected];
- if (node) {
- scrollToElement(node, { topOffset: 30, bottomOffset: 30, parent: this.node });
- }
- }
- };
-
- handleKeyDown = (event /*: KeyboardEvent */) => {
- switch (event.keyCode) {
- case 13:
- event.preventDefault();
- this.openSelected();
- return;
- case 38:
- event.preventDefault();
- this.selectPrevious();
- return;
- case 40:
- event.preventDefault();
- this.selectNext();
- // keep this return to prevent fall-through in case more cases will be adder later
- // eslint-disable-next-line no-useless-return
- return;
- }
- };
-
- handleSelect = (selected /*: string */) => {
- this.setState({ selected });
- };
-
- innerRef = (component /*: string */, node /*: HTMLElement */) => {
- this.nodes[component] = node;
- };
-
- searchInputRef = (node /*: HTMLInputElement | null */) => {
- this.input = node;
- };
-
- renderResult = (component /*: Component */) => (
- <SearchResult
- appState={this.props.appState}
- component={component}
- innerRef={this.innerRef}
- key={component.key}
- onClose={this.closeSearch}
- onSelect={this.handleSelect}
- organizations={this.state.organizations}
- projects={this.state.projects}
- selected={this.state.selected === component.key}
- />
- );
-
- renderNoResults = () => (
- <div className="navbar-search-no-results">
- {translateWithParameters('no_results_for_x', this.state.query)}
- </div>
- );
-
- render() {
- const search = (
- <li className="navbar-search dropdown">
- <DeferredSpinner className="navbar-search-icon" loading={this.state.loading} />
-
- <SearchBox
- autoFocus={this.state.open}
- innerRef={this.searchInputRef}
- minLength={2}
- onChange={this.handleQueryChange}
- onFocus={this.handleFocus}
- onKeyDown={this.handleKeyDown}
- placeholder={translate('search.placeholder')}
- value={this.state.query}
- />
-
- {this.state.shortQuery && (
- <span className="navbar-search-input-hint">
- {translateWithParameters('select2.tooShort', 2)}
- </span>
- )}
-
- {this.state.open &&
- Object.keys(this.state.results).length > 0 && (
- <DropdownOverlay noPadding={true}>
- <div className="global-navbar-search-dropdown" ref={node => (this.node = node)}>
- <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="dropdown-bottom-hint">
- <div className="pull-right">
- <ClockIcon className="little-spacer-right" size={12} />
- {translate('recently_browsed')}
- </div>
- <FormattedMessage
- defaultMessage={translate('search.shortcut_hint')}
- id="search.shortcut_hint"
- values={{
- shortcut: <span className="shortcut-button shortcut-button-small">s</span>
- }}
- />
- </div>
- </div>
- </DropdownOverlay>
- )}
- </li>
- );
-
- return this.state.open ? (
- <OutsideClickHandler onClickOutside={this.handleClickOutside}>{search}</OutsideClickHandler>
- ) : (
- search
- );
- }
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+import * as React from 'react';
+import * as key from 'keymaster';
+import { debounce, keyBy, uniqBy } from 'lodash';
+import { FormattedMessage } from 'react-intl';
+import { withRouter, WithRouterProps } from 'react-router';
+import { sortQualifiers, More, Results, ComponentResult } from './utils';
+import RecentHistory from '../RecentHistory';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { DropdownOverlay } from '../../../components/controls/Dropdown';
+import ClockIcon from '../../../components/icons-components/ClockIcon';
+import OutsideClickHandler from '../../../components/controls/OutsideClickHandler';
+import SearchBox from '../../../components/controls/SearchBox';
+import { lazyLoad } from '../../../components/lazyLoad';
+import { getSuggestions } from '../../../api/components';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { scrollToElement } from '../../../helpers/scrolling';
+import { getProjectUrl } from '../../../helpers/urls';
+import { AppState, CurrentUser } from '../../types';
+import './Search.css';
+
+const SearchResults = lazyLoad(() => import('./SearchResults'));
+const SearchResult = lazyLoad(() => import('./SearchResult'));
+
+interface OwnProps {
+ appState: Pick<AppState, 'organizationsEnabled'>;
+ currentUser: CurrentUser;
+}
+
+type Props = OwnProps & WithRouterProps;
+
+interface State {
+ loading: boolean;
+ loadingMore?: string;
+ more: More;
+ open: boolean;
+ organizations: { [key: string]: { name: string } };
+ projects: { [key: string]: { name: string } };
+ query: string;
+ results: Results;
+ selected?: string;
+ shortQuery: boolean;
+}
+
+export class Search extends React.PureComponent<Props, State> {
+ input?: HTMLInputElement | null;
+ node?: HTMLElement | null;
+ nodes: { [x: string]: HTMLElement };
+ mounted = false;
+
+ constructor(props: Props) {
+ super(props);
+ this.nodes = {};
+ this.search = debounce(this.search, 250);
+ this.state = {
+ loading: false,
+ more: {},
+ open: false,
+ organizations: {},
+ projects: {},
+ query: '',
+ results: {},
+ shortQuery: false
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ key('s', () => {
+ this.focusInput();
+ this.openSearch();
+ return false;
+ });
+ }
+
+ componentWillUpdate() {
+ this.nodes = {};
+ }
+
+ componentDidUpdate(_prevProps: Props, prevState: State) {
+ if (prevState.selected !== this.state.selected) {
+ this.scrollToSelected();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ key.unbind('s');
+ }
+
+ focusInput = () => {
+ if (this.input) {
+ this.input.focus();
+ }
+ };
+
+ handleClickOutside = () => {
+ this.closeSearch(false);
+ };
+
+ handleFocus = () => {
+ if (!this.state.open) {
+ // simulate click to close any other dropdowns
+ const body = document.documentElement;
+ if (body) {
+ body.click();
+ }
+ }
+ this.openSearch();
+ };
+
+ openSearch = () => {
+ if (!this.state.open && !this.state.query) {
+ this.search('');
+ }
+ this.setState({ open: true });
+ };
+
+ closeSearch = (clear = true) => {
+ if (this.input) {
+ this.input.blur();
+ }
+ if (clear) {
+ this.setState({
+ more: {},
+ open: false,
+ organizations: {},
+ projects: {},
+ query: '',
+ results: {},
+ selected: undefined,
+ shortQuery: false
+ });
+ } else {
+ this.setState({ open: false });
+ }
+ };
+
+ getPlainComponentsList = (results: Results, more: More) =>
+ 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: ComponentResult[]) => {
+ const recentlyBrowsed = RecentHistory.get().map(component => ({
+ ...component,
+ isRecentlyBrowsed: true,
+ qualifier: component.icon.toUpperCase()
+ }));
+ return uniqBy([...components, ...recentlyBrowsed], 'key');
+ };
+
+ stopLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
+ search = (query: string) => {
+ 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: Results = {};
+ const more: 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] : undefined,
+ shortQuery: query.length > 2 && response.warning === 'short_input'
+ }));
+ }
+ }, this.stopLoading);
+ } else {
+ this.setState({ loading: false });
+ }
+ };
+
+ searchMore = (qualifier: string) => {
+ const { query } = this.state;
+ if (query.length === 1) {
+ return;
+ }
+
+ this.setState({ loading: true, loadingMore: qualifier });
+ const recentlyBrowsed = RecentHistory.get().map(component => component.key);
+ getSuggestions(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: undefined,
+ 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
+ }));
+ this.focusInput();
+ }
+ }, this.stopLoading);
+ };
+
+ handleQueryChange = (query: string) => {
+ this.setState({ query, shortQuery: query.length === 1 });
+ this.search(query);
+ };
+
+ selectPrevious = () => {
+ this.setState(({ more, results, selected }) => {
+ if (selected) {
+ const list = this.getPlainComponentsList(results, more);
+ const index = list.indexOf(selected);
+ return index > 0 ? { selected: list[index - 1] } : null;
+ } else {
+ return null;
+ }
+ });
+ };
+
+ selectNext = () => {
+ this.setState(({ more, results, selected }) => {
+ if (selected) {
+ const list = this.getPlainComponentsList(results, more);
+ const index = list.indexOf(selected);
+ return index >= 0 && index < list.length - 1 ? { selected: list[index + 1] } : null;
+ } else {
+ return null;
+ }
+ });
+ };
+
+ openSelected = () => {
+ const { selected } = this.state;
+ if (selected) {
+ if (selected.startsWith('qualifier###')) {
+ this.searchMore(selected.substr(12));
+ } else {
+ this.props.router.push(getProjectUrl(selected));
+ this.closeSearch();
+ }
+ }
+ };
+
+ scrollToSelected = () => {
+ if (this.state.selected) {
+ const node = this.nodes[this.state.selected];
+ if (node && this.node) {
+ scrollToElement(node, { topOffset: 30, bottomOffset: 30, parent: this.node });
+ }
+ }
+ };
+
+ handleKeyDown = (event: React.KeyboardEvent) => {
+ switch (event.keyCode) {
+ case 13:
+ event.preventDefault();
+ this.openSelected();
+ return;
+ case 38:
+ event.preventDefault();
+ this.selectPrevious();
+ return;
+ case 40:
+ event.preventDefault();
+ this.selectNext();
+ // keep this return to prevent fall-through in case more cases will be adder later
+ // eslint-disable-next-line no-useless-return
+ return;
+ }
+ };
+
+ handleSelect = (selected: string) => {
+ this.setState({ selected });
+ };
+
+ innerRef = (component: string, node: HTMLElement | null) => {
+ if (node) {
+ this.nodes[component] = node;
+ }
+ };
+
+ searchInputRef = (node: HTMLInputElement | null) => {
+ this.input = node;
+ };
+
+ renderResult = (component: ComponentResult) => (
+ <SearchResult
+ appState={this.props.appState}
+ component={component}
+ innerRef={this.innerRef}
+ key={component.key}
+ onClose={this.closeSearch}
+ onSelect={this.handleSelect}
+ organizations={this.state.organizations}
+ projects={this.state.projects}
+ selected={this.state.selected === component.key}
+ />
+ );
+
+ renderNoResults = () => (
+ <div className="navbar-search-no-results">
+ {translateWithParameters('no_results_for_x', this.state.query)}
+ </div>
+ );
+
+ render() {
+ const search = (
+ <li className="navbar-search dropdown">
+ <DeferredSpinner className="navbar-search-icon" loading={this.state.loading} />
+
+ <SearchBox
+ autoFocus={this.state.open}
+ innerRef={this.searchInputRef}
+ minLength={2}
+ onChange={this.handleQueryChange}
+ onFocus={this.handleFocus}
+ onKeyDown={this.handleKeyDown}
+ placeholder={translate('search.placeholder')}
+ value={this.state.query}
+ />
+
+ {this.state.shortQuery && (
+ <span className="navbar-search-input-hint">
+ {translateWithParameters('select2.tooShort', 2)}
+ </span>
+ )}
+
+ {this.state.open &&
+ Object.keys(this.state.results).length > 0 && (
+ <DropdownOverlay noPadding={true}>
+ <div className="global-navbar-search-dropdown" ref={node => (this.node = node)}>
+ <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="dropdown-bottom-hint">
+ <div className="pull-right">
+ <ClockIcon className="little-spacer-right" size={12} />
+ {translate('recently_browsed')}
+ </div>
+ <FormattedMessage
+ defaultMessage={translate('search.shortcut_hint')}
+ id="search.shortcut_hint"
+ values={{
+ shortcut: <span className="shortcut-button shortcut-button-small">s</span>
+ }}
+ />
+ </div>
+ </div>
+ </DropdownOverlay>
+ )}
+ </li>
+ );
+
+ return this.state.open ? (
+ <OutsideClickHandler onClickOutside={this.handleClickOutside}>{search}</OutsideClickHandler>
+ ) : (
+ search
+ );
+ }
+}
+
+export default withRouter<OwnProps>(Search);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { Link } from 'react-router';
-/*:: import type { Component } from './utils'; */
-import FavoriteIcon from '../../../components/icons-components/FavoriteIcon';
-import QualifierIcon from '../../../components/icons-components/QualifierIcon';
-import ClockIcon from '../../../components/icons-components/ClockIcon';
-import Tooltip from '../../../components/controls/Tooltip';
-import { getProjectUrl } from '../../../helpers/urls';
-
-/*::
-type Props = {|
- appState: { organizationsEnabled: boolean },
- component: Component,
- innerRef: (string, HTMLElement) => void,
- onClose: () => void,
- onSelect: string => void,
- organizations: { [string]: { name: string } },
- projects: { [string]: { name: string } },
- 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);
- };
-
- renderOrganization = (component /*: Component */) => {
- if (!this.props.appState.organizationsEnabled) {
- return null;
- }
-
- if (
- !['VW', 'SVW', 'APP', 'TRK'].includes(component.qualifier) ||
- component.organization == null
- ) {
- return null;
- }
-
- const organization = this.props.organizations[component.organization];
- return organization ? (
- <div className="navbar-search-item-right text-muted-2">{organization.name}</div>
- ) : null;
- };
-
- renderProject = (component /*: Component */) => {
- if (!['BRC', 'FIL', 'UTS'].includes(component.qualifier) || component.project == null) {
- return null;
- }
-
- const project = this.props.projects[component.project];
- return project ? (
- <div className="navbar-search-item-right text-muted-2">{project.name}</div>
- ) : null;
- };
-
- render() {
- const { component } = this.props;
-
- return (
- <li
- className={this.props.selected ? 'active' : undefined}
- key={component.key}
- ref={node => this.props.innerRef(component.key, node)}>
- <Tooltip
- mouseEnterDelay={TOOLTIP_DELAY / 1000}
- overlay={component.key}
- placement="left"
- visible={this.state.tooltipVisible}>
- <Link
- className="navbar-search-item-link"
- data-key={component.key}
- onClick={this.props.onClose}
- onMouseEnter={this.handleMouseEnter}
- to={getProjectUrl(component.key)}>
- <span className="navbar-search-item-icons little-spacer-right">
- {component.isFavorite && <FavoriteIcon favorite={true} size={12} />}
- {!component.isFavorite && component.isRecentlyBrowsed && <ClockIcon size={12} />}
- <QualifierIcon className="little-spacer-right" qualifier={component.qualifier} />
- </span>
-
- {component.match ? (
- <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>
- </li>
- );
- }
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+import * as React from 'react';
+import { Link } from 'react-router';
+import { ComponentResult } from './utils';
+import FavoriteIcon from '../../../components/icons-components/FavoriteIcon';
+import QualifierIcon from '../../../components/icons-components/QualifierIcon';
+import ClockIcon from '../../../components/icons-components/ClockIcon';
+import Tooltip from '../../../components/controls/Tooltip';
+import { getProjectUrl } from '../../../helpers/urls';
+import { AppState } from '../../types';
+
+interface Props {
+ appState: Pick<AppState, 'organizationsEnabled'>;
+ component: ComponentResult;
+ innerRef: (componentKey: string, node: HTMLElement | null) => void;
+ onClose: () => void;
+ onSelect: (componentKey: string) => void;
+ organizations: { [key: string]: { name: string } };
+ projects: { [key: string]: { name: string } };
+ selected: boolean;
+}
+
+interface State {
+ tooltipVisible: boolean;
+}
+
+const TOOLTIP_DELAY = 1000;
+
+export default class SearchResult extends React.PureComponent<Props, State> {
+ interval?: number;
+ 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 = window.setTimeout(() => {
+ this.setState({ tooltipVisible: true });
+ }, TOOLTIP_DELAY);
+ };
+
+ unscheduleTooltip = () => {
+ if (this.interval) {
+ window.clearInterval(this.interval);
+ }
+ };
+
+ handleMouseEnter = () => {
+ this.props.onSelect(this.props.component.key);
+ };
+
+ renderOrganization = (component: ComponentResult) => {
+ if (!this.props.appState.organizationsEnabled) {
+ return null;
+ }
+
+ if (!['VW', 'SVW', 'APP', 'TRK'].includes(component.qualifier) || !component.organization) {
+ return null;
+ }
+
+ const organization = this.props.organizations[component.organization];
+ return organization ? (
+ <div className="navbar-search-item-right text-muted-2">{organization.name}</div>
+ ) : null;
+ };
+
+ renderProject = (component: ComponentResult) => {
+ if (!['BRC', 'FIL', 'UTS'].includes(component.qualifier) || component.project == null) {
+ return null;
+ }
+
+ const project = this.props.projects[component.project];
+ return project ? (
+ <div className="navbar-search-item-right text-muted-2">{project.name}</div>
+ ) : null;
+ };
+
+ render() {
+ const { component } = this.props;
+
+ return (
+ <li
+ className={this.props.selected ? 'active' : undefined}
+ key={component.key}
+ ref={node => this.props.innerRef(component.key, node)}>
+ <Tooltip
+ mouseEnterDelay={TOOLTIP_DELAY / 1000}
+ overlay={component.key}
+ placement="left"
+ visible={this.state.tooltipVisible}>
+ <Link
+ className="navbar-search-item-link"
+ data-key={component.key}
+ onClick={this.props.onClose}
+ onMouseEnter={this.handleMouseEnter}
+ to={getProjectUrl(component.key)}>
+ <span className="navbar-search-item-icons little-spacer-right">
+ {component.isFavorite && <FavoriteIcon favorite={true} size={12} />}
+ {!component.isFavorite && component.isRecentlyBrowsed && <ClockIcon size={12} />}
+ <QualifierIcon className="little-spacer-right" qualifier={component.qualifier} />
+ </span>
+
+ {component.match ? (
+ <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>
+ </li>
+ );
+ }
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 className="divider" key={`divider-${qualifier}`} />);
- }
-
- if (components.length > 0) {
- renderedComponents.push(
- <li className="menu-header" key={`header-${qualifier}`}>
- {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()
- );
- }
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+import * as React from 'react';
+import SearchShowMore from './SearchShowMore';
+import { sortQualifiers, More, ComponentResult, Results } from './utils';
+import { translate } from '../../../helpers/l10n';
+
+export interface Props {
+ allowMore: boolean;
+ loadingMore?: string;
+ more: More;
+ onMoreClick: (qualifier: string) => void;
+ onSelect: (componentKey: string) => void;
+ renderNoResults: () => React.ReactElement<any>;
+ renderResult: (component: ComponentResult) => React.ReactNode;
+ results: Results;
+ selected?: string;
+}
+
+export default function SearchResults(props: Props): React.ReactElement<Props> {
+ const qualifiers = Object.keys(props.results);
+ const renderedComponents: React.ReactNode[] = [];
+
+ sortQualifiers(qualifiers).forEach(qualifier => {
+ const components = props.results[qualifier];
+
+ if (components.length > 0 && renderedComponents.length > 0) {
+ renderedComponents.push(<li className="divider" key={`divider-${qualifier}`} />);
+ }
+
+ if (components.length > 0) {
+ renderedComponents.push(
+ <li className="menu-header" key={`header-${qualifier}`}>
+ {translate('qualifiers', qualifier)}
+ </li>
+ );
+ }
+
+ components.forEach(component => renderedComponents.push(props.renderResult(component)));
+
+ const more = props.more[qualifier];
+ if (more !== undefined && more > 0) {
+ renderedComponents.push(
+ <SearchShowMore
+ allowMore={props.allowMore}
+ key={`more-${qualifier}`}
+ loadingMore={props.loadingMore}
+ onMoreClick={props.onMoreClick}
+ onSelect={props.onSelect}
+ qualifier={qualifier}
+ selected={props.selected === `qualifier###${qualifier}`}
+ />
+ );
+ }
+ });
+
+ return renderedComponents.length > 0 ? (
+ <ul className="menu">{renderedComponents}</ul>
+ ) : (
+ props.renderNoResults()
+ );
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 className={classNames('menu-footer', { active: selected })} key={`more-${qualifier}`}>
- <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>
- );
- }
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+interface Props {
+ allowMore: boolean;
+ loadingMore?: string;
+ onMoreClick: (qualifier: string) => void;
+ onSelect: (qualifier: string) => void;
+ qualifier: string;
+ selected: boolean;
+}
+
+export default class SearchShowMore extends React.PureComponent<Props> {
+ handleMoreClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.stopPropagation();
+ event.currentTarget.blur();
+ const { qualifier } = event.currentTarget.dataset;
+ if (qualifier) {
+ this.props.onMoreClick(qualifier);
+ }
+ };
+
+ handleMoreMouseEnter = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ const { qualifier } = event.currentTarget.dataset;
+ if (qualifier) {
+ this.props.onSelect(`qualifier###${qualifier}`);
+ }
+ };
+
+ render() {
+ const { loadingMore, qualifier, selected } = this.props;
+
+ return (
+ <li className={classNames('menu-footer', { active: selected })} key={`more-${qualifier}`}>
+ <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>
+ );
+ }
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-import React from 'react';
-import { shallow, mount } from 'enzyme';
-/*:: import type { ShallowWrapper } from 'enzyme'; */
-import Search from '../Search';
-import { elementKeydown, clickOutside } from '../../../../helpers/testUtils';
-
-function render(props /*: ?Object */) {
- return shallow(
- <Search
- appState={{ organizationsEnabled: false }}
- currentUser={{ isLoggedIn: false }}
- {...props}
- />
- );
-}
-
-function component(key /*: string */, qualifier /*: string */ = 'TRK') {
- return { key, name: key, qualifier };
-}
-
-function next(form /*: ShallowWrapper */, expected /*: string */) {
- elementKeydown(form.find('SearchBox'), 40);
- expect(form.state().selected).toBe(expected);
-}
-
-function prev(form /*: ShallowWrapper */, expected /*: string */) {
- elementKeydown(form.find('SearchBox'), 38);
- expect(form.state().selected).toBe(expected);
-}
-
-function select(form /*: ShallowWrapper */, expected /*: string */) {
- form.instance().handleSelect(expected);
- expect(form.state().selected).toBe(expected);
-}
-
-it('selects results', () => {
- const form = render();
- form.setState({
- more: { TRK: 15, BRC: 0 },
- open: true,
- results: {
- TRK: [component('foo'), component('bar')],
- BRC: [component('qwe', 'BRC')]
- },
- selected: 'foo'
- });
- 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');
-});
-
-it('opens selected on enter', () => {
- const form = render();
- form.setState({
- open: true,
- results: { TRK: [component('foo')] },
- selected: 'foo'
- });
- const openSelected = jest.fn();
- form.instance().openSelected = openSelected;
- elementKeydown(form.find('SearchBox'), 13);
- expect(openSelected).toBeCalled();
-});
-
-it('shows warning about short input', () => {
- const form = render();
- form.setState({ shortQuery: true });
- expect(form.find('.navbar-search-input-hint')).toMatchSnapshot();
- form.setState({ query: 'foobar x' });
- expect(form.find('.navbar-search-input-hint')).toMatchSnapshot();
-});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+import * as React from 'react';
+import { shallow, ShallowWrapper } from 'enzyme';
+import { Search } from '../Search';
+import { elementKeydown } from '../../../../helpers/testUtils';
+
+it('selects results', () => {
+ const form = shallowRender();
+ form.setState({
+ more: { TRK: 15, BRC: 0 },
+ open: true,
+ results: {
+ TRK: [component('foo'), component('bar')],
+ BRC: [component('qwe', 'BRC')]
+ },
+ selected: 'foo'
+ });
+ 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');
+});
+
+it('opens selected on enter', () => {
+ const form = shallowRender();
+ form.setState({
+ open: true,
+ results: { TRK: [component('foo')] },
+ selected: 'foo'
+ });
+ const openSelected = jest.fn();
+ (form.instance() as Search).openSelected = openSelected;
+ elementKeydown(form.find('SearchBox'), 13);
+ expect(openSelected).toBeCalled();
+});
+
+it('shows warning about short input', () => {
+ const form = shallowRender();
+ form.setState({ shortQuery: true });
+ expect(form.find('.navbar-search-input-hint')).toMatchSnapshot();
+ form.setState({ query: 'foobar x' });
+ expect(form.find('.navbar-search-input-hint')).toMatchSnapshot();
+});
+
+function shallowRender(props: Partial<Search['props']> = {}) {
+ return shallow(
+ // @ts-ignore
+ <Search
+ appState={{ organizationsEnabled: false }}
+ currentUser={{ isLoggedIn: false }}
+ {...props}
+ />
+ );
+}
+
+function component(key: string, qualifier = 'TRK') {
+ return { key, name: key, qualifier };
+}
+
+function next(form: ShallowWrapper<Search['props'], Search['state']>, expected: string) {
+ elementKeydown(form.find('SearchBox'), 40);
+ expect(form.state().selected).toBe(expected);
+}
+
+function prev(form: ShallowWrapper<Search['props'], Search['state']>, expected: string) {
+ elementKeydown(form.find('SearchBox'), 38);
+ expect(form.state().selected).toBe(expected);
+}
+
+function select(form: ShallowWrapper<Search['props'], Search['state']>, expected: string) {
+ (form.instance() as Search).handleSelect(expected);
+ expect(form.state().selected).toBe(expected);
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 SearchResult from '../SearchResult';
-
-function render(props /*: ?Object */) {
- return shallow(
- // $FlowFixMe
- <SearchResult
- appState={{ organizationsEnabled: false }}
- component={{ key: 'foo', name: 'foo', qualifier: 'TRK', organization: 'bar' }}
- innerRef={jest.fn()}
- onClose={jest.fn()}
- onSelect={jest.fn()}
- organizations={{ bar: { name: 'bar' } }}
- projects={{ foo: { name: 'foo' } }}
- selected={false}
- {...props}
- />
- );
-}
-
-jest.useFakeTimers();
-
-it('renders selected', () => {
- const wrapper = render();
- expect(wrapper).toMatchSnapshot();
- wrapper.setProps({ selected: true });
- expect(wrapper).toMatchSnapshot();
-});
-
-it('renders match', () => {
- const component = {
- key: 'foo',
- name: 'foo',
- match: 'f<mark>o</mark>o',
- qualifier: 'TRK',
- organization: 'bar'
- };
- const wrapper = render({ component });
- expect(wrapper).toMatchSnapshot();
-});
-
-it('renders favorite', () => {
- const component = {
- isFavorite: true,
- key: 'foo',
- name: 'foo',
- qualifier: 'TRK',
- organization: 'bar'
- };
- const wrapper = render({ component });
- expect(wrapper).toMatchSnapshot();
-});
-
-it('renders recently browsed', () => {
- const component = {
- isRecentlyBrowsed: true,
- key: 'foo',
- name: 'foo',
- qualifier: 'TRK',
- organization: 'bar'
- };
- const wrapper = render({ component });
- expect(wrapper).toMatchSnapshot();
-});
-
-it('renders projects', () => {
- const component = {
- isRecentlyBrowsed: true,
- key: 'qwe',
- name: 'qwe',
- qualifier: 'BRC',
- project: 'foo'
- };
- const wrapper = render({ component });
- expect(wrapper).toMatchSnapshot();
-});
-
-it('renders organizations', () => {
- const component = {
- isRecentlyBrowsed: true,
- key: 'foo',
- name: 'foo',
- qualifier: 'TRK',
- organization: 'bar'
- };
- const wrapper = render({ appState: { organizationsEnabled: true }, component });
- expect(wrapper).toMatchSnapshot();
- 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();
- wrapper.update();
- expect(wrapper.find('Tooltip').prop('visible')).toBe(true);
-
- wrapper.setProps({ selected: false });
- expect(wrapper.find('Tooltip').prop('visible')).toBe(false);
-});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import SearchResult from '../SearchResult';
+
+jest.useFakeTimers();
+
+it('renders selected', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setProps({ selected: true });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('renders match', () => {
+ const component = {
+ key: 'foo',
+ name: 'foo',
+ match: 'f<mark>o</mark>o',
+ qualifier: 'TRK',
+ organization: 'bar'
+ };
+ const wrapper = shallowRender({ component });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('renders favorite', () => {
+ const component = {
+ isFavorite: true,
+ key: 'foo',
+ name: 'foo',
+ qualifier: 'TRK',
+ organization: 'bar'
+ };
+ const wrapper = shallowRender({ component });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('renders recently browsed', () => {
+ const component = {
+ isRecentlyBrowsed: true,
+ key: 'foo',
+ name: 'foo',
+ qualifier: 'TRK',
+ organization: 'bar'
+ };
+ const wrapper = shallowRender({ component });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('renders projects', () => {
+ const component = {
+ isRecentlyBrowsed: true,
+ key: 'qwe',
+ name: 'qwe',
+ qualifier: 'BRC',
+ project: 'foo'
+ };
+ const wrapper = shallowRender({ component });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('renders organizations', () => {
+ const component = {
+ isRecentlyBrowsed: true,
+ key: 'foo',
+ name: 'foo',
+ qualifier: 'TRK',
+ organization: 'bar'
+ };
+ const wrapper = shallowRender({ appState: { organizationsEnabled: true }, component });
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setProps({ appState: { organizationsEnabled: false } });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('shows tooltip after delay', () => {
+ const wrapper = shallowRender();
+ expect(wrapper.find('Tooltip').prop('visible')).toBe(false);
+
+ wrapper.setProps({ selected: true });
+ expect(wrapper.find('Tooltip').prop('visible')).toBe(false);
+
+ jest.runAllTimers();
+ wrapper.update();
+ expect(wrapper.find('Tooltip').prop('visible')).toBe(true);
+
+ wrapper.setProps({ selected: false });
+ expect(wrapper.find('Tooltip').prop('visible')).toBe(false);
+});
+
+function shallowRender(props: Partial<SearchResult['props']> = {}) {
+ return shallow(
+ <SearchResult
+ appState={{ organizationsEnabled: false }}
+ component={{ key: 'foo', name: 'foo', qualifier: 'TRK', organization: 'bar' }}
+ innerRef={jest.fn()}
+ onClose={jest.fn()}
+ onSelect={jest.fn()}
+ organizations={{ bar: { name: 'bar' } }}
+ projects={{ foo: { name: 'foo' } }}
+ selected={false}
+ {...props}
+ />
+ );
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 };
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import SearchResults, { Props } from '../SearchResults';
+
+it('renders different components and dividers between them', () => {
+ expect(
+ shallow(
+ <SearchResults
+ allowMore={true}
+ 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')]
+ }}
+ />
+ )
+ ).toMatchSnapshot();
+});
+
+it('renders "Show More" link', () => {
+ expect(
+ shallow(
+ <SearchResults
+ allowMore={true}
+ 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')]
+ }}
+ />
+ )
+ ).toMatchSnapshot();
+});
+
+it('should render no results', () => {
+ // eslint-disable-next-line react/display-name
+ expect(shallowRender({ renderNoResults: () => <div id="no-results" /> })).toMatchSnapshot();
+});
+
+function component(key: string, qualifier = 'TRK') {
+ return { key, name: key, qualifier };
+}
+
+function shallowRender(props: Partial<Props> = {}) {
+ return shallow(
+ <SearchResults
+ allowMore={true}
+ more={{}}
+ onMoreClick={jest.fn()}
+ onSelect={jest.fn()}
+ renderNoResults={() => <div />}
+ renderResult={() => <div />}
+ results={{}}
+ {...props}
+ />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import SearchShowMore from '../SearchShowMore';
+import { click } from '../../../../helpers/testUtils';
+
+it('should render', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should trigger showing more', () => {
+ const onMoreClick = jest.fn();
+ const wrapper = shallowRender({ onMoreClick });
+ click(wrapper.find('a'), {
+ currentTarget: {
+ blur() {},
+ dataset: { qualifier: 'TRK' },
+ preventDefault() {},
+ stopPropagation() {}
+ }
+ });
+ expect(onMoreClick).toBeCalledWith('TRK');
+});
+
+it('should select on mouse over', () => {
+ const onSelect = jest.fn();
+ const wrapper = shallowRender({ onSelect });
+ wrapper.find('a').simulate('mouseenter', { currentTarget: { dataset: { qualifier: 'TRK' } } });
+ expect(onSelect).toBeCalledWith('qualifier###TRK');
+});
+
+function shallowRender(props: Partial<SearchShowMore['props']> = {}) {
+ return shallow(
+ <SearchShowMore
+ allowMore={true}
+ onMoreClick={jest.fn()}
+ onSelect={jest.fn()}
+ qualifier="TRK"
+ selected={false}
+ {...props}
+ />
+ );
+}
+++ /dev/null
-// 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"
->
- select2.tooShort.2
-</span>
-`;
--- /dev/null
+// 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"
+>
+ select2.tooShort.2
+</span>
+`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders favorite 1`] = `
-<li
- key="foo"
->
- <Tooltip
- mouseEnterDelay={1}
- overlay="foo"
- placement="left"
- visible={false}
- >
- <Link
- className="navbar-search-item-link"
- data-key="foo"
- onClick={[MockFunction]}
- onMouseEnter={[Function]}
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "branch": undefined,
- "id": "foo",
- },
- }
- }
- >
- <span
- className="navbar-search-item-icons little-spacer-right"
- >
- <FavoriteIcon
- favorite={true}
- size={12}
- />
- <QualifierIcon
- className="little-spacer-right"
- qualifier="TRK"
- />
- </span>
- <span
- className="navbar-search-item-match"
- >
- foo
- </span>
- </Link>
- </Tooltip>
-</li>
-`;
-
-exports[`renders match 1`] = `
-<li
- key="foo"
->
- <Tooltip
- mouseEnterDelay={1}
- overlay="foo"
- placement="left"
- visible={false}
- >
- <Link
- className="navbar-search-item-link"
- data-key="foo"
- onClick={[MockFunction]}
- onMouseEnter={[Function]}
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "branch": undefined,
- "id": "foo",
- },
- }
- }
- >
- <span
- className="navbar-search-item-icons little-spacer-right"
- >
- <QualifierIcon
- className="little-spacer-right"
- qualifier="TRK"
- />
- </span>
- <span
- className="navbar-search-item-match"
- dangerouslySetInnerHTML={
- Object {
- "__html": "f<mark>o</mark>o",
- }
- }
- />
- </Link>
- </Tooltip>
-</li>
-`;
-
-exports[`renders organizations 1`] = `
-<li
- key="foo"
->
- <Tooltip
- mouseEnterDelay={1}
- overlay="foo"
- placement="left"
- visible={false}
- >
- <Link
- className="navbar-search-item-link"
- data-key="foo"
- onClick={[MockFunction]}
- onMouseEnter={[Function]}
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "branch": undefined,
- "id": "foo",
- },
- }
- }
- >
- <span
- className="navbar-search-item-icons little-spacer-right"
- >
- <ClockIcon
- size={12}
- />
- <QualifierIcon
- className="little-spacer-right"
- qualifier="TRK"
- />
- </span>
- <span
- className="navbar-search-item-match"
- >
- foo
- </span>
- <div
- className="navbar-search-item-right text-muted-2"
- >
- bar
- </div>
- </Link>
- </Tooltip>
-</li>
-`;
-
-exports[`renders organizations 2`] = `
-<li
- key="foo"
->
- <Tooltip
- mouseEnterDelay={1}
- overlay="foo"
- placement="left"
- visible={false}
- >
- <Link
- className="navbar-search-item-link"
- data-key="foo"
- onClick={[MockFunction]}
- onMouseEnter={[Function]}
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "branch": undefined,
- "id": "foo",
- },
- }
- }
- >
- <span
- className="navbar-search-item-icons little-spacer-right"
- >
- <ClockIcon
- size={12}
- />
- <QualifierIcon
- className="little-spacer-right"
- qualifier="TRK"
- />
- </span>
- <span
- className="navbar-search-item-match"
- >
- foo
- </span>
- </Link>
- </Tooltip>
-</li>
-`;
-
-exports[`renders projects 1`] = `
-<li
- key="qwe"
->
- <Tooltip
- mouseEnterDelay={1}
- overlay="qwe"
- placement="left"
- visible={false}
- >
- <Link
- className="navbar-search-item-link"
- data-key="qwe"
- onClick={[MockFunction]}
- onMouseEnter={[Function]}
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "branch": undefined,
- "id": "qwe",
- },
- }
- }
- >
- <span
- className="navbar-search-item-icons little-spacer-right"
- >
- <ClockIcon
- size={12}
- />
- <QualifierIcon
- className="little-spacer-right"
- qualifier="BRC"
- />
- </span>
- <span
- className="navbar-search-item-match"
- >
- qwe
- </span>
- <div
- className="navbar-search-item-right text-muted-2"
- >
- foo
- </div>
- </Link>
- </Tooltip>
-</li>
-`;
-
-exports[`renders recently browsed 1`] = `
-<li
- key="foo"
->
- <Tooltip
- mouseEnterDelay={1}
- overlay="foo"
- placement="left"
- visible={false}
- >
- <Link
- className="navbar-search-item-link"
- data-key="foo"
- onClick={[MockFunction]}
- onMouseEnter={[Function]}
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "branch": undefined,
- "id": "foo",
- },
- }
- }
- >
- <span
- className="navbar-search-item-icons little-spacer-right"
- >
- <ClockIcon
- size={12}
- />
- <QualifierIcon
- className="little-spacer-right"
- qualifier="TRK"
- />
- </span>
- <span
- className="navbar-search-item-match"
- >
- foo
- </span>
- </Link>
- </Tooltip>
-</li>
-`;
-
-exports[`renders selected 1`] = `
-<li
- key="foo"
->
- <Tooltip
- mouseEnterDelay={1}
- overlay="foo"
- placement="left"
- visible={false}
- >
- <Link
- className="navbar-search-item-link"
- data-key="foo"
- onClick={[MockFunction]}
- onMouseEnter={[Function]}
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "branch": undefined,
- "id": "foo",
- },
- }
- }
- >
- <span
- className="navbar-search-item-icons little-spacer-right"
- >
- <QualifierIcon
- className="little-spacer-right"
- qualifier="TRK"
- />
- </span>
- <span
- className="navbar-search-item-match"
- >
- foo
- </span>
- </Link>
- </Tooltip>
-</li>
-`;
-
-exports[`renders selected 2`] = `
-<li
- className="active"
- key="foo"
->
- <Tooltip
- mouseEnterDelay={1}
- overlay="foo"
- placement="left"
- visible={false}
- >
- <Link
- className="navbar-search-item-link"
- data-key="foo"
- onClick={[MockFunction]}
- onMouseEnter={[Function]}
- onlyActiveOnIndex={false}
- style={Object {}}
- to={
- Object {
- "pathname": "/dashboard",
- "query": Object {
- "branch": undefined,
- "id": "foo",
- },
- }
- }
- >
- <span
- className="navbar-search-item-icons little-spacer-right"
- >
- <QualifierIcon
- className="little-spacer-right"
- qualifier="TRK"
- />
- </span>
- <span
- className="navbar-search-item-match"
- >
- foo
- </span>
- </Link>
- </Tooltip>
-</li>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders favorite 1`] = `
+<li
+ key="foo"
+>
+ <Tooltip
+ mouseEnterDelay={1}
+ overlay="foo"
+ placement="left"
+ visible={false}
+ >
+ <Link
+ className="navbar-search-item-link"
+ data-key="foo"
+ onClick={[MockFunction]}
+ onMouseEnter={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": undefined,
+ "id": "foo",
+ },
+ }
+ }
+ >
+ <span
+ className="navbar-search-item-icons little-spacer-right"
+ >
+ <FavoriteIcon
+ favorite={true}
+ size={12}
+ />
+ <QualifierIcon
+ className="little-spacer-right"
+ qualifier="TRK"
+ />
+ </span>
+ <span
+ className="navbar-search-item-match"
+ >
+ foo
+ </span>
+ </Link>
+ </Tooltip>
+</li>
+`;
+
+exports[`renders match 1`] = `
+<li
+ key="foo"
+>
+ <Tooltip
+ mouseEnterDelay={1}
+ overlay="foo"
+ placement="left"
+ visible={false}
+ >
+ <Link
+ className="navbar-search-item-link"
+ data-key="foo"
+ onClick={[MockFunction]}
+ onMouseEnter={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": undefined,
+ "id": "foo",
+ },
+ }
+ }
+ >
+ <span
+ className="navbar-search-item-icons little-spacer-right"
+ >
+ <QualifierIcon
+ className="little-spacer-right"
+ qualifier="TRK"
+ />
+ </span>
+ <span
+ className="navbar-search-item-match"
+ dangerouslySetInnerHTML={
+ Object {
+ "__html": "f<mark>o</mark>o",
+ }
+ }
+ />
+ </Link>
+ </Tooltip>
+</li>
+`;
+
+exports[`renders organizations 1`] = `
+<li
+ key="foo"
+>
+ <Tooltip
+ mouseEnterDelay={1}
+ overlay="foo"
+ placement="left"
+ visible={false}
+ >
+ <Link
+ className="navbar-search-item-link"
+ data-key="foo"
+ onClick={[MockFunction]}
+ onMouseEnter={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": undefined,
+ "id": "foo",
+ },
+ }
+ }
+ >
+ <span
+ className="navbar-search-item-icons little-spacer-right"
+ >
+ <ClockIcon
+ size={12}
+ />
+ <QualifierIcon
+ className="little-spacer-right"
+ qualifier="TRK"
+ />
+ </span>
+ <span
+ className="navbar-search-item-match"
+ >
+ foo
+ </span>
+ <div
+ className="navbar-search-item-right text-muted-2"
+ >
+ bar
+ </div>
+ </Link>
+ </Tooltip>
+</li>
+`;
+
+exports[`renders organizations 2`] = `
+<li
+ key="foo"
+>
+ <Tooltip
+ mouseEnterDelay={1}
+ overlay="foo"
+ placement="left"
+ visible={false}
+ >
+ <Link
+ className="navbar-search-item-link"
+ data-key="foo"
+ onClick={[MockFunction]}
+ onMouseEnter={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": undefined,
+ "id": "foo",
+ },
+ }
+ }
+ >
+ <span
+ className="navbar-search-item-icons little-spacer-right"
+ >
+ <ClockIcon
+ size={12}
+ />
+ <QualifierIcon
+ className="little-spacer-right"
+ qualifier="TRK"
+ />
+ </span>
+ <span
+ className="navbar-search-item-match"
+ >
+ foo
+ </span>
+ </Link>
+ </Tooltip>
+</li>
+`;
+
+exports[`renders projects 1`] = `
+<li
+ key="qwe"
+>
+ <Tooltip
+ mouseEnterDelay={1}
+ overlay="qwe"
+ placement="left"
+ visible={false}
+ >
+ <Link
+ className="navbar-search-item-link"
+ data-key="qwe"
+ onClick={[MockFunction]}
+ onMouseEnter={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": undefined,
+ "id": "qwe",
+ },
+ }
+ }
+ >
+ <span
+ className="navbar-search-item-icons little-spacer-right"
+ >
+ <ClockIcon
+ size={12}
+ />
+ <QualifierIcon
+ className="little-spacer-right"
+ qualifier="BRC"
+ />
+ </span>
+ <span
+ className="navbar-search-item-match"
+ >
+ qwe
+ </span>
+ <div
+ className="navbar-search-item-right text-muted-2"
+ >
+ foo
+ </div>
+ </Link>
+ </Tooltip>
+</li>
+`;
+
+exports[`renders recently browsed 1`] = `
+<li
+ key="foo"
+>
+ <Tooltip
+ mouseEnterDelay={1}
+ overlay="foo"
+ placement="left"
+ visible={false}
+ >
+ <Link
+ className="navbar-search-item-link"
+ data-key="foo"
+ onClick={[MockFunction]}
+ onMouseEnter={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": undefined,
+ "id": "foo",
+ },
+ }
+ }
+ >
+ <span
+ className="navbar-search-item-icons little-spacer-right"
+ >
+ <ClockIcon
+ size={12}
+ />
+ <QualifierIcon
+ className="little-spacer-right"
+ qualifier="TRK"
+ />
+ </span>
+ <span
+ className="navbar-search-item-match"
+ >
+ foo
+ </span>
+ </Link>
+ </Tooltip>
+</li>
+`;
+
+exports[`renders selected 1`] = `
+<li
+ key="foo"
+>
+ <Tooltip
+ mouseEnterDelay={1}
+ overlay="foo"
+ placement="left"
+ visible={false}
+ >
+ <Link
+ className="navbar-search-item-link"
+ data-key="foo"
+ onClick={[MockFunction]}
+ onMouseEnter={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": undefined,
+ "id": "foo",
+ },
+ }
+ }
+ >
+ <span
+ className="navbar-search-item-icons little-spacer-right"
+ >
+ <QualifierIcon
+ className="little-spacer-right"
+ qualifier="TRK"
+ />
+ </span>
+ <span
+ className="navbar-search-item-match"
+ >
+ foo
+ </span>
+ </Link>
+ </Tooltip>
+</li>
+`;
+
+exports[`renders selected 2`] = `
+<li
+ className="active"
+ key="foo"
+>
+ <Tooltip
+ mouseEnterDelay={1}
+ overlay="foo"
+ placement="left"
+ visible={false}
+ >
+ <Link
+ className="navbar-search-item-link"
+ data-key="foo"
+ onClick={[MockFunction]}
+ onMouseEnter={[Function]}
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "branch": undefined,
+ "id": "foo",
+ },
+ }
+ }
+ >
+ <span
+ className="navbar-search-item-icons little-spacer-right"
+ >
+ <QualifierIcon
+ className="little-spacer-right"
+ qualifier="TRK"
+ />
+ </span>
+ <span
+ className="navbar-search-item-match"
+ >
+ foo
+ </span>
+ </Link>
+ </Tooltip>
+</li>
+`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders "Show More" link 1`] = `
-<ul
- className="menu"
->
- <li
- className="menu-header"
- key="header-TRK"
- >
- qualifiers.TRK
- </li>
- <span
- key="foo"
- >
- foo
- </span>
- <span
- key="bar"
- >
- bar
- </span>
- <SearchShowMore
- allowMore={true}
- key="more-TRK"
- loadingMore={null}
- onMoreClick={[MockFunction]}
- onSelect={[MockFunction]}
- qualifier="TRK"
- selected={false}
- />
- <li
- className="divider"
- key="divider-BRC"
- />
- <li
- className="menu-header"
- key="header-BRC"
- >
- qualifiers.BRC
- </li>
- <span
- key="qwe"
- >
- qwe
- </span>
- <span
- key="qux"
- >
- qux
- </span>
-</ul>
-`;
-
-exports[`renders different components and dividers between them 1`] = `
-<ul
- className="menu"
->
- <li
- className="menu-header"
- key="header-TRK"
- >
- qualifiers.TRK
- </li>
- <span
- key="foo"
- >
- foo
- </span>
- <span
- key="bar"
- >
- bar
- </span>
- <li
- className="divider"
- key="divider-BRC"
- />
- <li
- className="menu-header"
- key="header-BRC"
- >
- qualifiers.BRC
- </li>
- <span
- key="qwe"
- >
- qwe
- </span>
- <span
- key="qux"
- >
- qux
- </span>
- <li
- className="divider"
- key="divider-FIL"
- />
- <li
- className="menu-header"
- key="header-FIL"
- >
- qualifiers.FIL
- </li>
- <span
- key="zux"
- >
- zux
- </span>
-</ul>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders "Show More" link 1`] = `
+<ul
+ className="menu"
+>
+ <li
+ className="menu-header"
+ key="header-TRK"
+ >
+ qualifiers.TRK
+ </li>
+ <span
+ key="foo"
+ >
+ foo
+ </span>
+ <span
+ key="bar"
+ >
+ bar
+ </span>
+ <SearchShowMore
+ allowMore={true}
+ key="more-TRK"
+ onMoreClick={[MockFunction]}
+ onSelect={[MockFunction]}
+ qualifier="TRK"
+ selected={false}
+ />
+ <li
+ className="divider"
+ key="divider-BRC"
+ />
+ <li
+ className="menu-header"
+ key="header-BRC"
+ >
+ qualifiers.BRC
+ </li>
+ <span
+ key="qwe"
+ >
+ qwe
+ </span>
+ <span
+ key="qux"
+ >
+ qux
+ </span>
+</ul>
+`;
+
+exports[`renders different components and dividers between them 1`] = `
+<ul
+ className="menu"
+>
+ <li
+ className="menu-header"
+ key="header-TRK"
+ >
+ qualifiers.TRK
+ </li>
+ <span
+ key="foo"
+ >
+ foo
+ </span>
+ <span
+ key="bar"
+ >
+ bar
+ </span>
+ <li
+ className="divider"
+ key="divider-BRC"
+ />
+ <li
+ className="menu-header"
+ key="header-BRC"
+ >
+ qualifiers.BRC
+ </li>
+ <span
+ key="qwe"
+ >
+ qwe
+ </span>
+ <span
+ key="qux"
+ >
+ qux
+ </span>
+ <li
+ className="divider"
+ key="divider-FIL"
+ />
+ <li
+ className="menu-header"
+ key="header-FIL"
+ >
+ qualifiers.FIL
+ </li>
+ <span
+ key="zux"
+ >
+ zux
+ </span>
+</ul>
+`;
+
+exports[`should render no results 1`] = `
+<div
+ id="no-results"
+/>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<li
+ className="menu-footer"
+ key="more-TRK"
+>
+ <DeferredSpinner
+ className="navbar-search-icon"
+ loading={false}
+ timeout={100}
+ >
+ <a
+ className=""
+ data-qualifier="TRK"
+ href="#"
+ onClick={[Function]}
+ onMouseEnter={[Function]}
+ >
+ <div
+ className="pull-right text-muted-2 menu-footer-note"
+ dangerouslySetInnerHTML={
+ Object {
+ "__html": "search.show_more.hint.<span class=\\"shortcut-button shortcut-button-small\\">Enter</span>",
+ }
+ }
+ />
+ <span>
+ show_more
+ </span>
+ </a>
+ </DeferredSpinner>
+</li>
+`;
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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', 'APP', '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 };
-*/
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+import { sortBy } from 'lodash';
+
+const ORDER = ['DEV', 'VW', 'SVW', 'APP', 'TRK', 'BRC', 'FIL', 'UTS'];
+
+export function sortQualifiers(qualifiers: string[]) {
+ return sortBy(qualifiers, qualifier => ORDER.indexOf(qualifier));
+}
+
+export interface ComponentResult {
+ isFavorite?: boolean;
+ isRecentlyBrowsed?: boolean;
+ key: string;
+ match?: string;
+ name: string;
+ organization?: string;
+ project?: string;
+ qualifier: string;
+}
+
+export interface Results {
+ [qualifier: string]: ComponentResult[];
+}
+
+export interface More {
+ [qualifier: string]: number;
+}