]> source.dussan.org Git - sonarqube.git/commitdiff
Add back missing action to bulk unassign issues
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Tue, 30 Jan 2018 15:02:17 +0000 (16:02 +0100)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 31 Jan 2018 08:51:31 +0000 (09:51 +0100)
server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js
server/sonar-web/src/main/js/apps/issues/utils.js
server/sonar-web/src/main/js/components/controls/SearchSelect.js [deleted file]
server/sonar-web/src/main/js/components/controls/SearchSelect.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/SearchSelect-test.js [deleted file]
server/sonar-web/src/main/js/components/controls/__tests__/SearchSelect-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetFooter-test.js.snap

index 6ac8ce8b88f39dab3e0c1c5a949ca5e017e09102..c5d585adcf3e8ea57d5c366f7e0eba738cfb73b8 100644 (file)
@@ -109,44 +109,40 @@ export default class BulkChangeModal extends React.PureComponent {
     this.mounted = false;
   }
 
+  loadIssues = () => this.props.fetchIssues({ additionalFields: 'actions,transitions', ps: 250 });
+
+  getDefaultAssignee = () => {
+    const { currentUser } = this.props;
+    const { issues } = this.state;
+    const options = [];
+
+    if (currentUser.isLoggedIn) {
+      const canBeAssignedToMe =
+        issues.filter(issue => issue.assignee !== currentUser.login).length > 0;
+      if (canBeAssignedToMe) {
+        options.push({
+          avatar: currentUser.avatar,
+          label: currentUser.name,
+          value: currentUser.login
+        });
+      }
+    }
+
+    const canBeUnassigned = issues.filter(issue => issue.assignee).length > 0;
+    if (canBeUnassigned) {
+      options.push({ label: translate('unassigned'), value: '' });
+    }
+
+    return options;
+  };
+
   handleCloseClick = (e /*: Event & { target: HTMLElement } */) => {
     e.preventDefault();
     e.target.blur();
     this.props.onClose();
   };
 
-  loadIssues = () => {
-    return this.props.fetchIssues({ additionalFields: 'actions,transitions', ps: 250 });
-  };
-
-  handleAssigneeSearch = (query /*: string */) => {
-    if (query.length > 1) {
-      return searchAssignees(query, this.state.organization);
-    } else {
-      const { currentUser } = this.props;
-      const { issues } = this.state;
-      const options = [];
-
-      if (currentUser.isLoggedIn) {
-        const canBeAssignedToMe =
-          issues.filter(issue => issue.assignee !== currentUser.login).length > 0;
-        if (canBeAssignedToMe) {
-          options.push({
-            email: currentUser.email,
-            label: currentUser.name,
-            value: currentUser.login
-          });
-        }
-      }
-
-      const canBeUnassigned = issues.filter(issue => issue.assignee).length > 0;
-      if (canBeUnassigned) {
-        options.push({ label: translate('unassigned'), value: '' });
-      }
-
-      return Promise.resolve(options);
-    }
-  };
+  handleAssigneeSearch = (query /*: string */) => searchAssignees(query, this.state.organization);
 
   handleAssigneeSelect = (assignee /*: string */) => {
     this.setState({ assignee });
@@ -270,19 +266,16 @@ export default class BulkChangeModal extends React.PureComponent {
     </div>
   );
 
-  renderAssigneeOption = (option /*: { avatar?: string, email?: string, label: string } */) => (
-    <span>
-      {option.avatar != null && (
-        <Avatar
-          className="little-spacer-right"
-          hash={option.avatar}
-          name={option.label}
-          size={16}
-        />
-      )}
-      {option.label}
-    </span>
-  );
+  renderAssigneeOption = (option /*: { avatar?: string, email?: string, label: string } */) => {
+    return (
+      <span>
+        {option.avatar != null && (
+          <Avatar className="spacer-right" hash={option.avatar} name={option.label} size={16} />
+        )}
+        {option.label}
+      </span>
+    );
+  };
 
   renderAssigneeField = () => {
     const affected /*: number */ = this.state.issues.filter(hasAction('assign')).length;
@@ -293,6 +286,7 @@ export default class BulkChangeModal extends React.PureComponent {
 
     const input = (
       <SearchSelect
+        defaultOptions={this.getDefaultAssignee()}
         onSearch={this.handleAssigneeSearch}
         onSelect={this.handleAssigneeSelect}
         minimumQueryLength={2}
index d4455f7b03163117c608038959d2ddf72ebc3648..0878f213bd0089d91bfe3e64467e2de244ad291c 100644 (file)
@@ -218,7 +218,7 @@ export type Component = {
 /*::
 export type CurrentUser =
   | { isLoggedIn: false }
-  | { isLoggedIn: true, email?: string, login: string, name: string };
+  | { isLoggedIn: true, avatar:string, email?: string, login: string, name: string };
 */
 
 export const searchAssignees = (query /*: string */, organization /*: ?string */) => {
diff --git a/server/sonar-web/src/main/js/components/controls/SearchSelect.js b/server/sonar-web/src/main/js/components/controls/SearchSelect.js
deleted file mode 100644 (file)
index bed2a3a..0000000
+++ /dev/null
@@ -1,135 +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 { debounce } from 'lodash';
-import Select from '../../components/controls/Select';
-import { translate, translateWithParameters } from '../../helpers/l10n';
-
-/*::
-type Option = { label: string, value: string };
-*/
-
-/*::
-type Props = {|
-  autofocus: boolean,
-  minimumQueryLength: number,
-  onSearch: (query: string) => Promise<Array<Option>>,
-  onSelect: (value: string) => void,
-  renderOption?: (option: Object) => React.Element<*>,
-  resetOnBlur: boolean,
-  value?: string
-|};
-*/
-
-/*::
-type State = {
-  loading: boolean,
-  options: Array<Option>,
-  query: string
-};
-*/
-
-export default class SearchSelect extends React.PureComponent {
-  /*:: mounted: boolean; */
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  static defaultProps = {
-    autofocus: true,
-    minimumQueryLength: 2,
-    resetOnBlur: true
-  };
-
-  constructor(props /*: Props */) {
-    super(props);
-    this.state = { loading: false, options: [], query: '' };
-    this.search = debounce(this.search, 250);
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  search = (query /*: string */) => {
-    this.props.onSearch(query).then(
-      options => {
-        if (this.mounted) {
-          this.setState({ loading: false, options });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
-  };
-
-  handleChange = (option /*: Option */) => {
-    this.props.onSelect(option.value);
-  };
-
-  handleInputChange = (query /*: string */) => {
-    // `onInputChange` is called with an empty string after a user selects a value
-    // in this case we shouldn't reset `options`, because it also resets select value :(
-    if (query.length >= this.props.minimumQueryLength) {
-      this.setState({ loading: true, query });
-      this.search(query);
-    } else if (query.length > 0) {
-      this.setState({ options: [], query });
-    }
-  };
-
-  // disable internal filtering
-  handleFilterOption = () => true;
-
-  render() {
-    return (
-      <Select
-        autofocus={this.props.autofocus}
-        cache={false}
-        className="input-super-large"
-        clearable={false}
-        filterOption={this.handleFilterOption}
-        isLoading={this.state.loading}
-        noResultsText={
-          this.state.query.length < this.props.minimumQueryLength
-            ? translateWithParameters('select2.tooShort', this.props.minimumQueryLength)
-            : translate('select2.noMatches')
-        }
-        onBlurResetsInput={this.props.resetOnBlur}
-        onChange={this.handleChange}
-        onInputChange={this.handleInputChange}
-        onOpen={this.props.minimumQueryLength === 0 ? this.handleInputChange : undefined}
-        optionRenderer={this.props.renderOption}
-        options={this.state.options}
-        placeholder={translate('search_verb')}
-        searchable={true}
-        value={this.props.value}
-        valueRenderer={this.props.renderOption}
-      />
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/controls/SearchSelect.tsx b/server/sonar-web/src/main/js/components/controls/SearchSelect.tsx
new file mode 100644 (file)
index 0000000..07f9b2d
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * 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 { debounce } from 'lodash';
+import Select from '../../components/controls/Select';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+
+interface Option {
+  label: string;
+  value: string;
+}
+
+interface Props {
+  autofocus?: boolean;
+  defaultOptions?: Option[];
+  minimumQueryLength?: number;
+  onSearch: (query: string) => Promise<Option[]>;
+  onSelect: (value: string) => void;
+  renderOption?: (option: Object) => JSX.Element;
+  resetOnBlur?: boolean;
+  value?: string;
+}
+
+interface State {
+  loading: boolean;
+  options: Option[];
+  query: string;
+}
+
+export default class SearchSelect extends React.PureComponent<Props, State> {
+  mounted: boolean;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = { loading: false, options: props.defaultOptions || [], query: '' };
+    this.handleSearch = debounce(this.handleSearch, 250);
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  get autofocus() {
+    return this.props.autofocus !== undefined ? this.props.autofocus : true;
+  }
+
+  get minimumQueryLength() {
+    return this.props.minimumQueryLength !== undefined ? this.props.minimumQueryLength : 2;
+  }
+  get resetOnBlur() {
+    return this.props.resetOnBlur !== undefined ? this.props.resetOnBlur : true;
+  }
+
+  handleSearch = (query: string) =>
+    this.props.onSearch(query).then(
+      options => {
+        if (this.mounted) {
+          this.setState({ loading: false, options });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+
+  handleChange = (option: Option) => this.props.onSelect(option.value);
+
+  handleInputChange = (query: string) => {
+    // `onInputChange` is called with an empty string after a user selects a value
+    // in this case we shouldn't reset `options`, because it also resets select value :(
+    if (query.length >= this.minimumQueryLength) {
+      this.setState({ loading: true, query });
+      this.handleSearch(query);
+    } else {
+      const options = (query.length === 0 && this.props.defaultOptions) || [];
+      this.setState({ options, query });
+    }
+  };
+
+  // disable internal filtering
+  handleFilterOption = () => true;
+
+  render() {
+    return (
+      <Select
+        autofocus={this.autofocus}
+        className="input-super-large"
+        clearable={false}
+        filterOption={this.handleFilterOption}
+        isLoading={this.state.loading}
+        noResultsText={
+          this.state.query.length < this.minimumQueryLength
+            ? translateWithParameters('select2.tooShort', this.minimumQueryLength)
+            : translate('select2.noMatches')
+        }
+        onBlurResetsInput={this.resetOnBlur}
+        onChange={this.handleChange}
+        onInputChange={this.handleInputChange}
+        optionRenderer={this.props.renderOption}
+        options={this.state.options}
+        placeholder={translate('search_verb')}
+        searchable={true}
+        value={this.props.value}
+        valueRenderer={this.props.renderOption}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/SearchSelect-test.js b/server/sonar-web/src/main/js/components/controls/__tests__/SearchSelect-test.js
deleted file mode 100644 (file)
index 0f8017b..0000000
+++ /dev/null
@@ -1,50 +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 React from 'react';
-import { shallow } from 'enzyme';
-import SearchSelect from '../SearchSelect';
-
-jest.mock('lodash', () => {
-  const lodash = require.requireActual('lodash');
-  lodash.debounce = jest.fn(fn => fn);
-  return lodash;
-});
-
-it('should render Select', () => {
-  expect(shallow(<SearchSelect onSearch={jest.fn()} onSelect={jest.fn()} />)).toMatchSnapshot();
-});
-
-it('should call onSelect', () => {
-  const onSelect = jest.fn();
-  const wrapper = shallow(<SearchSelect onSearch={jest.fn()} onSelect={onSelect} />);
-  wrapper.prop('onChange')({ value: 'foo' });
-  expect(onSelect).lastCalledWith('foo');
-});
-
-it('should call onSearch', () => {
-  const onSearch = jest.fn().mockReturnValue(Promise.resolve([]));
-  const wrapper = shallow(
-    <SearchSelect minimumQueryLength={2} onSearch={onSearch} onSelect={jest.fn()} />
-  );
-  wrapper.prop('onInputChange')('f');
-  expect(onSearch).not.toHaveBeenCalled();
-  wrapper.prop('onInputChange')('foo');
-  expect(onSearch).lastCalledWith('foo');
-});
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/SearchSelect-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/SearchSelect-test.tsx
new file mode 100644 (file)
index 0000000..34d9645
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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 SearchSelect from '../SearchSelect';
+
+jest.mock('lodash', () => {
+  const lodash = require.requireActual('lodash');
+  lodash.debounce = jest.fn(fn => fn);
+  return lodash;
+});
+
+it('should render Select', () => {
+  expect(shallow(<SearchSelect onSearch={jest.fn()} onSelect={jest.fn()} />)).toMatchSnapshot();
+});
+
+it('should call onSelect', () => {
+  const onSelect = jest.fn();
+  const wrapper = shallow(<SearchSelect onSearch={jest.fn()} onSelect={onSelect} />);
+  wrapper.prop('onChange')({ value: 'foo' });
+  expect(onSelect).lastCalledWith('foo');
+});
+
+it('should call onSearch', () => {
+  const onSearch = jest.fn().mockReturnValue(Promise.resolve([]));
+  const wrapper = shallow(
+    <SearchSelect minimumQueryLength={2} onSearch={onSearch} onSelect={jest.fn()} />
+  );
+  wrapper.prop('onInputChange')('f');
+  expect(onSearch).not.toHaveBeenCalled();
+  wrapper.prop('onInputChange')('foo');
+  expect(onSearch).lastCalledWith('foo');
+});
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.js.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.js.snap
deleted file mode 100644 (file)
index 00b3422..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render Select 1`] = `
-<Select
-  autofocus={true}
-  cache={false}
-  className="input-super-large"
-  clearable={false}
-  filterOption={[Function]}
-  isLoading={false}
-  noResultsText="select2.tooShort.2"
-  onBlurResetsInput={true}
-  onChange={[Function]}
-  onInputChange={[Function]}
-  options={Array []}
-  placeholder="search_verb"
-  searchable={true}
-/>
-`;
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.tsx.snap
new file mode 100644 (file)
index 0000000..0ecd070
--- /dev/null
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render Select 1`] = `
+<Select
+  autofocus={true}
+  className="input-super-large"
+  clearable={false}
+  filterOption={[Function]}
+  isLoading={false}
+  noResultsText="select2.tooShort.2"
+  onBlurResetsInput={true}
+  onChange={[Function]}
+  onInputChange={[Function]}
+  options={Array []}
+  placeholder="search_verb"
+  searchable={true}
+/>
+`;
index e57d0a002f63a4c48b570a94086ef3d18fbc4a0c..9043a2cc22c012606f472328e5fe6863a22c0e0e 100644 (file)
@@ -6,10 +6,8 @@ exports[`should render 1`] = `
 >
   <SearchSelect
     autofocus={false}
-    minimumQueryLength={2}
     onSearch={[MockFunction]}
     onSelect={[MockFunction]}
-    resetOnBlur={true}
   />
 </div>
 `;