]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10207 Show a loading spinner in tags selector
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Thu, 22 Feb 2018 11:08:16 +0000 (12:08 +0100)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 23 Feb 2018 15:34:13 +0000 (16:34 +0100)
19 files changed:
server/sonar-web/src/main/js/api/issues.ts
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx
server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx
server/sonar-web/src/main/js/components/common/MultiSelect.tsx
server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap
server/sonar-web/src/main/js/components/controls/SearchBox.css
server/sonar-web/src/main/js/components/controls/SearchBox.tsx
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchBox-test.tsx.snap
server/sonar-web/src/main/js/components/issue/components/IssueTags.js
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTags-test.js.snap
server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js [deleted file]
server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.js [deleted file]
server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/tags/TagsSelector.tsx
server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.tsx

index 9830e627e54658cd99a88ed2b3168e2af51c83b4..f5ec6453bccccb80dd707a00e3461463b0926d80 100644 (file)
@@ -20,6 +20,7 @@
 import { FacetValue } from '../app/types';
 import { getJSON, post, postJSON, RequestData } from '../helpers/request';
 import { RawIssue } from '../helpers/issues';
+import throwGlobalError from '../app/utils/throwGlobalError';
 
 export interface IssueResponse {
   components?: Array<{}>;
@@ -117,7 +118,9 @@ export function getIssuesCount(query: RequestData): Promise<any> {
 export function searchIssueTags(
   data: { organization?: string; ps?: number; q?: string } = { ps: 100 }
 ): Promise<string[]> {
-  return getJSON('/api/issues/tags', data).then(r => r.tags);
+  return getJSON('/api/issues/tags', data)
+    .then(r => r.tags)
+    .catch(throwGlobalError);
 }
 
 export function getIssueChangelog(issue: string): Promise<any> {
index d60647520709a1472b88fd68a527826e82fcff97..ddd9a5d54edcf8f0a9750972f7c39825047c4111 100644 (file)
@@ -32,7 +32,7 @@ interface Props {
 }
 
 interface State {
-  searchResult: any[];
+  searchResult: string[];
 }
 
 const LIST_SIZE = 10;
@@ -43,7 +43,6 @@ export default class RuleDetailsTagsPopup extends React.PureComponent<Props, Sta
 
   componentDidMount() {
     this.mounted = true;
-    this.onSearch('');
   }
 
   componentWillUnmount() {
@@ -51,7 +50,7 @@ export default class RuleDetailsTagsPopup extends React.PureComponent<Props, Sta
   }
 
   onSearch = (query: string) => {
-    getRuleTags({
+    return getRuleTags({
       q: query,
       ps: Math.min(this.props.tags.length + LIST_SIZE, 100),
       organization: this.props.organization
@@ -77,13 +76,13 @@ export default class RuleDetailsTagsPopup extends React.PureComponent<Props, Sta
   render() {
     return (
       <TagsSelector
-        position={this.props.popupPosition || {}}
-        tags={this.state.searchResult}
-        selectedTags={this.props.tags}
         listSize={LIST_SIZE}
         onSearch={this.onSearch}
         onSelect={this.onSelect}
         onUnselect={this.onUnselect}
+        position={this.props.popupPosition || {}}
+        selectedTags={this.props.tags}
+        tags={this.state.searchResult}
       />
     );
   }
index 2e2bb774a8013dd7fcc56f5001348ac5eb4b914a..2fdd9fe1f6e4a2813478cc5cf8e543c572cee660 100644 (file)
@@ -37,17 +37,29 @@ interface State {
 const LIST_SIZE = 10;
 
 export default class MetaTagsSelector extends React.PureComponent<Props, State> {
+  mounted = false;
   state: State = { searchResult: [] };
 
   componentDidMount() {
-    this.onSearch('');
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
   }
 
   onSearch = (query: string) => {
-    searchProjectTags({
+    return searchProjectTags({
       q: query,
       ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100)
-    }).then(result => this.setState({ searchResult: result.tags }), () => {});
+    }).then(
+      ({ tags }) => {
+        if (this.mounted) {
+          this.setState({ searchResult: tags });
+        }
+      },
+      () => {}
+    );
   };
 
   onSelect = (tag: string) => {
@@ -61,13 +73,13 @@ export default class MetaTagsSelector extends React.PureComponent<Props, State>
   render() {
     return (
       <TagsSelector
-        position={this.props.position}
-        tags={this.state.searchResult}
-        selectedTags={this.props.selectedTags}
         listSize={LIST_SIZE}
         onSearch={this.onSearch}
         onSelect={this.onSelect}
         onUnselect={this.onUnselect}
+        position={this.props.position}
+        selectedTags={this.props.selectedTags}
+        tags={this.state.searchResult}
       />
     );
   }
index bc235e5a1af159c9f27f06155c6e45af6d48d048..91a68f25fe887b698967b46425ff8063a75e6738 100644 (file)
@@ -23,21 +23,22 @@ import MultiSelectOption from './MultiSelectOption';
 import SearchBox from '../controls/SearchBox';
 
 interface Props {
-  selectedElements: Array<string>;
-  elements: Array<string>;
+  elements: string[];
   listSize?: number;
-  onSearch: (query: string) => void;
+  onSearch: (query: string) => Promise<void>;
   onSelect: (item: string) => void;
   onUnselect: (item: string) => void;
-  validateSearchInput?: (value: string) => string;
   placeholder: string;
+  selectedElements: string[];
+  validateSearchInput?: (value: string) => string;
 }
 
 interface State {
-  query: string;
-  selectedElements: Array<string>;
-  unselectedElements: Array<string>;
   activeIdx: number;
+  loading: boolean;
+  query: string;
+  selectedElements: string[];
+  unselectedElements: string[];
 }
 
 interface DefaultProps {
@@ -50,6 +51,7 @@ type PropsWithDefault = Props & DefaultProps;
 export default class MultiSelect extends React.PureComponent<Props, State> {
   container?: HTMLDivElement | null;
   searchInput?: HTMLInputElement | null;
+  mounted = false;
 
   static defaultProps: DefaultProps = {
     listSize: 10,
@@ -59,14 +61,17 @@ export default class MultiSelect extends React.PureComponent<Props, State> {
   constructor(props: Props) {
     super(props);
     this.state = {
+      activeIdx: 0,
+      loading: true,
       query: '',
       selectedElements: [],
-      unselectedElements: [],
-      activeIdx: 0
+      unselectedElements: []
     };
   }
 
   componentDidMount() {
+    this.mounted = true;
+    this.onSearchQuery('');
     this.updateSelectedElements(this.props);
     this.updateUnselectedElements(this.props as PropsWithDefault);
     if (this.container) {
@@ -96,6 +101,7 @@ export default class MultiSelect extends React.PureComponent<Props, State> {
   }
 
   componentWillUnmount() {
+    this.mounted = false;
     if (this.container) {
       this.container.removeEventListener('keydown', this.handleKeyboard);
     }
@@ -122,14 +128,14 @@ export default class MultiSelect extends React.PureComponent<Props, State> {
   handleKeyboard = (evt: KeyboardEvent) => {
     switch (evt.keyCode) {
       case 40: // down
-        this.setState(this.selectNextElement);
         evt.stopPropagation();
         evt.preventDefault();
+        this.setState(this.selectNextElement);
         break;
       case 38: // up
-        this.setState(this.selectPreviousElement);
         evt.stopPropagation();
         evt.preventDefault();
+        this.setState(this.selectPreviousElement);
         break;
       case 37: // left
       case 39: // right
@@ -144,8 +150,8 @@ export default class MultiSelect extends React.PureComponent<Props, State> {
   };
 
   onSearchQuery = (query: string) => {
-    this.setState({ query, activeIdx: 0 });
-    this.props.onSearch(query);
+    this.setState({ activeIdx: 0, loading: true, query });
+    this.props.onSearch(query).then(this.stopLoading, this.stopLoading);
   };
 
   onSelectItem = (item: string) => {
@@ -218,6 +224,12 @@ export default class MultiSelect extends React.PureComponent<Props, State> {
     }
   };
 
+  stopLoading = () => {
+    if (this.mounted) {
+      this.setState({ loading: false });
+    }
+  };
+
   toggleSelect = (item: string) => {
     if (this.props.selectedElements.indexOf(item) === -1) {
       this.onSelectItem(item);
@@ -236,6 +248,7 @@ export default class MultiSelect extends React.PureComponent<Props, State> {
           <SearchBox
             autoFocus={true}
             className="little-spacer-top"
+            loading={this.state.loading}
             onChange={this.handleSearchChange}
             placeholder={this.props.placeholder}
             value={query}
index 13bb1651866539f32458bf2449fdf362dbc67d5b..097fd543c364f0adce90c694d841648895cff873 100644 (file)
@@ -24,7 +24,7 @@ import MultiSelect from '../MultiSelect';
 const props = {
   selectedElements: ['bar'],
   elements: [],
-  onSearch: () => {},
+  onSearch: () => Promise.resolve(),
   onSelect: () => {},
   onUnselect: () => {},
   placeholder: ''
index 15c17fc74ad101d06a0860b5bdc24786f0be127c..69b2384f3963ea176eb3613b87b2fb8dd55b40c9 100644 (file)
@@ -10,6 +10,7 @@ exports[`should render multiselect with selected elements 1`] = `
     <SearchBox
       autoFocus={true}
       className="little-spacer-top"
+      loading={true}
       onChange={[Function]}
       placeholder=""
       value=""
@@ -40,6 +41,7 @@ exports[`should render multiselect with selected elements 2`] = `
     <SearchBox
       autoFocus={true}
       className="little-spacer-top"
+      loading={true}
       onChange={[Function]}
       placeholder=""
       value=""
@@ -84,6 +86,7 @@ exports[`should render multiselect with selected elements 3`] = `
     <SearchBox
       autoFocus={true}
       className="little-spacer-top"
+      loading={true}
       onChange={[Function]}
       placeholder=""
       value=""
@@ -128,6 +131,7 @@ exports[`should render multiselect with selected elements 4`] = `
     <SearchBox
       autoFocus={true}
       className="little-spacer-top"
+      loading={true}
       onChange={[Function]}
       placeholder=""
       value="test"
index daecb042ac096349caa87691977b72eae5764813..7c5d32ecae49edceda7be7554f43e7f3e2361c2d 100644 (file)
   transition: color 0.3s ease;
 }
 
+.search-box > .spinner {
+  position: absolute;
+  top: 4px;
+  left: 5px;
+}
+
 .search-box-clear {
   position: absolute;
   top: 4px;
index b5b29941883a8fbb5db6df659159924791d733be..84329946fedc16eb54eeefaf459baf2cb096c949 100644 (file)
@@ -20,8 +20,9 @@
 import * as React from 'react';
 import * as classNames from 'classnames';
 import { debounce, Cancelable } from 'lodash';
-import SearchIcon from '../icons-components/SearchIcon';
 import ClearIcon from '../icons-components/ClearIcon';
+import SearchIcon from '../icons-components/SearchIcon';
+import DeferredSpinner from '../common/DeferredSpinner';
 import { ButtonIcon } from '../ui/buttons';
 import * as theme from '../../app/theme';
 import { translateWithParameters } from '../../helpers/l10n';
@@ -32,6 +33,7 @@ interface Props {
   className?: string;
   innerRef?: (node: HTMLInputElement | null) => void;
   id?: string;
+  loading?: boolean;
   minLength?: number;
   onChange: (value: string) => void;
   onClick?: React.MouseEventHandler<HTMLInputElement>;
@@ -119,7 +121,7 @@ export default class SearchBox extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { minLength } = this.props;
+    const { loading, minLength } = this.props;
     const { value } = this.state;
 
     const inputClassName = classNames('search-box-input', {
@@ -145,7 +147,9 @@ export default class SearchBox extends React.PureComponent<Props, State> {
           value={value}
         />
 
-        <SearchIcon className="search-box-magnifier" />
+        <DeferredSpinner loading={loading !== undefined ? loading : false}>
+          <SearchIcon className="search-box-magnifier" />
+        </DeferredSpinner>
 
         {value && (
           <ButtonIcon
index d69e12de1e3a0c3a886821c41dd21b566be196e6..25b226be5a2b163f69756a2f9c0d402dddcaaa6a 100644 (file)
@@ -14,9 +14,14 @@ exports[`renders 1`] = `
     type="search"
     value="foo"
   />
-  <SearchIcon
-    className="search-box-magnifier"
-  />
+  <DeferredSpinner
+    loading={false}
+    timeout={100}
+  >
+    <SearchIcon
+      className="search-box-magnifier"
+    />
+  </DeferredSpinner>
   <ButtonIcon
     className="button-tiny search-box-clear"
     color="#999"
index e4ba9ab996805ade0df8451c0d75d61855515637..e40a85b71afb8c479ffcb21bd3ca07f7aa00e699 100644 (file)
@@ -68,7 +68,6 @@ export default class IssueTags extends React.PureComponent {
           togglePopup={this.toggleSetTags}
           popup={
             <SetIssueTagsPopup
-              onFail={this.props.onFail}
               organization={issue.projectOrganization}
               selectedTags={issue.tags}
               setTags={this.setTags}
index 178f8b75e19613b33674275289b7d4ddbc8bcbbe..fa30aff7c20968a06fd666aaccd578e046e13b72 100644 (file)
@@ -23,7 +23,6 @@ exports[`should open the popup when the button is clicked 2`] = `
   isOpen={true}
   popup={
     <SetIssueTagsPopup
-      onFail={[MockFunction]}
       organization="foo"
       selectedTags={
         Array [
@@ -59,7 +58,6 @@ exports[`should render with the action 1`] = `
   isOpen={false}
   popup={
     <SetIssueTagsPopup
-      onFail={[MockFunction]}
       organization="foo"
       selectedTags={
         Array [
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js
deleted file mode 100644 (file)
index c334412..0000000
+++ /dev/null
@@ -1,91 +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 { without } from 'lodash';
-import TagsSelector from '../../../components/tags/TagsSelector';
-import { searchIssueTags } from '../../../api/issues';
-
-/*::
-type Props = {
-  popupPosition?: {},
-  onFail: Error => void,
-  organization: string,
-  selectedTags: Array<string>,
-  setTags: (Array<string>) => void
-};
-*/
-
-/*::
-type State = {
-  searchResult: Array<string>
-};
-*/
-
-const LIST_SIZE = 10;
-
-export default class SetIssueTagsPopup extends React.PureComponent {
-  /*:: mounted: boolean; */
-  /*:: props: Props; */
-  state /*: State */ = { searchResult: [] };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.onSearch('');
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  onSearch = (query /*: string */) => {
-    searchIssueTags({
-      q: query,
-      ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100),
-      organization: this.props.organization
-    }).then((tags /*: Array<string> */) => {
-      if (this.mounted) {
-        this.setState({ searchResult: tags });
-      }
-    }, this.props.onFail);
-  };
-
-  onSelect = (tag /*: string */) => {
-    this.props.setTags([...this.props.selectedTags, tag]);
-  };
-
-  onUnselect = (tag /*: string */) => {
-    this.props.setTags(without(this.props.selectedTags, tag));
-  };
-
-  render() {
-    return (
-      <TagsSelector
-        position={this.props.popupPosition}
-        tags={this.state.searchResult}
-        selectedTags={this.props.selectedTags}
-        listSize={LIST_SIZE}
-        onSearch={this.onSearch}
-        onSelect={this.onSelect}
-        onUnselect={this.onUnselect}
-      />
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx
new file mode 100644 (file)
index 0000000..7aa3beb
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * 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 { without } from 'lodash';
+import { BubblePopupPosition } from '../../../components/common/BubblePopup';
+import TagsSelector from '../../../components/tags/TagsSelector';
+import { searchIssueTags } from '../../../api/issues';
+
+interface Props {
+  popupPosition: BubblePopupPosition;
+  organization: string;
+  selectedTags: string[];
+  setTags: (tags: string[]) => void;
+}
+
+interface State {
+  searchResult: string[];
+}
+
+const LIST_SIZE = 10;
+
+export default class SetIssueTagsPopup extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { searchResult: [] };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  onSearch = (query: string) => {
+    return searchIssueTags({
+      q: query,
+      ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100),
+      organization: this.props.organization
+    }).then(
+      (tags: string[]) => {
+        if (this.mounted) {
+          this.setState({ searchResult: tags });
+        }
+      },
+      () => {}
+    );
+  };
+
+  onSelect = (tag: string) => {
+    this.props.setTags([...this.props.selectedTags, tag]);
+  };
+
+  onUnselect = (tag: string) => {
+    this.props.setTags(without(this.props.selectedTags, tag));
+  };
+
+  render() {
+    return (
+      <TagsSelector
+        listSize={LIST_SIZE}
+        onSearch={this.onSearch}
+        onSelect={this.onSelect}
+        onUnselect={this.onUnselect}
+        position={this.props.popupPosition}
+        selectedTags={this.props.selectedTags}
+        tags={this.state.searchResult}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.js b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.js
deleted file mode 100644 (file)
index d2cdf42..0000000
+++ /dev/null
@@ -1,35 +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 { shallow } from 'enzyme';
-import React from 'react';
-import SetIssueTagsPopup from '../SetIssueTagsPopup';
-
-it('should render tags popup correctly', () => {
-  const element = shallow(
-    <SetIssueTagsPopup
-      onFail={jest.fn()}
-      organization="foo"
-      selectedTags="mytag"
-      setTags={jest.fn()}
-    />
-  );
-  element.setState({ searchResult: ['mytag', 'test', 'second'] });
-  expect(element).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx b/server/sonar-web/src/main/js/components/issue/popups/__tests__/SetIssueTagsPopup-test.tsx
new file mode 100644 (file)
index 0000000..f66e329
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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 SetIssueTagsPopup from '../SetIssueTagsPopup';
+
+it('should render tags popup correctly', () => {
+  const element = shallow(
+    <SetIssueTagsPopup
+      organization="foo"
+      popupPosition={{}}
+      selectedTags={['mytag']}
+      setTags={jest.fn()}
+    />
+  );
+  element.setState({ searchResult: ['mytag', 'test', 'second'] });
+  expect(element).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.js.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.js.snap
deleted file mode 100644 (file)
index 3eee842..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render tags popup correctly 1`] = `
-<TagsSelector
-  listSize={10}
-  onSearch={[Function]}
-  onSelect={[Function]}
-  onUnselect={[Function]}
-  selectedTags="mytag"
-  tags={
-    Array [
-      "mytag",
-      "test",
-      "second",
-    ]
-  }
-/>
-`;
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap
new file mode 100644 (file)
index 0000000..36d73b1
--- /dev/null
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render tags popup correctly 1`] = `
+<TagsSelector
+  listSize={10}
+  onSearch={[Function]}
+  onSelect={[Function]}
+  onUnselect={[Function]}
+  position={Object {}}
+  selectedTags={
+    Array [
+      "mytag",
+    ]
+  }
+  tags={
+    Array [
+      "mytag",
+      "test",
+      "second",
+    ]
+  }
+/>
+`;
index 82d42c93710fae3064485b313bbabbc4f93f1a31..120f7cdeef5eaba48837b2f560d508a4f9482346 100644 (file)
@@ -24,29 +24,29 @@ import { translate } from '../../helpers/l10n';
 import './TagsList.css';
 
 interface Props {
-  position: BubblePopupPosition;
-  tags: string[];
-  selectedTags: string[];
   listSize: number;
-  onSearch: (query: string) => void;
+  onSearch: (query: string) => Promise<void>;
   onSelect: (item: string) => void;
   onUnselect: (item: string) => void;
+  position: BubblePopupPosition;
+  selectedTags: string[];
+  tags: string[];
 }
 
 export default function TagsSelector(props: Props) {
   return (
     <BubblePopup
-      position={props.position}
-      customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300">
+      customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300"
+      position={props.position}>
       <MultiSelect
         elements={props.tags}
-        selectedElements={props.selectedTags}
         listSize={props.listSize}
         onSearch={props.onSearch}
         onSelect={props.onSelect}
         onUnselect={props.onUnselect}
-        validateSearchInput={validateTag}
         placeholder={translate('search.search_for_tags')}
+        selectedElements={props.selectedTags}
+        validateSearchInput={validateTag}
       />
     </BubblePopup>
   );
index 1b5169a294085557cd26492f9b8652113f0117f8..f2dd87eda022f5a9f5d16918386995149e9799c1 100644 (file)
@@ -22,13 +22,13 @@ import { shallow } from 'enzyme';
 import TagsSelector, { validateTag } from '../TagsSelector';
 
 const props = {
-  position: { right: 0, top: 0 },
   listSize: 10,
-  tags: ['foo', 'bar', 'baz'],
-  selectedTags: ['bar'],
-  onSearch: () => {},
+  onSearch: () => Promise.resolve(),
   onSelect: () => {},
-  onUnselect: () => {}
+  onUnselect: () => {},
+  position: { right: 0, top: 0 },
+  selectedTags: ['bar'],
+  tags: ['foo', 'bar', 'baz']
 };
 
 it('should render with selected tags', () => {
@@ -37,7 +37,7 @@ it('should render with selected tags', () => {
 });
 
 it('should render without tags at all', () => {
-  expect(shallow(<TagsSelector {...props} tags={[]} selectedTags={[]} />)).toMatchSnapshot();
+  expect(shallow(<TagsSelector {...props} selectedTags={[]} tags={[]} />)).toMatchSnapshot();
 });
 
 it('should validate tags correctly', () => {