aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2018-02-26 16:49:40 +0100
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>2018-02-27 14:44:23 +0100
commit2bc87f33684239adbfa746db78ca73290688adaa (patch)
treee4f04535c3733d28f00da796abfe9e6143a59805
parent2947082abde85a0e16ce65da155e1e9d9d7cb78d (diff)
downloadsonarqube-2bc87f33684239adbfa746db78ca73290688adaa.tar.gz
sonarqube-2bc87f33684239adbfa746db78ca73290688adaa.zip
SONAR-10047 Use SearchSelect for tags in Bulk Issue Change
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx9
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js102
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js2
-rw-r--r--server/sonar-web/src/main/js/components/controls/SearchSelect.tsx40
-rw-r--r--server/sonar-web/src/main/js/components/controls/Select.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/SearchSelect-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/components/controls/react-select.css2
-rw-r--r--server/sonar-web/src/main/js/components/facet/FacetFooter.tsx2
13 files changed, 107 insertions, 71 deletions
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx
index 9ca37cb5de4..deb1a36043a 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/TagFacet.tsx
@@ -28,12 +28,15 @@ interface Props extends BasicProps {
}
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) => (
<>
diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js
index b9f7ef9014f..e8f9b22fd0c 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js
+++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js
@@ -23,7 +23,7 @@ import { pickBy, sortBy } from 'lodash';
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';
@@ -49,21 +49,21 @@ type Props = {|
/*::
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
@@ -84,7 +84,7 @@ export default class BulkChangeModal extends React.PureComponent {
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() {
@@ -93,16 +93,19 @@ export default class BulkChangeModal extends React.PureComponent {
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() {
@@ -142,12 +145,26 @@ export default class BulkChangeModal extends React.PureComponent {
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 });
@@ -164,28 +181,24 @@ export default class BulkChangeModal extends React.PureComponent {
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 });
@@ -320,8 +333,8 @@ export default class BulkChangeModal extends React.PureComponent {
clearable={false}
id="type"
onChange={this.handleSelectFieldChange('type')}
- options={options}
optionRenderer={optionRenderer}
+ options={options}
searchable={false}
value={this.state.type}
valueRenderer={optionRenderer}
@@ -349,8 +362,8 @@ export default class BulkChangeModal extends React.PureComponent {
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} />}
@@ -361,28 +374,25 @@ export default class BulkChangeModal extends React.PureComponent {
};
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]}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js
index a2d1685f607..3034677596f 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js
@@ -84,9 +84,9 @@ export default class AssigneeFacet extends React.PureComponent {
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 */) {
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js
index fba2560cc83..3083a35b69b 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js
@@ -96,9 +96,9 @@ export default class ProjectFacet extends React.PureComponent {
);
};
- 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 */ {
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js
index 18704e1f4ff..b30780fa363 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js
@@ -80,9 +80,9 @@ export default class RuleFacet extends React.PureComponent {
);
};
- 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 */ {
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js
index 51df80106cc..8c62b5d4801 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js
@@ -78,9 +78,9 @@ export default class TagFacet extends React.PureComponent {
);
};
- 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 */ {
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js
index a2f956dc4a6..815472708ac 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js
@@ -93,6 +93,6 @@ it('should handle footer callbacks', () => {
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'] });
});
diff --git a/server/sonar-web/src/main/js/components/controls/SearchSelect.tsx b/server/sonar-web/src/main/js/components/controls/SearchSelect.tsx
index 22b1630f1a0..1712ac8c4f0 100644
--- a/server/sonar-web/src/main/js/components/controls/SearchSelect.tsx
+++ b/server/sonar-web/src/main/js/components/controls/SearchSelect.tsx
@@ -19,7 +19,7 @@
*/
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 {
@@ -29,10 +29,14 @@ 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;
@@ -73,11 +77,16 @@ export default class SearchSelect extends React.PureComponent<Props, State> {
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
+ }));
}
},
() => {
@@ -86,16 +95,25 @@ export default class SearchSelect extends React.PureComponent<Props, State> {
}
}
);
+ };
- 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 });
}
@@ -105,13 +123,16 @@ export default class SearchSelect extends React.PureComponent<Props, State> {
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)
@@ -123,6 +144,7 @@ export default class SearchSelect extends React.PureComponent<Props, State> {
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}
diff --git a/server/sonar-web/src/main/js/components/controls/Select.tsx b/server/sonar-web/src/main/js/components/controls/Select.tsx
index f8886578b8a..04e49151b88 100644
--- a/server/sonar-web/src/main/js/components/controls/Select.tsx
+++ b/server/sonar-web/src/main/js/components/controls/Select.tsx
@@ -49,7 +49,7 @@ export default function Select({ innerRef, ...props }: WithInnerRef & ReactSelec
// 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} />
);
}
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
index 34d964549b4..292d904c6e1 100644
--- 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
@@ -35,7 +35,7 @@ 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');
+ expect(onSelect).lastCalledWith({ value: 'foo' });
});
it('should call onSearch', () => {
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
index 0ecd070f31d..0e61c2bcc6e 100644
--- 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
@@ -5,6 +5,7 @@ exports[`should render Select 1`] = `
autofocus={true}
className="input-super-large"
clearable={false}
+ escapeClearsValue={false}
filterOption={[Function]}
isLoading={false}
noResultsText="select2.tooShort.2"
diff --git a/server/sonar-web/src/main/js/components/controls/react-select.css b/server/sonar-web/src/main/js/components/controls/react-select.css
index bc84ab6c512..a8bc7eac879 100644
--- a/server/sonar-web/src/main/js/components/controls/react-select.css
+++ b/server/sonar-web/src/main/js/components/controls/react-select.css
@@ -417,7 +417,7 @@
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,
diff --git a/server/sonar-web/src/main/js/components/facet/FacetFooter.tsx b/server/sonar-web/src/main/js/components/facet/FacetFooter.tsx
index 2f34ccf9fe0..8879f11264f 100644
--- a/server/sonar-web/src/main/js/components/facet/FacetFooter.tsx
+++ b/server/sonar-web/src/main/js/components/facet/FacetFooter.tsx
@@ -25,7 +25,7 @@ type Option = { label: string; value: string };
interface Props {
minimumQueryLength?: number;
onSearch: (query: string) => Promise<Option[]>;
- onSelect: (value: string) => void;
+ onSelect: (option: Option) => void;
renderOption?: (option: Object) => JSX.Element;
}