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 });
</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;
const input = (
<SearchSelect
+ defaultOptions={this.getDefaultAssignee()}
onSearch={this.handleAssigneeSearch}
onSelect={this.handleAssigneeSelect}
minimumQueryLength={2}
/*::
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 */) => {
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import { 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}
- />
- );
- }
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { 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}
+ />
+ );
+ }
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import React from 'react';
-import { shallow } 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');
-});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import 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');
+});
+++ /dev/null
-// 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}
-/>
-`;
--- /dev/null
+// 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}
+/>
+`;
>
<SearchSelect
autofocus={false}
- minimumQueryLength={2}
onSearch={[MockFunction]}
onSelect={[MockFunction]}
- resetOnBlur={true}
/>
</div>
`;