aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2018-09-05 13:51:44 +0200
committerSonarTech <sonartech@sonarsource.com>2018-09-05 20:21:03 +0200
commita52c25d808596e62e66a5977c3314fb1a00ac76e (patch)
tree2ee537d3239e7febc0f4e726a9c161ab15dd587a
parent0928b5632fd08edb53614a125bc52b3f5bee2b7d (diff)
downloadsonarqube-a52c25d808596e62e66a5977c3314fb1a00ac76e.tar.gz
sonarqube-a52c25d808596e62e66a5977c3314fb1a00ac76e.zip
rewrite global search in ts (#680)
-rw-r--r--server/sonar-web/src/main/js/app/components/RecentHistory.ts (renamed from server/sonar-web/src/main/js/app/components/RecentHistory.js)33
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/RecentHistory-test.tsx103
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap4
-rw-r--r--server/sonar-web/src/main/js/app/components/search/Search.d.ts28
-rw-r--r--server/sonar-web/src/main/js/app/components/search/Search.tsx (renamed from server/sonar-web/src/main/js/app/components/search/Search.js)215
-rw-r--r--server/sonar-web/src/main/js/app/components/search/SearchResult.tsx (renamed from server/sonar-web/src/main/js/app/components/search/SearchResult.js)62
-rw-r--r--server/sonar-web/src/main/js/app/components/search/SearchResults.js87
-rw-r--r--server/sonar-web/src/main/js/app/components/search/SearchResults.tsx79
-rw-r--r--server/sonar-web/src/main/js/app/components/search/SearchShowMore.tsx (renamed from server/sonar-web/src/main/js/app/components/search/SearchShowMore.js)39
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx (renamed from server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js)76
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.tsx (renamed from server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.js)50
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/SearchResults-test.tsx (renamed from server/sonar-web/src/main/js/app/components/search/__tests__/SearchResults-test.js)31
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/SearchShowMore-test.tsx61
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.tsx.snap (renamed from server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.js.snap)0
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap (renamed from server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap)0
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.tsx.snap (renamed from server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResults-test.js.snap)7
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchShowMore-test.tsx.snap34
-rw-r--r--server/sonar-web/src/main/js/app/components/search/utils.ts (renamed from server/sonar-web/src/main/js/app/components/search/utils.js)37
18 files changed, 556 insertions, 390 deletions
diff --git a/server/sonar-web/src/main/js/app/components/RecentHistory.js b/server/sonar-web/src/main/js/app/components/RecentHistory.ts
index 0967b38c07a..4d10570d82f 100644
--- a/server/sonar-web/src/main/js/app/components/RecentHistory.js
+++ b/server/sonar-web/src/main/js/app/components/RecentHistory.ts
@@ -17,50 +17,47 @@
* 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 type History = Array<{
+ key: string;
+ name: string;
+ icon: string;
+ organization?: string;
}>;
-*/
export default class RecentHistory {
- static get() /*: History */ {
+ static get(): History {
const history = get(RECENT_HISTORY);
if (history == null) {
return [];
} else {
try {
return JSON.parse(history);
- } catch (e) {
+ } catch {
remove(RECENT_HISTORY);
return [];
}
}
}
- static set(newHistory /*: History */) /*: void */ {
+ static set(newHistory: History) {
save(RECENT_HISTORY, JSON.stringify(newHistory));
}
- static clear() /*: void */ {
+ static clear() {
remove(RECENT_HISTORY);
}
static add(
- componentKey /*: string */,
- componentName /*: string */,
- icon /*: string */,
- organization /*: string | void */
- ) /*: void */ {
+ 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);
@@ -69,7 +66,7 @@ export default class RecentHistory {
RecentHistory.set(newHistory);
}
- static remove(componentKey /*: string */) /*: void */ {
+ static remove(componentKey: string) {
const history = RecentHistory.get();
const newHistory = history.filter(entry => entry.key !== componentKey);
RecentHistory.set(newHistory);
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/RecentHistory-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/RecentHistory-test.tsx
new file mode 100644
index 00000000000..8898e899200
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/__tests__/RecentHistory-test.tsx
@@ -0,0 +1,103 @@
+/*
+ * 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)])
+ );
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap
index 1ffdab8f252..6598367a9cc 100644
--- a/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/global/__tests__/__snapshots__/GlobalNav-test.tsx.snap
@@ -47,7 +47,7 @@ exports[`should render for SonarCloud 1`] = `
suggestions={Array []}
tooltip={false}
/>
- <Search
+ <withRouter(Search)
appState={
Object {
"canAdmin": false,
@@ -127,7 +127,7 @@ exports[`should render for SonarQube 1`] = `
suggestions={Array []}
tooltip={true}
/>
- <Search
+ <withRouter(Search)
appState={
Object {
"canAdmin": false,
diff --git a/server/sonar-web/src/main/js/app/components/search/Search.d.ts b/server/sonar-web/src/main/js/app/components/search/Search.d.ts
deleted file mode 100644
index 58ceb74bdc0..00000000000
--- a/server/sonar-web/src/main/js/app/components/search/Search.d.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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> {}
diff --git a/server/sonar-web/src/main/js/app/components/search/Search.js b/server/sonar-web/src/main/js/app/components/search/Search.tsx
index b65be42af15..9d91d8b3acd 100644
--- a/server/sonar-web/src/main/js/app/components/search/Search.js
+++ b/server/sonar-web/src/main/js/app/components/search/Search.tsx
@@ -17,14 +17,12 @@
* 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 * as React from 'react';
+import * as 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 { 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';
@@ -36,60 +34,50 @@ 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'));
-/*::
-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
- };
+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;
+}
- constructor(props /*: Props */) {
+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,
- loadingMore: null,
more: {},
open: false,
organizations: {},
projects: {},
query: '',
results: {},
- selected: null,
shortQuery: false
};
}
@@ -97,9 +85,7 @@ export default class Search extends React.PureComponent {
componentDidMount() {
this.mounted = true;
key('s', () => {
- if (this.input) {
- this.input.focus();
- }
+ this.focusInput();
this.openSearch();
return false;
});
@@ -109,7 +95,7 @@ export default class Search extends React.PureComponent {
this.nodes = {};
}
- componentDidUpdate(prevProps /*: Props */, prevState /*: State */) {
+ componentDidUpdate(_prevProps: Props, prevState: State) {
if (prevState.selected !== this.state.selected) {
this.scrollToSelected();
}
@@ -120,6 +106,12 @@ export default class Search extends React.PureComponent {
key.unbind('s');
}
+ focusInput = () => {
+ if (this.input) {
+ this.input.focus();
+ }
+ };
+
handleClickOutside = () => {
this.closeSearch(false);
};
@@ -142,29 +134,27 @@ export default class Search extends React.PureComponent {
this.setState({ open: true });
};
- closeSearch = (clear /*: boolean */ = true) => {
+ closeSearch = (clear = true) => {
if (this.input) {
this.input.blur();
}
- this.setState(
- clear
- ? {
- more: {},
- open: false,
- organizations: {},
- projects: {},
- query: '',
- results: {},
- selected: null,
- shortQuery: false
- }
- : {
- open: false
- }
- );
+ 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 */) =>
+ getPlainComponentsList = (results: Results, more: More) =>
sortQualifiers(Object.keys(results)).reduce((components, qualifier) => {
const next = [...components, ...results[qualifier].map(component => component.key)];
if (more[qualifier]) {
@@ -173,7 +163,7 @@ export default class Search extends React.PureComponent {
return next;
}, []);
- mergeWithRecentlyBrowsed = (components /*: Array<Component> */) => {
+ mergeWithRecentlyBrowsed = (components: ComponentResult[]) => {
const recentlyBrowsed = RecentHistory.get().map(component => ({
...component,
isRecentlyBrowsed: true,
@@ -188,7 +178,7 @@ export default class Search extends React.PureComponent {
}
};
- search = (query /*: string */) => {
+ search = (query: string) => {
if (query.length === 0 || query.length >= 2) {
this.setState({ loading: true });
const recentlyBrowsed = RecentHistory.get().map(component => component.key);
@@ -196,8 +186,8 @@ export default class Search extends React.PureComponent {
// 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 = {};
+ 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;
@@ -209,7 +199,7 @@ export default class Search extends React.PureComponent {
organizations: { ...state.organizations, ...keyBy(response.organizations, 'key') },
projects: { ...state.projects, ...keyBy(response.projects, 'key') },
results,
- selected: list.length > 0 ? list[0] : null,
+ selected: list.length > 0 ? list[0] : undefined,
shortQuery: query.length > 2 && response.warning === 'short_input'
}));
}
@@ -219,55 +209,60 @@ export default class Search extends React.PureComponent {
}
};
- 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);
+ 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 */) => {
+ handleQueryChange = (query: string) => {
this.setState({ query, shortQuery: query.length === 1 });
this.search(query);
};
selectPrevious = () => {
- this.setState(({ more, results, selected } /*: State */) => {
+ 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] } : undefined;
+ return index > 0 ? { selected: list[index - 1] } : null;
+ } else {
+ return null;
}
});
};
selectNext = () => {
- this.setState(({ more, results, selected } /*: State */) => {
+ 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] } : undefined;
+ return index >= 0 && index < list.length - 1 ? { selected: list[index + 1] } : null;
+ } else {
+ return null;
}
});
};
@@ -278,7 +273,7 @@ export default class Search extends React.PureComponent {
if (selected.startsWith('qualifier###')) {
this.searchMore(selected.substr(12));
} else {
- this.context.router.push(getProjectUrl(selected));
+ this.props.router.push(getProjectUrl(selected));
this.closeSearch();
}
}
@@ -287,13 +282,13 @@ export default class Search extends React.PureComponent {
scrollToSelected = () => {
if (this.state.selected) {
const node = this.nodes[this.state.selected];
- if (node) {
+ if (node && this.node) {
scrollToElement(node, { topOffset: 30, bottomOffset: 30, parent: this.node });
}
}
};
- handleKeyDown = (event /*: KeyboardEvent */) => {
+ handleKeyDown = (event: React.KeyboardEvent) => {
switch (event.keyCode) {
case 13:
event.preventDefault();
@@ -312,19 +307,21 @@ export default class Search extends React.PureComponent {
}
};
- handleSelect = (selected /*: string */) => {
+ handleSelect = (selected: string) => {
this.setState({ selected });
};
- innerRef = (component /*: string */, node /*: HTMLElement */) => {
- this.nodes[component] = node;
+ innerRef = (component: string, node: HTMLElement | null) => {
+ if (node) {
+ this.nodes[component] = node;
+ }
};
- searchInputRef = (node /*: HTMLInputElement | null */) => {
+ searchInputRef = (node: HTMLInputElement | null) => {
this.input = node;
};
- renderResult = (component /*: Component */) => (
+ renderResult = (component: ComponentResult) => (
<SearchResult
appState={this.props.appState}
component={component}
@@ -407,3 +404,5 @@ export default class Search extends React.PureComponent {
);
}
}
+
+export default withRouter<OwnProps>(Search);
diff --git a/server/sonar-web/src/main/js/app/components/search/SearchResult.js b/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx
index c8426ea96a9..7dd26bd190d 100644
--- a/server/sonar-web/src/main/js/app/components/search/SearchResult.js
+++ b/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx
@@ -17,41 +17,36 @@
* 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 * as React from 'react';
import { Link } from 'react-router';
-/*:: import type { Component } from './utils'; */
+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;
+}
-/*::
-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
-};
-*/
+interface State {
+ tooltipVisible: boolean;
+}
const TOOLTIP_DELAY = 1000;
-export default class SearchResult extends React.PureComponent {
- /*:: interval: ?number; */
- /*:: props: Props; */
- state /*: State */ = { tooltipVisible: false };
+export default class SearchResult extends React.PureComponent<Props, State> {
+ interval?: number;
+ state: State = { tooltipVisible: false };
componentDidMount() {
if (this.props.selected) {
@@ -59,7 +54,7 @@ export default class SearchResult extends React.PureComponent {
}
}
- componentWillReceiveProps(nextProps /*: Props */) {
+ componentWillReceiveProps(nextProps: Props) {
if (!this.props.selected && nextProps.selected) {
this.scheduleTooltip();
} else if (this.props.selected && !nextProps.selected) {
@@ -73,12 +68,14 @@ export default class SearchResult extends React.PureComponent {
}
scheduleTooltip = () => {
- this.interval = setTimeout(() => this.setState({ tooltipVisible: true }), TOOLTIP_DELAY);
+ this.interval = window.setTimeout(() => {
+ this.setState({ tooltipVisible: true });
+ }, TOOLTIP_DELAY);
};
unscheduleTooltip = () => {
if (this.interval) {
- clearInterval(this.interval);
+ window.clearInterval(this.interval);
}
};
@@ -86,15 +83,12 @@ export default class SearchResult extends React.PureComponent {
this.props.onSelect(this.props.component.key);
};
- renderOrganization = (component /*: Component */) => {
+ renderOrganization = (component: ComponentResult) => {
if (!this.props.appState.organizationsEnabled) {
return null;
}
- if (
- !['VW', 'SVW', 'APP', 'TRK'].includes(component.qualifier) ||
- component.organization == null
- ) {
+ if (!['VW', 'SVW', 'APP', 'TRK'].includes(component.qualifier) || !component.organization) {
return null;
}
@@ -104,7 +98,7 @@ export default class SearchResult extends React.PureComponent {
) : null;
};
- renderProject = (component /*: Component */) => {
+ renderProject = (component: ComponentResult) => {
if (!['BRC', 'FIL', 'UTS'].includes(component.qualifier) || component.project == null) {
return null;
}
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
deleted file mode 100644
index 2d8e3b7532f..00000000000
--- a/server/sonar-web/src/main/js/app/components/search/SearchResults.js
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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()
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/app/components/search/SearchResults.tsx b/server/sonar-web/src/main/js/app/components/search/SearchResults.tsx
new file mode 100644
index 00000000000..0c02e8fd58a
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/search/SearchResults.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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()
+ );
+}
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.tsx
index a05ea9b4e98..7b1d9eb479b 100644
--- a/server/sonar-web/src/main/js/app/components/search/SearchShowMore.js
+++ b/server/sonar-web/src/main/js/app/components/search/SearchShowMore.tsx
@@ -17,37 +17,36 @@
* 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 * as React from 'react';
+import * as 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; */
+interface Props {
+ allowMore: boolean;
+ loadingMore?: string;
+ onMoreClick: (qualifier: string) => void;
+ onSelect: (qualifier: string) => void;
+ qualifier: string;
+ selected: boolean;
+}
- handleMoreClick = (event /*: MouseEvent & { currentTarget: HTMLElement } */) => {
+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;
- this.props.onMoreClick(qualifier);
+ if (qualifier) {
+ this.props.onMoreClick(qualifier);
+ }
};
- handleMoreMouseEnter = (event /*: { currentTarget: HTMLElement } */) => {
+ handleMoreMouseEnter = (event: React.MouseEvent<HTMLAnchorElement>) => {
const { qualifier } = event.currentTarget.dataset;
- this.props.onSelect(`qualifier###${qualifier}`);
+ if (qualifier) {
+ this.props.onSelect(`qualifier###${qualifier}`);
+ }
};
render() {
diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx
index 01168e7a49b..a3e6645a611 100644
--- a/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js
+++ b/server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx
@@ -17,43 +17,13 @@
* 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);
-}
+import * as React from 'react';
+import { shallow, ShallowWrapper } from 'enzyme';
+import { Search } from '../Search';
+import { elementKeydown } from '../../../../helpers/testUtils';
it('selects results', () => {
- const form = render();
+ const form = shallowRender();
form.setState({
more: { TRK: 15, BRC: 0 },
open: true,
@@ -75,22 +45,52 @@ it('selects results', () => {
});
it('opens selected on enter', () => {
- const form = render();
+ const form = shallowRender();
form.setState({
open: true,
results: { TRK: [component('foo')] },
selected: 'foo'
});
const openSelected = jest.fn();
- form.instance().openSelected = openSelected;
+ (form.instance() as Search).openSelected = openSelected;
elementKeydown(form.find('SearchBox'), 13);
expect(openSelected).toBeCalled();
});
it('shows warning about short input', () => {
- const form = render();
+ 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);
+}
diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.js b/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.tsx
index 91ca09f72aa..83cfe819c49 100644
--- a/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.js
+++ b/server/sonar-web/src/main/js/app/components/search/__tests__/SearchResult-test.tsx
@@ -17,32 +17,14 @@
* 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 * as 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();
+ const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
wrapper.setProps({ selected: true });
expect(wrapper).toMatchSnapshot();
@@ -56,7 +38,7 @@ it('renders match', () => {
qualifier: 'TRK',
organization: 'bar'
};
- const wrapper = render({ component });
+ const wrapper = shallowRender({ component });
expect(wrapper).toMatchSnapshot();
});
@@ -68,7 +50,7 @@ it('renders favorite', () => {
qualifier: 'TRK',
organization: 'bar'
};
- const wrapper = render({ component });
+ const wrapper = shallowRender({ component });
expect(wrapper).toMatchSnapshot();
});
@@ -80,7 +62,7 @@ it('renders recently browsed', () => {
qualifier: 'TRK',
organization: 'bar'
};
- const wrapper = render({ component });
+ const wrapper = shallowRender({ component });
expect(wrapper).toMatchSnapshot();
});
@@ -92,7 +74,7 @@ it('renders projects', () => {
qualifier: 'BRC',
project: 'foo'
};
- const wrapper = render({ component });
+ const wrapper = shallowRender({ component });
expect(wrapper).toMatchSnapshot();
});
@@ -104,14 +86,14 @@ it('renders organizations', () => {
qualifier: 'TRK',
organization: 'bar'
};
- const wrapper = render({ appState: { organizationsEnabled: true }, component });
+ 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 = render();
+ const wrapper = shallowRender();
expect(wrapper.find('Tooltip').prop('visible')).toBe(false);
wrapper.setProps({ selected: true });
@@ -124,3 +106,19 @@ it('shows tooltip after delay', () => {
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}
+ />
+ );
+}
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.tsx
index 114bf2a67b6..f04fd8b5a58 100644
--- 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.tsx
@@ -17,17 +17,15 @@
* 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 * as React from 'react';
import { shallow } from 'enzyme';
-import SearchResults from '../SearchResults';
+import SearchResults, { Props } from '../SearchResults';
it('renders different components and dividers between them', () => {
expect(
shallow(
<SearchResults
allowMore={true}
- loadingMore={null}
more={{}}
onMoreClick={jest.fn()}
onSelect={jest.fn()}
@@ -38,7 +36,6 @@ it('renders different components and dividers between them', () => {
BRC: [component('qwe', 'BRC'), component('qux', 'BRC')],
FIL: [component('zux', 'FIL')]
}}
- selected={null}
/>
)
).toMatchSnapshot();
@@ -49,7 +46,6 @@ it('renders "Show More" link', () => {
shallow(
<SearchResults
allowMore={true}
- loadingMore={null}
more={{ TRK: 175, BRC: 0 }}
onMoreClick={jest.fn()}
onSelect={jest.fn()}
@@ -59,12 +55,31 @@ it('renders "Show More" link', () => {
TRK: [component('foo'), component('bar')],
BRC: [component('qwe', 'BRC'), component('qux', 'BRC')]
}}
- selected={null}
/>
)
).toMatchSnapshot();
});
-function component(key /*: string */, qualifier /*: string */ = 'TRK') {
+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}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/SearchShowMore-test.tsx b/server/sonar-web/src/main/js/app/components/search/__tests__/SearchShowMore-test.tsx
new file mode 100644
index 00000000000..20c94d36f64
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/search/__tests__/SearchShowMore-test.tsx
@@ -0,0 +1,61 @@
+/*
+ * 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}
+ />
+ );
+}
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.tsx.snap
index 6541c673539..6541c673539 100644
--- 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.tsx.snap
diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap
index 0a9d6781bf0..0a9d6781bf0 100644
--- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.js.snap
+++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap
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.tsx.snap
index f13cf142896..f49668c7822 100644
--- 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.tsx.snap
@@ -23,7 +23,6 @@ exports[`renders "Show More" link 1`] = `
<SearchShowMore
allowMore={true}
key="more-TRK"
- loadingMore={null}
onMoreClick={[MockFunction]}
onSelect={[MockFunction]}
qualifier="TRK"
@@ -109,3 +108,9 @@ exports[`renders different components and dividers between them 1`] = `
</span>
</ul>
`;
+
+exports[`should render no results 1`] = `
+<div
+ id="no-results"
+/>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchShowMore-test.tsx.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchShowMore-test.tsx.snap
new file mode 100644
index 00000000000..9a3d97e3982
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchShowMore-test.tsx.snap
@@ -0,0 +1,34 @@
+// 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>
+`;
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.ts
index 24e22cdf647..1a4d8a92135 100644
--- a/server/sonar-web/src/main/js/app/components/search/utils.js
+++ b/server/sonar-web/src/main/js/app/components/search/utils.ts
@@ -17,32 +17,29 @@
* 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> */) {
+export function sortQualifiers(qualifiers: 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 interface ComponentResult {
+ isFavorite?: boolean;
+ isRecentlyBrowsed?: boolean;
+ key: string;
+ match?: string;
+ name: string;
+ organization?: string;
+ project?: string;
+ qualifier: string;
+}
-/*::
-export type Results = { [qualifier: string]: Array<Component> };
-*/
+export interface Results {
+ [qualifier: string]: ComponentResult[];
+}
-/*::
-export type More = { [string]: number };
-*/
+export interface More {
+ [qualifier: string]: number;
+}