}
export default class TagFacet extends React.PureComponent<Props> {
- handleSearch = (query: string) =>
- getRuleTags({ organization: this.props.organization, ps: 50, q: query }).then(tags =>
+ handleSearch = (query: string) => {
+ return getRuleTags({ organization: this.props.organization, ps: 50, q: query }).then(tags =>
tags.map(tag => ({ label: tag, value: tag }))
);
+ };
- handleSelect = (tag: string) => this.props.onChange({ tags: uniq([...this.props.values, tag]) });
+ handleSelect = (option: { value: string }) => {
+ this.props.onChange({ tags: uniq([...this.props.values, option.value]) });
+ };
renderName = (tag: string) => (
<>
import SearchSelect from '../../../components/controls/SearchSelect';
import Checkbox from '../../../components/controls/Checkbox';
import Modal from '../../../components/controls/Modal';
-import Select, { Creatable } from '../../../components/controls/Select';
+import Select from '../../../components/controls/Select';
import Tooltip from '../../../components/controls/Tooltip';
import MarkdownTips from '../../../components/common/MarkdownTips';
import SeverityHelper from '../../../components/shared/SeverityHelper';
/*::
type State = {|
+ initialTags: Array<{ label:string, value: string }>,
issues: Array<Issue>,
// used for initial loading of issues
loading: boolean,
paging?: Paging,
// used when submitting a form
submitting: boolean,
- tags?: Array<string>,
// form fields
- addTags?: Array<string>,
- assignee?: string,
+ addTags?: Array<{ label: string, value: string }>,
+ assignee?: { avatar?: string, label: string, value: string },
comment?: string,
notifications?: boolean,
organization?: string,
- removeTags?: Array<string>,
+ removeTags?: Array<{ label: string, value: string }>,
severity?: string,
transition?: string,
type?: string
if (props.organization && !organization) {
organization = props.organization.key;
}
- this.state = { issues: [], loading: true, submitting: false, organization };
+ this.state = { initialTags: [], issues: [], loading: true, submitting: false, organization };
}
componentDidMount() {
Promise.all([
this.loadIssues(),
searchIssueTags({ organization: this.state.organization })
- ]).then(([issues, tags]) => {
- if (this.mounted) {
- this.setState({
- issues: issues.issues,
- loading: false,
- paging: issues.paging,
- tags
- });
- }
- });
+ ]).then(
+ ([issues, tags]) => {
+ if (this.mounted) {
+ this.setState({
+ initialTags: tags.map(tag => ({ label: tag, value: tag })),
+ issues: issues.issues,
+ loading: false,
+ paging: issues.paging
+ });
+ }
+ },
+ () => {}
+ );
}
componentWillUnmount() {
this.props.onClose();
};
- handleAssigneeSearch = (query /*: string */) => searchAssignees(query, this.state.organization);
+ handleAssigneeSearch = (query /*: string */) => {
+ return searchAssignees(query, this.state.organization);
+ };
- handleAssigneeSelect = (assignee /*: string */) => {
+ handleAssigneeSelect = (assignee /*: { avatar?: string, label: string, value: string } */) => {
this.setState({ assignee });
};
+ handleTagsSearch = (query /*: string */) => {
+ return searchIssueTags({ organization: this.state.organization, q: query }).then(tags =>
+ tags.map(tag => ({ label: tag, value: tag }))
+ );
+ };
+
+ handleTagsSelect = (field /*: string */) => (
+ options /*: Array<{ label: string, value: string }> */
+ ) => {
+ this.setState({ [field]: options });
+ };
+
handleFieldCheck = (field /*: string */) => (checked /*: boolean */) => {
if (!checked) {
this.setState({ [field]: undefined });
this.setState({ [field]: value });
};
- handleMultiSelectFieldChange = (field /*: string */) => (
- options /*: Array<{ value: string }> */
- ) => {
- this.setState({ [field]: options.map(option => option.value) });
- };
-
handleSubmit = (e /*: Event */) => {
e.preventDefault();
+ /* eslint-disable camelcase */
const query = pickBy(
{
- assign: this.state.assignee,
- set_type: this.state.type,
- set_severity: this.state.severity,
- add_tags: this.state.addTags && this.state.addTags.join(),
- remove_tags: this.state.removeTags && this.state.removeTags.join(),
- do_transition: this.state.transition,
+ add_tags: this.state.addTags && this.state.addTags.map(t => t.value).join(),
+ assign: this.state.assignee && this.state.assignee.value,
comment: this.state.comment,
- sendNotifications: this.state.notifications
+ do_transition: this.state.transition,
+ remove_tags: this.state.removeTags && this.state.removeTags.map(t => t.value).join(),
+ sendNotifications: this.state.notifications,
+ set_severity: this.state.severity,
+ set_type: this.state.type
},
// remove null, but keep empty string
x => x != null
);
+ /* eslint-enable camelcase */
const issueKeys = this.state.issues.map(issue => issue.key);
this.setState({ submitting: true });
clearable={false}
id="type"
onChange={this.handleSelectFieldChange('type')}
- options={options}
optionRenderer={optionRenderer}
+ options={options}
searchable={false}
value={this.state.type}
valueRenderer={optionRenderer}
clearable={false}
id="severity"
onChange={this.handleSelectFieldChange('severity')}
- options={options}
optionRenderer={option => <SeverityHelper severity={option.value} />}
+ options={options}
searchable={false}
value={this.state.severity}
valueRenderer={option => <SeverityHelper severity={option.value} />}
};
renderTagsField = (field /*: string */, label /*: string */, allowCreate /*: boolean */) => {
+ const { initialTags } = this.state;
const affected /*: number */ = this.state.issues.filter(hasAction('set_tags')).length;
- if (this.state.tags == null || affected === 0) {
+ if (initialTags == null || affected === 0) {
return null;
}
- const Component = allowCreate ? Creatable : Select;
-
- const options = [...this.state.tags, ...(this.state[field] || [])].map(tag => ({
- label: tag,
- value: tag
- }));
-
const input = (
- <Component
- clearable={false}
+ <SearchSelect
+ canCreate={allowCreate}
+ defaultOptions={this.state.initialTags}
id={field}
+ minimumQueryLength={0}
multi={true}
- onChange={this.handleMultiSelectFieldChange(field)}
- options={options}
+ onMultiSelect={this.handleTagsSelect(field)}
+ onSearch={this.handleTagsSearch}
promptTextCreator={promptCreateTag}
- searchable={true}
+ renderOption={this.renderAssigneeOption}
+ resetOnBlur={false}
value={this.state[field]}
/>
);
return searchAssignees(query, organization);
};
- handleSelect = (assignee /*: string */) => {
+ handleSelect = (option /*: { value: string } */) => {
const { assignees } = this.props;
- this.props.onChange({ assigned: true, [this.property]: uniq([...assignees, assignee]) });
+ this.props.onChange({ assigned: true, [this.property]: uniq([...assignees, option.value]) });
};
isAssigneeActive(assignee /*: string */) {
);
};
- handleSelect = (rule /*: string */) => {
+ handleSelect = (option /*: { value: string } */) => {
const { projects } = this.props;
- this.props.onChange({ [this.property]: uniq([...projects, rule]) });
+ this.props.onChange({ [this.property]: uniq([...projects, option.value]) });
};
getStat(project /*: string */) /*: ?number */ {
);
};
- handleSelect = (rule /*: string */) => {
+ handleSelect = (option /*: { value: string } */) => {
const { rules } = this.props;
- this.props.onChange({ [this.property]: uniq([...rules, rule]) });
+ this.props.onChange({ [this.property]: uniq([...rules, option.value]) });
};
getRuleName(rule /*: string */) /*: string */ {
);
};
- handleSelect = (tag /*: string */) => {
+ handleSelect = (option /*: { value: string } */) => {
const { tags } = this.props;
- this.props.onChange({ [this.property]: uniq([...tags, tag]) });
+ this.props.onChange({ [this.property]: uniq([...tags, option.value]) });
};
getStat(tag /*: string */) /*: ?number */ {
const wrapper = renderAssigneeFacet({ assignees: ['foo'], onChange });
const onSelect = wrapper.find('FacetFooter').prop('onSelect');
- onSelect('qux');
+ onSelect({ value: 'qux' });
expect(onChange).lastCalledWith({ assigned: true, assignees: ['foo', 'qux'] });
});
*/
import * as React from 'react';
import { debounce } from 'lodash';
-import Select from '../../components/controls/Select';
+import Select, { Creatable } from '../../components/controls/Select';
import { translate, translateWithParameters } from '../../helpers/l10n';
interface Option {
interface Props {
autofocus?: boolean;
+ canCreate?: boolean;
defaultOptions?: Option[];
minimumQueryLength?: number;
+ multi?: boolean;
onSearch: (query: string) => Promise<Option[]>;
- onSelect: (value: string) => void;
+ onSelect?: (option: Option) => void;
+ onMultiSelect?: (options: Option[]) => void;
+ promptTextCreator?: (label: string) => string;
renderOption?: (option: Object) => JSX.Element;
resetOnBlur?: boolean;
value?: string;
return this.props.resetOnBlur !== undefined ? this.props.resetOnBlur : true;
}
- handleSearch = (query: string) =>
- this.props.onSearch(query).then(
+ handleSearch = (query: string) => {
+ // Ignore the result if the query changed
+ const currentQuery = query;
+ this.props.onSearch(currentQuery).then(
options => {
if (this.mounted) {
- this.setState({ loading: false, options });
+ this.setState(state => ({
+ loading: false,
+ options: state.query === currentQuery ? options : state.options
+ }));
}
},
() => {
}
}
);
+ };
- handleChange = (option: Option) => this.props.onSelect(option.value);
+ handleChange = (option: Option | Option[]) => {
+ if (Array.isArray(option)) {
+ if (this.props.onMultiSelect) {
+ this.props.onMultiSelect(option);
+ }
+ } else if (this.props.onSelect) {
+ this.props.onSelect(option);
+ }
+ };
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 {
+ // `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 :(
const options = (query.length === 0 && this.props.defaultOptions) || [];
this.setState({ options, query });
}
handleFilterOption = () => true;
render() {
+ const Component = this.props.canCreate ? Creatable : Select;
return (
- <Select
+ <Component
autofocus={this.autofocus}
className="input-super-large"
clearable={false}
+ escapeClearsValue={false}
filterOption={this.handleFilterOption}
isLoading={this.state.loading}
+ multi={this.props.multi}
noResultsText={
this.state.query.length < this.minimumQueryLength
? translateWithParameters('select2.tooShort', this.minimumQueryLength)
optionRenderer={this.props.renderOption}
options={this.state.options}
placeholder={translate('search_verb')}
+ promptTextCreator={this.props.promptTextCreator}
searchable={true}
value={this.props.value}
valueRenderer={this.props.renderOption}
// hide the "x" icon when select is empty
const clearable = props.clearable ? Boolean(props.value) : false;
return (
- <ReactSelectAny {...props} clearable={clearable} clearRenderer={renderInput} ref={innerRef} />
+ <ReactSelectAny {...props} clearRenderer={renderInput} clearable={clearable} ref={innerRef} />
);
}
const onSelect = jest.fn();
const wrapper = shallow(<SearchSelect onSearch={jest.fn()} onSelect={onSelect} />);
wrapper.prop('onChange')({ value: 'foo' });
- expect(onSelect).lastCalledWith('foo');
+ expect(onSelect).lastCalledWith({ value: 'foo' });
});
it('should call onSearch', () => {
autofocus={true}
className="input-super-large"
clearable={false}
+ escapeClearsValue={false}
filterOption={[Function]}
isLoading={false}
noResultsText="select2.tooShort.2"
border-bottom-left-radius: 2px;
border-top-left-radius: 2px;
border-right: 1px solid rgba(0, 126, 255, 0.24);
- padding: 1px 5px 3px;
+ padding: 1px 5px;
}
.Select--multi .Select-value-icon:hover,
interface Props {
minimumQueryLength?: number;
onSearch: (query: string) => Promise<Option[]>;
- onSelect: (value: string) => void;
+ onSelect: (option: Option) => void;
renderOption?: (option: Object) => JSX.Element;
}