aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2022-04-01 15:48:02 +0200
committersonartech <sonartech@sonarsource.com>2022-04-05 20:03:16 +0000
commit58533b7fcf8a2089df72928888fe3102b782fa77 (patch)
tree95e3af283956331a48a30594c6dd3b83a378363f /server/sonar-web/src
parentca68dabbefbad5122b57d57174130b33b2e93d22 (diff)
downloadsonarqube-58533b7fcf8a2089df72928888fe3102b782fa77.tar.gz
sonarqube-58533b7fcf8a2089df72928888fe3102b782fa77.zip
SONAR-16221 Add select variants (creatable and async) and replace SelectLegacy
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts3
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx16
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap28
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/AssigneeSelect.tsx139
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx143
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/AssigneeSelect-test.tsx149
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx63
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/AssigneeSelect-test.tsx.snap84
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap20
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx69
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx51
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BaselineSettingReferenceBranch-test.tsx.snap151
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap9
-rw-r--r--server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/components/controls/SearchSelect.tsx154
-rw-r--r--server/sonar-web/src/main/js/components/controls/Select.tsx118
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/SearchSelect-test.tsx50
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/Select-test.tsx68
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.tsx.snap17
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Select-test.tsx.snap151
-rw-r--r--server/sonar-web/src/main/js/helpers/mocks/react-select.ts2
24 files changed, 932 insertions, 563 deletions
diff --git a/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts b/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts
index ab9847c117b..429cb1ae374 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts
+++ b/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts
@@ -41,8 +41,7 @@ import Radio from '../../../components/controls/Radio';
import RadioToggle from '../../../components/controls/RadioToggle';
import ReloadButton from '../../../components/controls/ReloadButton';
import SearchBox from '../../../components/controls/SearchBox';
-import SearchSelect from '../../../components/controls/SearchSelect';
-import Select from '../../../components/controls/Select';
+import Select, { SearchSelect } from '../../../components/controls/Select';
import SelectLegacy from '../../../components/controls/SelectLegacy';
import SelectList, { SelectListFilter } from '../../../components/controls/SelectList';
import SimpleModal from '../../../components/controls/SimpleModal';
diff --git a/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx b/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx
index 7b26bf5d4b4..ffde9cf777e 100644
--- a/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/GitHubProjectCreateRenderer.tsx
@@ -25,7 +25,7 @@ import { Button } from '../../../components/controls/buttons';
import ListFooter from '../../../components/controls/ListFooter';
import Radio from '../../../components/controls/Radio';
import SearchBox from '../../../components/controls/SearchBox';
-import SearchSelect from '../../../components/controls/SearchSelect';
+import Select, { BasicSelectOption } from '../../../components/controls/Select';
import CheckIcon from '../../../components/icons/CheckIcon';
import QualifierIcon from '../../../components/icons/QualifierIcon';
import { Alert } from '../../../components/ui/Alert';
@@ -62,9 +62,6 @@ function orgToOption({ key, name }: GithubOrganization) {
return { value: key, label: name };
}
-const handleSearch = (organizations: GithubOrganization[]) => (q: string) =>
- Promise.resolve(organizations.filter(o => !q || o.name.includes(q)).map(orgToOption));
-
function renderRepositoryList(props: GitHubProjectCreateRendererProps) {
const {
importing,
@@ -240,12 +237,11 @@ export default function GitHubProjectCreateRenderer(props: GitHubProjectCreateRe
<div className="form-field">
<label>{translate('onboarding.create_project.github.choose_organization')}</label>
{organizations.length > 0 ? (
- <SearchSelect
- defaultOptions={organizations.map(orgToOption)}
- onSearch={handleSearch(organizations)}
- minimumQueryLength={0}
- onSelect={({ value }) => props.onSelectOrganization(value)}
- value={selectedOrganization && orgToOption(selectedOrganization)}
+ <Select
+ className="input-super-large"
+ options={organizations.map(orgToOption)}
+ onChange={({ value }: BasicSelectOption) => props.onSelectOrganization(value)}
+ value={selectedOrganization ? orgToOption(selectedOrganization) : null}
/>
) : (
!loadingOrganizations && (
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx
index 2f37b51928b..380170f9a01 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/GitHubProjectCreateRenderer-test.tsx
@@ -21,7 +21,7 @@ import { shallow } from 'enzyme';
import * as React from 'react';
import Radio from '../../../../components/controls/Radio';
import SearchBox from '../../../../components/controls/SearchBox';
-import SearchSelect from '../../../../components/controls/SearchSelect';
+import Select from '../../../../components/controls/Select';
import { mockGitHubRepository } from '../../../../helpers/mocks/alm-integrations';
import { GithubOrganization } from '../../../../types/alm-integration';
import GitHubProjectCreateRenderer, {
@@ -85,7 +85,7 @@ describe('callback', () => {
it('should be called when org is selected', () => {
const value = 'o1';
- wrapper.find(SearchSelect).simulate('select', { value });
+ wrapper.find(Select).simulate('change', { value });
expect(onSelectOrganization).toBeCalledWith(value);
});
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap
index ea876a7498e..56300a11343 100644
--- a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/GitHubProjectCreateRenderer-test.tsx.snap
@@ -172,8 +172,10 @@ exports[`should render correctly: no repositories 1`] = `
<label>
onboarding.create_project.github.choose_organization
</label>
- <SearchSelect
- defaultOptions={
+ <Select
+ className="input-super-large"
+ onChange={[Function]}
+ options={
Array [
Object {
"label": "org1",
@@ -185,9 +187,6 @@ exports[`should render correctly: no repositories 1`] = `
},
]
}
- minimumQueryLength={0}
- onSearch={[Function]}
- onSelect={[Function]}
value={
Object {
"label": "org2",
@@ -226,8 +225,10 @@ exports[`should render correctly: organizations 1`] = `
<label>
onboarding.create_project.github.choose_organization
</label>
- <SearchSelect
- defaultOptions={
+ <Select
+ className="input-super-large"
+ onChange={[Function]}
+ options={
Array [
Object {
"label": "org1",
@@ -239,9 +240,7 @@ exports[`should render correctly: organizations 1`] = `
},
]
}
- minimumQueryLength={0}
- onSearch={[Function]}
- onSelect={[Function]}
+ value={null}
/>
</div>
</DeferredSpinner>
@@ -291,8 +290,10 @@ exports[`should render correctly: repositories 1`] = `
<label>
onboarding.create_project.github.choose_organization
</label>
- <SearchSelect
- defaultOptions={
+ <Select
+ className="input-super-large"
+ onChange={[Function]}
+ options={
Array [
Object {
"label": "org1",
@@ -304,9 +305,6 @@ exports[`should render correctly: repositories 1`] = `
},
]
}
- minimumQueryLength={0}
- onSearch={[Function]}
- onSelect={[Function]}
value={
Object {
"label": "org2",
diff --git a/server/sonar-web/src/main/js/apps/issues/components/AssigneeSelect.tsx b/server/sonar-web/src/main/js/apps/issues/components/AssigneeSelect.tsx
new file mode 100644
index 00000000000..987c4cb2039
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/AssigneeSelect.tsx
@@ -0,0 +1,139 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { debounce } from 'lodash';
+import * as React from 'react';
+import { components, OptionProps, SingleValueProps } from 'react-select';
+import { BasicSelectOption, SearchSelect } from '../../../components/controls/Select';
+import Avatar from '../../../components/ui/Avatar';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { Issue } from '../../../types/types';
+import { CurrentUser, isLoggedIn, isUserActive } from '../../../types/users';
+import { searchAssignees } from '../utils';
+
+const DEBOUNCE_DELAY = 250;
+// exported for test
+export const MIN_QUERY_LENGTH = 2;
+
+export interface AssigneeOption extends BasicSelectOption {
+ avatar?: string;
+ email?: string;
+ label: string;
+ value: string;
+}
+
+export interface AssigneeSelectProps {
+ currentUser: CurrentUser;
+ issues: Issue[];
+ onAssigneeSelect: (assignee: AssigneeOption) => void;
+}
+
+export default class AssigneeSelect extends React.Component<AssigneeSelectProps> {
+ constructor(props: AssigneeSelectProps) {
+ super(props);
+
+ this.handleAssigneeSearch = debounce(this.handleAssigneeSearch, DEBOUNCE_DELAY);
+ }
+
+ getDefaultAssignee = () => {
+ const { currentUser, issues } = this.props;
+ const options = [];
+
+ if (isLoggedIn(currentUser)) {
+ 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;
+ };
+
+ handleAssigneeSearch = (query: string, resolve: (options: AssigneeOption[]) => void) => {
+ if (query.length < MIN_QUERY_LENGTH) {
+ resolve([]);
+ return;
+ }
+
+ searchAssignees(query)
+ .then(({ results }) =>
+ results.map(r => {
+ const userInfo = r.name || r.login;
+
+ return {
+ avatar: r.avatar,
+ label: isUserActive(r) ? userInfo : translateWithParameters('user.x_deleted', userInfo),
+ value: r.login
+ };
+ })
+ )
+ .then(resolve)
+ .catch(() => resolve([]));
+ };
+
+ renderAssignee = (option: AssigneeOption) => {
+ return (
+ <div className="display-flex-center">
+ {option.avatar !== undefined && (
+ <Avatar className="spacer-right" hash={option.avatar} name={option.label} size={16} />
+ )}
+ {option.label}
+ </div>
+ );
+ };
+
+ renderAssigneeOption = (props: OptionProps<AssigneeOption, false>) => (
+ <components.Option {...props}>{this.renderAssignee(props.data)}</components.Option>
+ );
+
+ renderSingleAssignee = (props: SingleValueProps<AssigneeOption>) => (
+ <components.SingleValue {...props}>{this.renderAssignee(props.data)}</components.SingleValue>
+ );
+
+ render() {
+ return (
+ <SearchSelect
+ className="input-super-large"
+ components={{
+ Option: this.renderAssigneeOption,
+ SingleValue: this.renderSingleAssignee
+ }}
+ isClearable={true}
+ defaultOptions={this.getDefaultAssignee()}
+ loadOptions={this.handleAssigneeSearch}
+ onChange={this.props.onAssigneeSelect}
+ noOptionsMessage={({ inputValue }) =>
+ inputValue.length < MIN_QUERY_LENGTH
+ ? translateWithParameters('select2.tooShort', MIN_QUERY_LENGTH)
+ : translate('select2.noMatches')
+ }
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
index 7754785e2af..78e5b564bee 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
@@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { pickBy, sortBy } from 'lodash';
+import { debounce, pickBy, sortBy } from 'lodash';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { components, OptionProps, SingleValueProps } from 'react-select';
@@ -28,27 +28,24 @@ import Checkbox from '../../../components/controls/Checkbox';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import Modal from '../../../components/controls/Modal';
import Radio from '../../../components/controls/Radio';
-import SearchSelect from '../../../components/controls/SearchSelect';
-import Select, { BasicSelectOption } from '../../../components/controls/Select';
+import Select, {
+ BasicSelectOption,
+ CreatableSelect,
+ SearchSelect
+} from '../../../components/controls/Select';
import Tooltip from '../../../components/controls/Tooltip';
import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import { Alert } from '../../../components/ui/Alert';
-import Avatar from '../../../components/ui/Avatar';
import { throwGlobalError } from '../../../helpers/error';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Component, Dict, Issue, IssueType, Paging } from '../../../types/types';
-import { CurrentUser, isLoggedIn, isUserActive } from '../../../types/users';
-import { searchAssignees } from '../utils';
+import { CurrentUser } from '../../../types/users';
+import AssigneeSelect, { AssigneeOption } from './AssigneeSelect';
-interface AssigneeOption {
- avatar?: string;
- email?: string;
- label: string;
- value: string;
-}
+const DEBOUNCE_DELAY = 250;
-interface TagOption {
+interface TagOption extends BasicSelectOption {
label: string;
value: string;
}
@@ -82,12 +79,6 @@ interface State extends FormFields {
submitting: boolean;
}
-type AssigneeSelectType = new () => SearchSelect<AssigneeOption>;
-const AssigneeSelect = SearchSelect as AssigneeSelectType;
-
-type TagSelectType = new () => SearchSelect<TagOption>;
-const TagSelect = SearchSelect as TagSelectType;
-
export const MAX_PAGE_SIZE = 500;
export default class BulkChangeModal extends React.PureComponent<Props, State> {
@@ -96,6 +87,8 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = { initialTags: [], issues: [], loading: true, submitting: false };
+
+ this.handleTagsSearch = debounce(this.handleTagsSearch, DEBOUNCE_DELAY);
}
componentDidMount() {
@@ -128,58 +121,18 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
return this.props.fetchIssues({ additionalFields: 'actions,transitions', ps: MAX_PAGE_SIZE });
};
- getDefaultAssignee = () => {
- const { currentUser } = this.props;
- const { issues } = this.state;
- const options = [];
-
- if (isLoggedIn(currentUser)) {
- 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;
- };
-
- handleAssigneeSearch = (query: string) => {
- return searchAssignees(query).then(({ results }) =>
- results.map(r => {
- const userInfo = r.name || r.login;
-
- return {
- avatar: r.avatar,
- label: isUserActive(r) ? userInfo : translateWithParameters('user.x_deleted', userInfo),
- value: r.login
- };
- })
- );
- };
-
handleAssigneeSelect = (assignee: AssigneeOption) => {
this.setState({ assignee });
};
- handleTagsSearch = (query: string) => {
- return searchIssueTags({ q: query }).then(tags =>
- tags.map(tag => ({ label: tag, value: tag }))
- );
+ handleTagsSearch = (query: string, resolve: (option: TagOption[]) => void) => {
+ searchIssueTags({ q: query })
+ .then(tags => tags.map(tag => ({ label: tag, value: tag })))
+ .then(resolve)
+ .catch(() => resolve([]));
};
- handleTagsSelect = (field: 'addTags' | 'removeTags') => (
- options: Array<{ label: string; value: string }>
- ) => {
+ handleTagsSelect = (field: 'addTags' | 'removeTags') => (options: TagOption[]) => {
this.setState<keyof FormFields>({ [field]: options });
};
@@ -306,18 +259,9 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
</div>
);
- renderAssigneeOption = (option: AssigneeOption) => {
- return (
- <span>
- {option.avatar !== undefined && (
- <Avatar className="spacer-right" hash={option.avatar} name={option.label} size={16} />
- )}
- {option.label}
- </span>
- );
- };
-
renderAssigneeField = () => {
+ const { currentUser } = this.props;
+ const { issues } = this.state;
const affected = this.state.issues.filter(hasAction('assign')).length;
if (affected === 0) {
@@ -326,14 +270,9 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
const input = (
<AssigneeSelect
- className="input-super-large"
- clearable={true}
- defaultOptions={this.getDefaultAssignee()}
- onSearch={this.handleAssigneeSearch}
- onSelect={this.handleAssigneeSelect}
- renderOption={this.renderAssigneeOption}
- resetOnBlur={false}
- value={this.state.assignee}
+ currentUser={currentUser}
+ issues={issues}
+ onAssigneeSelect={this.handleAssigneeSelect}
/>
);
@@ -419,10 +358,6 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
return this.renderField('severity', 'issue.set_severity', affected, input);
};
- renderTagOption = (option: TagOption) => {
- return <span>{option.label}</span>;
- };
-
renderTagsField = (field: 'addTags' | 'removeTags', label: string, allowCreate: boolean) => {
const { initialTags } = this.state;
const affected = this.state.issues.filter(hasAction('set_tags')).length;
@@ -431,21 +366,19 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
return null;
}
- const input = (
- <TagSelect
- canCreate={allowCreate}
- className="input-super-large"
- clearable={true}
- defaultOptions={this.state.initialTags}
- minimumQueryLength={0}
- multi={true}
- onMultiSelect={this.handleTagsSelect(field)}
- onSearch={this.handleTagsSearch}
- promptTextCreator={promptCreateTag}
- renderOption={this.renderTagOption}
- resetOnBlur={false}
- value={this.state[field]}
- />
+ const props = {
+ className: 'input-super-large',
+ isClearable: true,
+ defaultOptions: this.state.initialTags,
+ isMulti: true,
+ onChange: this.handleTagsSelect(field),
+ loadOptions: this.handleTagsSearch
+ };
+
+ const input = allowCreate ? (
+ <CreatableSelect {...props} formatCreateLabel={createTagPrompt} />
+ ) : (
+ <SearchSelect {...props} />
);
return this.renderField(field, label, affected, input);
@@ -580,6 +513,6 @@ function hasAction(action: string) {
return (issue: Issue) => issue.actions && issue.actions.includes(action);
}
-function promptCreateTag(label: string) {
- return `+ ${label}`;
+function createTagPrompt(label: string) {
+ return translateWithParameters('issue.create_tag_x', label);
}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/AssigneeSelect-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/AssigneeSelect-test.tsx
new file mode 100644
index 00000000000..211010e483e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/AssigneeSelect-test.tsx
@@ -0,0 +1,149 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 * as React from 'react';
+import { Props as ReactSelectAsyncProps } from 'react-select/async';
+import { SearchSelect } from '../../../../components/controls/Select';
+import Avatar from '../../../../components/ui/Avatar';
+import { mockCurrentUser, mockIssue, mockLoggedInUser } from '../../../../helpers/testMocks';
+import { searchAssignees } from '../../utils';
+import AssigneeSelect, {
+ AssigneeOption,
+ AssigneeSelectProps,
+ MIN_QUERY_LENGTH
+} from '../AssigneeSelect';
+
+jest.mock('../../utils', () => ({
+ searchAssignees: jest.fn().mockResolvedValue({
+ results: [
+ {
+ active: true,
+ avatar: '##avatar1',
+ login: 'toto@toto',
+ name: 'toto'
+ },
+ {
+ active: false,
+ avatar: '##avatar2',
+ login: 'tata@tata',
+ name: 'tata'
+ },
+ {
+ active: true,
+ avatar: '##avatar3',
+ login: 'titi@titi'
+ }
+ ]
+ })
+}));
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot('default');
+ expect(shallowRender({ currentUser: mockLoggedInUser(), issues: [mockIssue()] })).toMatchSnapshot(
+ 'logged in & assignable issues'
+ );
+ expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot(
+ 'logged in & no assignable issues'
+ );
+ expect(shallowRender({ issues: [mockIssue(false, { assignee: 'someone' })] })).toMatchSnapshot(
+ 'unassignable issues'
+ );
+});
+
+it('should render options correctly', () => {
+ const wrapper = shallowRender();
+
+ expect(
+ shallow(
+ wrapper.instance().renderAssignee({
+ avatar: '##avatar1',
+ value: 'toto@toto',
+ label: 'toto'
+ })
+ )
+ .find(Avatar)
+ .exists()
+ ).toBe(true);
+
+ expect(
+ shallow(
+ wrapper.instance().renderAssignee({
+ value: 'toto@toto',
+ label: 'toto'
+ })
+ )
+ .find(Avatar)
+ .exists()
+ ).toBe(false);
+});
+
+it('should render noOptionsMessage correctly', () => {
+ const wrapper = shallowRender();
+ expect(
+ wrapper.find<ReactSelectAsyncProps<AssigneeOption, false>>(SearchSelect).props()
+ .noOptionsMessage!({ inputValue: 'a' })
+ ).toBe(`select2.tooShort.${MIN_QUERY_LENGTH}`);
+
+ expect(
+ wrapper.find<ReactSelectAsyncProps<AssigneeOption, false>>(SearchSelect).props()
+ .noOptionsMessage!({ inputValue: 'droids' })
+ ).toBe('select2.noMatches');
+});
+
+it('should handle assignee search', async () => {
+ const onAssigneeSelect = jest.fn();
+ const wrapper = shallowRender({ onAssigneeSelect });
+
+ wrapper.instance().handleAssigneeSearch('a', jest.fn());
+ expect(searchAssignees).not.toBeCalled();
+
+ const result = await new Promise((resolve: (opts: AssigneeOption[]) => void) => {
+ wrapper.instance().handleAssigneeSearch('someone', resolve);
+ });
+
+ expect(result).toEqual([
+ {
+ avatar: '##avatar1',
+ value: 'toto@toto',
+ label: 'toto'
+ },
+ {
+ avatar: '##avatar2',
+ value: 'tata@tata',
+ label: 'user.x_deleted.tata'
+ },
+ {
+ avatar: '##avatar3',
+ value: 'titi@titi',
+ label: 'user.x_deleted.titi@titi'
+ }
+ ]);
+});
+
+function shallowRender(overrides: Partial<AssigneeSelectProps> = {}) {
+ return shallow<AssigneeSelect>(
+ <AssigneeSelect
+ currentUser={mockCurrentUser()}
+ issues={[]}
+ onAssigneeSelect={jest.fn()}
+ {...overrides}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx
index 6e91c7258cb..19c592dbb47 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx
@@ -19,10 +19,11 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { Props as ReactSelectProps } from 'react-select';
import { SelectComponentsProps } from 'react-select/src/Select';
import { searchIssueTags } from '../../../../api/issues';
import { SubmitButton } from '../../../../components/controls/buttons';
-import Select from '../../../../components/controls/Select';
+import Select, { CreatableSelect, SearchSelect } from '../../../../components/controls/Select';
import { mockIssue } from '../../../../helpers/testMocks';
import { change, waitAndUpdate } from '../../../../helpers/testUtils';
import { Issue } from '../../../../types/types';
@@ -32,30 +33,6 @@ jest.mock('../../../../api/issues', () => ({
searchIssueTags: jest.fn().mockResolvedValue([undefined, []])
}));
-jest.mock('../../utils', () => ({
- searchAssignees: jest.fn().mockResolvedValue({
- results: [
- {
- active: true,
- avatar: '##toto',
- login: 'toto@toto',
- name: 'toto'
- },
- {
- active: false,
- avatar: '##toto',
- login: 'login@login',
- name: 'toto'
- },
- {
- active: true,
- avatar: '##toto',
- login: 'login@login'
- }
- ]
- })
-}));
-
it('should display error message when no issues available', async () => {
const wrapper = getWrapper([]);
await waitAndUpdate(wrapper);
@@ -80,20 +57,11 @@ it('should display warning when too many issues are passed', async () => {
expect(wrapper.find('Alert')).toMatchSnapshot();
});
-it('should properly handle the search for assignee', async () => {
- const issues: Issue[] = [];
- for (let i = MAX_PAGE_SIZE + 1; i > 0; i--) {
- issues.push(mockIssue());
- }
-
- const wrapper = getWrapper(issues);
- const result = await wrapper.instance().handleAssigneeSearch('toto');
- expect(result).toMatchSnapshot();
-});
-
it('should properly handle the search for tags', async () => {
const wrapper = getWrapper([]);
- await wrapper.instance().handleTagsSearch('query');
+ await new Promise(resolve => {
+ wrapper.instance().handleTagsSearch('query', resolve);
+ });
expect(searchIssueTags).toBeCalled();
});
@@ -110,27 +78,30 @@ it.each([
expect(SingleValue({ data: { label: 'label', value: 'value' } })).toMatchSnapshot('SingleValue');
});
+it('should render tags correctly', async () => {
+ const wrapper = getWrapper([mockIssue(false, { actions: ['set_tags'] })]);
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.find(CreatableSelect).exists()).toBe(true);
+ expect(wrapper.find(SearchSelect).exists()).toBe(true);
+});
+
it('should disable the submit button unless some change is configured', async () => {
const wrapper = getWrapper([mockIssue(false, { actions: ['set_severity', 'comment'] })]);
await waitAndUpdate(wrapper);
- return new Promise<void>((resolve, reject) => {
+ return new Promise<void>(resolve => {
expect(wrapper.find(SubmitButton).props().disabled).toBe(true);
// Setting a comment is not sufficient; some other change must occur.
change(wrapper.find('#comment'), 'Some comment');
expect(wrapper.find(SubmitButton).props().disabled).toBe(true);
- const { onChange } = wrapper
- .find(Select)
+ wrapper
+ .find<ReactSelectProps>(Select)
.at(0)
- .props();
- if (!onChange) {
- reject();
- return;
- }
+ .simulate('change', { value: 'foo' });
- onChange({ value: 'foo' });
expect(wrapper.find(SubmitButton).props().disabled).toBe(false);
resolve();
});
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/AssigneeSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/AssigneeSelect-test.tsx.snap
new file mode 100644
index 00000000000..9d4eaf6ca61
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/AssigneeSelect-test.tsx.snap
@@ -0,0 +1,84 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+<SearchSelect
+ className="input-super-large"
+ components={
+ Object {
+ "Option": [Function],
+ "SingleValue": [Function],
+ }
+ }
+ defaultOptions={Array []}
+ isClearable={true}
+ loadOptions={[Function]}
+ noOptionsMessage={[Function]}
+ onChange={[MockFunction]}
+/>
+`;
+
+exports[`should render correctly: logged in & assignable issues 1`] = `
+<SearchSelect
+ className="input-super-large"
+ components={
+ Object {
+ "Option": [Function],
+ "SingleValue": [Function],
+ }
+ }
+ defaultOptions={
+ Array [
+ Object {
+ "avatar": undefined,
+ "label": "Skywalker",
+ "value": "luke",
+ },
+ ]
+ }
+ isClearable={true}
+ loadOptions={[Function]}
+ noOptionsMessage={[Function]}
+ onChange={[MockFunction]}
+/>
+`;
+
+exports[`should render correctly: logged in & no assignable issues 1`] = `
+<SearchSelect
+ className="input-super-large"
+ components={
+ Object {
+ "Option": [Function],
+ "SingleValue": [Function],
+ }
+ }
+ defaultOptions={Array []}
+ isClearable={true}
+ loadOptions={[Function]}
+ noOptionsMessage={[Function]}
+ onChange={[MockFunction]}
+/>
+`;
+
+exports[`should render correctly: unassignable issues 1`] = `
+<SearchSelect
+ className="input-super-large"
+ components={
+ Object {
+ "Option": [Function],
+ "SingleValue": [Function],
+ }
+ }
+ defaultOptions={
+ Array [
+ Object {
+ "label": "unassigned",
+ "value": "",
+ },
+ ]
+ }
+ isClearable={true}
+ loadOptions={[Function]}
+ noOptionsMessage={[Function]}
+ onChange={[MockFunction]}
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap
index d79ff90dddb..444ba88d326 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap
@@ -131,26 +131,6 @@ exports[`should display warning when too many issues are passed 2`] = `
</Alert>
`;
-exports[`should properly handle the search for assignee 1`] = `
-Array [
- Object {
- "avatar": "##toto",
- "label": "toto",
- "value": "toto@toto",
- },
- Object {
- "avatar": "##toto",
- "label": "user.x_deleted.toto",
- "value": "login@login",
- },
- Object {
- "avatar": "##toto",
- "label": "user.x_deleted.login@login",
- "value": "login@login",
- },
-]
-`;
-
exports[`should render select for severity: Option 1`] = `
<Option
data={
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx
index cfe46334afe..12e94c24f5e 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx
@@ -18,8 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { components, OptionProps } from 'react-select';
import RadioCard from '../../../components/controls/RadioCard';
-import SearchSelect from '../../../components/controls/SearchSelect';
+import Select from '../../../components/controls/Select';
import Tooltip from '../../../components/controls/Tooltip';
import AlertErrorIcon from '../../../components/icons/AlertErrorIcon';
import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
@@ -40,32 +41,45 @@ export interface BaselineSettingReferenceBranchProps {
}
export interface BranchOption {
- disabled?: boolean;
+ isDisabled?: boolean;
isInvalid?: boolean;
isMain: boolean;
+ label: string;
value: string;
}
-function renderBranchOption(option: BranchOption) {
- return option.isInvalid ? (
- <Tooltip
- overlay={translateWithParameters('baseline.reference_branch.does_not_exist', option.value)}>
- <span>
- {option.value} <AlertErrorIcon />
- </span>
- </Tooltip>
- ) : (
- <>
- <span
- title={
- option.disabled ? translate('baseline.reference_branch.cannot_be_itself') : undefined
- }>
- {option.value}
- </span>
- {option.isMain && (
- <div className="badge spacer-left">{translate('branches.main_branch')}</div>
+/* Export for testing */
+export function renderBranchOption(props: OptionProps<BranchOption, false>) {
+ const { data: option } = props;
+
+ return (
+ <components.Option {...props}>
+ {option.isInvalid ? (
+ <Tooltip
+ overlay={translateWithParameters(
+ 'baseline.reference_branch.does_not_exist',
+ option.value
+ )}>
+ <span>
+ {option.value} <AlertErrorIcon />
+ </span>
+ </Tooltip>
+ ) : (
+ <>
+ <span
+ title={
+ option.isDisabled
+ ? translate('baseline.reference_branch.cannot_be_itself')
+ : undefined
+ }>
+ {option.value}
+ </span>
+ {option.isMain && (
+ <div className="badge spacer-left">{translate('branches.main_branch')}</div>
+ )}
+ </>
)}
- </>
+ </components.Option>
);
}
@@ -73,6 +87,7 @@ export default function BaselineSettingReferenceBranch(props: BaselineSettingRef
const { branchList, className, disabled, referenceBranch, selected, settingLevel } = props;
const currentBranch = branchList.find(b => b.value === referenceBranch) || {
+ label: referenceBranch,
value: referenceBranch,
isMain: false,
isInvalid: true
@@ -98,15 +113,15 @@ export default function BaselineSettingReferenceBranch(props: BaselineSettingRef
<strong>{translate('baseline.reference_branch.choose')}</strong>
<MandatoryFieldMarker />
</label>
- <SearchSelect<BranchOption>
+ <Select<BranchOption>
autofocus={false}
className="little-spacer-top spacer-bottom"
- defaultOptions={branchList}
- minimumQueryLength={1}
- onSearch={q => Promise.resolve(branchList.filter(b => b.value.includes(q)))}
- onSelect={option => props.onChangeReferenceBranch(option.value)}
- renderOption={renderBranchOption}
+ options={branchList}
+ onChange={(option: BranchOption) => props.onChangeReferenceBranch(option.value)}
value={currentBranch}
+ components={{
+ Option: renderBranchOption
+ }}
/>
</div>
</>
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx
index 7b2d84aa6e6..b3b074509f4 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx
@@ -82,9 +82,10 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop
}
branchToOption = (b: Branch) => ({
+ label: b.name,
value: b.name,
isMain: b.isMain,
- disabled: b.name === this.props.branch.name // cannot itself be used as a reference branch
+ isDisabled: b.name === this.props.branch.name // cannot itself be used as a reference branch
});
handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
index 0fec9735c38..190f93fcb13 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
@@ -78,7 +78,7 @@ function renderGeneralSetting(generalSetting: NewCodePeriod) {
}
function branchToOption(b: Branch) {
- return { value: b.name, isMain: b.isMain };
+ return { label: b.name, value: b.name, isMain: b.isMain };
}
export default function ProjectBaselineSelector(props: ProjectBaselineSelectorProps) {
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx
index 8d73da812a5..7ac3867884a 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx
@@ -19,11 +19,13 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { OptionProps, Props as ReactSelectProps } from 'react-select';
import RadioCard from '../../../../components/controls/RadioCard';
-import SearchSelect from '../../../../components/controls/SearchSelect';
+import Select from '../../../../components/controls/Select';
import BaselineSettingReferenceBranch, {
BaselineSettingReferenceBranchProps,
- BranchOption
+ BranchOption,
+ renderBranchOption
} from '../BaselineSettingReferenceBranch';
it('should render correctly', () => {
@@ -33,7 +35,7 @@ it('should render correctly', () => {
);
expect(
shallowRender({
- branchList: [{ value: 'master', isMain: true }],
+ branchList: [{ label: 'master', value: 'master', isMain: true }],
settingLevel: 'branch',
configuredBranchName: 'master'
})
@@ -61,9 +63,9 @@ it('should callback when changing selection', () => {
const wrapper = shallowRender({ onChangeReferenceBranch });
wrapper
- .find(SearchSelect)
+ .find(Select)
.first()
- .simulate('select', { value: 'branch-6.9' });
+ .simulate('change', { value: 'branch-6.9' });
expect(onChangeReferenceBranch).toHaveBeenCalledWith('branch-6.9');
});
@@ -73,34 +75,45 @@ it('should handle an invalid branch', () => {
expect(
wrapper
- .find(SearchSelect)
+ .find<ReactSelectProps>(Select)
.first()
.props().value
- ).toEqual({ value: unknownBranchName, isMain: false, isInvalid: true });
+ ).toEqual({ label: unknownBranchName, value: unknownBranchName, isMain: false, isInvalid: true });
});
describe('renderOption', () => {
- const select = shallowRender()
- .find(SearchSelect)
- .first();
- const renderFunction = select.props().renderOption as (option: BranchOption) => JSX.Element;
+ // fake props injected by the Select itself
+ const props = {} as OptionProps<BranchOption, false>;
it('should render correctly', () => {
- expect(renderFunction({ value: 'master', isMain: true })).toMatchSnapshot('main');
- expect(renderFunction({ value: 'branch-7.4', isMain: false })).toMatchSnapshot('branch');
- expect(renderFunction({ value: 'disabled', isMain: false, disabled: true })).toMatchSnapshot(
- 'disabled'
- );
expect(
- renderFunction({ value: 'branch-nope', isMain: false, isInvalid: true })
+ renderBranchOption({ ...props, data: { label: 'master', value: 'master', isMain: true } })
+ ).toMatchSnapshot('main');
+ expect(
+ renderBranchOption({
+ ...props,
+ data: { label: 'branch-7.4', value: 'branch-7.4', isMain: false }
+ })
+ ).toMatchSnapshot('branch');
+ expect(
+ renderBranchOption({
+ ...props,
+ data: { label: 'disabled', value: 'disabled', isMain: false, isDisabled: true }
+ })
+ ).toMatchSnapshot('disabled');
+ expect(
+ renderBranchOption({
+ ...props,
+ data: { value: 'branch-nope', isMain: false, isInvalid: true }
+ })
).toMatchSnapshot("branch doesn't exist");
});
});
function shallowRender(props: Partial<BaselineSettingReferenceBranchProps> = {}) {
const branchOptions = [
- { value: 'master', isMain: true },
- { value: 'branch-7.9', isMain: false }
+ { label: 'master', value: 'master', isMain: true },
+ { label: 'branch-7.9', value: 'branch-7.9', isMain: false }
];
return shallow(
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BaselineSettingReferenceBranch-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BaselineSettingReferenceBranch-test.tsx.snap
index 7ebf3e41e4f..d4f3b6d5534 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BaselineSettingReferenceBranch-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BaselineSettingReferenceBranch-test.tsx.snap
@@ -1,46 +1,87 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renderOption should render correctly: branch 1`] = `
-<React.Fragment>
- <span>
- branch-7.4
- </span>
-</React.Fragment>
+<Option
+ data={
+ Object {
+ "isMain": false,
+ "label": "branch-7.4",
+ "value": "branch-7.4",
+ }
+ }
+>
+ <React.Fragment>
+ <span>
+ branch-7.4
+ </span>
+ </React.Fragment>
+</Option>
`;
exports[`renderOption should render correctly: branch doesn't exist 1`] = `
-<Tooltip
- overlay="baseline.reference_branch.does_not_exist.branch-nope"
+<Option
+ data={
+ Object {
+ "isInvalid": true,
+ "isMain": false,
+ "value": "branch-nope",
+ }
+ }
>
- <span>
- branch-nope
-
- <AlertErrorIcon />
- </span>
-</Tooltip>
+ <Tooltip
+ overlay="baseline.reference_branch.does_not_exist.branch-nope"
+ >
+ <span>
+ branch-nope
+
+ <AlertErrorIcon />
+ </span>
+ </Tooltip>
+</Option>
`;
exports[`renderOption should render correctly: disabled 1`] = `
-<React.Fragment>
- <span
- title="baseline.reference_branch.cannot_be_itself"
- >
- disabled
- </span>
-</React.Fragment>
+<Option
+ data={
+ Object {
+ "isDisabled": true,
+ "isMain": false,
+ "label": "disabled",
+ "value": "disabled",
+ }
+ }
+>
+ <React.Fragment>
+ <span
+ title="baseline.reference_branch.cannot_be_itself"
+ >
+ disabled
+ </span>
+ </React.Fragment>
+</Option>
`;
exports[`renderOption should render correctly: main 1`] = `
-<React.Fragment>
- <span>
- master
- </span>
- <div
- className="badge spacer-left"
- >
- branches.main_branch
- </div>
-</React.Fragment>
+<Option
+ data={
+ Object {
+ "isMain": true,
+ "label": "master",
+ "value": "master",
+ }
+ }
+>
+ <React.Fragment>
+ <span>
+ master
+ </span>
+ <div
+ className="badge spacer-left"
+ >
+ branches.main_branch
+ </div>
+ </React.Fragment>
+</Option>
`;
exports[`should render correctly: Branch level - no other branches 1`] = `
@@ -67,24 +108,28 @@ exports[`should render correctly: Branch level - no other branches 1`] = `
</strong>
<MandatoryFieldMarker />
</label>
- <SearchSelect
+ <Select
autofocus={false}
className="little-spacer-top spacer-bottom"
- defaultOptions={
+ components={
+ Object {
+ "Option": [Function],
+ }
+ }
+ onChange={[Function]}
+ options={
Array [
Object {
"isMain": true,
+ "label": "master",
"value": "master",
},
]
}
- minimumQueryLength={1}
- onSearch={[Function]}
- onSelect={[Function]}
- renderOption={[Function]}
value={
Object {
"isMain": true,
+ "label": "master",
"value": "master",
}
}
@@ -117,28 +162,33 @@ exports[`should render correctly: Branch level 1`] = `
</strong>
<MandatoryFieldMarker />
</label>
- <SearchSelect
+ <Select
autofocus={false}
className="little-spacer-top spacer-bottom"
- defaultOptions={
+ components={
+ Object {
+ "Option": [Function],
+ }
+ }
+ onChange={[Function]}
+ options={
Array [
Object {
"isMain": true,
+ "label": "master",
"value": "master",
},
Object {
"isMain": false,
+ "label": "branch-7.9",
"value": "branch-7.9",
},
]
}
- minimumQueryLength={1}
- onSearch={[Function]}
- onSelect={[Function]}
- renderOption={[Function]}
value={
Object {
"isMain": true,
+ "label": "master",
"value": "master",
}
}
@@ -176,28 +226,33 @@ exports[`should render correctly: Project level 1`] = `
</strong>
<MandatoryFieldMarker />
</label>
- <SearchSelect
+ <Select
autofocus={false}
className="little-spacer-top spacer-bottom"
- defaultOptions={
+ components={
+ Object {
+ "Option": [Function],
+ }
+ }
+ onChange={[Function]}
+ options={
Array [
Object {
"isMain": true,
+ "label": "master",
"value": "master",
},
Object {
"isMain": false,
+ "label": "branch-7.9",
"value": "branch-7.9",
},
]
}
- minimumQueryLength={1}
- onSearch={[Function]}
- onSelect={[Function]}
- renderOption={[Function]}
value={
Object {
"isMain": true,
+ "label": "master",
"value": "master",
}
}
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap
index faeab231d18..03eb20e011e 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/BranchBaselineSettingModal-test.tsx.snap
@@ -44,13 +44,15 @@ exports[`should render correctly: multiple branches 1`] = `
branchList={
Array [
Object {
- "disabled": true,
+ "isDisabled": true,
"isMain": true,
+ "label": "master",
"value": "master",
},
Object {
- "disabled": false,
+ "isDisabled": false,
"isMain": false,
+ "label": "branch-6.7",
"value": "branch-6.7",
},
]
@@ -129,8 +131,9 @@ exports[`should render correctly: only one branch 1`] = `
branchList={
Array [
Object {
- "disabled": true,
+ "isDisabled": true,
"isMain": true,
+ "label": "master",
"value": "master",
},
]
diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap
index a0fe096a3b6..bb2a408d107 100644
--- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineSelector-test.tsx.snap
@@ -65,6 +65,7 @@ exports[`should render correctly 1`] = `
Array [
Object {
"isMain": true,
+ "label": "master",
"value": "master",
},
]
diff --git a/server/sonar-web/src/main/js/components/controls/SearchSelect.tsx b/server/sonar-web/src/main/js/components/controls/SearchSelect.tsx
deleted file mode 100644
index c89bd94f372..00000000000
--- a/server/sonar-web/src/main/js/components/controls/SearchSelect.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { debounce } from 'lodash';
-import * as React from 'react';
-import { translate, translateWithParameters } from '../../helpers/l10n';
-import SelectLegacy, { CreatableLegacy } from './SelectLegacy';
-
-interface Props<T> {
- autofocus?: boolean;
- canCreate?: boolean;
- className?: string;
- clearable?: boolean;
- defaultOptions?: T[];
- minimumQueryLength?: number;
- multi?: boolean;
- onSearch: (query: string) => Promise<T[]>;
- onSelect?: (option: T) => void;
- onMultiSelect?: (options: T[]) => void;
- promptTextCreator?: (label: string) => string;
- renderOption?: (option: T) => JSX.Element;
- resetOnBlur?: boolean;
- value?: T | T[];
-}
-
-interface State<T> {
- loading: boolean;
- options: T[];
- query: string;
-}
-
-export default class SearchSelect<T extends { value: string }> extends React.PureComponent<
- Props<T>,
- State<T>
-> {
- mounted = false;
-
- constructor(props: Props<T>) {
- 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) => {
- // Ignore the result if the query changed
- const currentQuery = query;
- this.props.onSearch(currentQuery).then(
- options => {
- if (this.mounted) {
- this.setState(state => ({
- loading: false,
- options: state.query === currentQuery ? options : state.options
- }));
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- }
- );
- };
-
- handleChange = (option: T | T[]) => {
- if (Array.isArray(option)) {
- if (this.props.onMultiSelect) {
- this.props.onMultiSelect(option);
- }
- } else if (this.props.onSelect) {
- this.props.onSelect(option);
- }
- };
-
- handleInputChange = (query: string) => {
- 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 });
- }
- };
-
- // disable internal filtering
- handleFilterOption = () => true;
-
- render() {
- const Component = this.props.canCreate ? CreatableLegacy : SelectLegacy;
- return (
- <Component
- autoFocus={this.autofocus}
- className={this.props.className}
- clearable={this.props.clearable}
- 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)
- : translate('select2.noMatches')
- }
- onBlurResetsInput={this.resetOnBlur}
- onChange={this.handleChange}
- onInputChange={this.handleInputChange}
- 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 4273c2fbbb0..42862806a66 100644
--- a/server/sonar-web/src/main/js/components/controls/Select.tsx
+++ b/server/sonar-web/src/main/js/components/controls/Select.tsx
@@ -18,8 +18,20 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import styled from '@emotion/styled';
+import classNames from 'classnames';
import * as React from 'react';
-import ReactSelect, { GroupTypeBase, IndicatorProps, Props, StylesConfig } from 'react-select';
+import ReactSelect, {
+ GroupTypeBase,
+ IndicatorProps,
+ OptionTypeBase,
+ Props,
+ StylesConfig
+} from 'react-select';
+import AsyncReactSelect, { AsyncProps } from 'react-select/async';
+import AsyncCreatableReactSelect, {
+ Props as AsyncCreatableProps
+} from 'react-select/async-creatable';
+import { LoadingIndicatorProps } from 'react-select/src/components/indicators';
import { MultiValueRemoveProps } from 'react-select/src/components/MultiValue';
import { colors, others, sizes, zIndexes } from '../../app/theme';
import { ClearButton } from './buttons';
@@ -38,29 +50,51 @@ export interface BasicSelectOption {
value: string;
}
-export default class Select<
- Option,
+export function dropdownIndicator<
+ Option extends OptionTypeBase,
IsMulti extends boolean = false,
Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
-> extends React.PureComponent<Props<Option, IsMulti, Group>> {
- dropdownIndicator({ innerProps }: IndicatorProps<Option, IsMulti, Group>) {
- return <ArrowSpan {...innerProps} />;
- }
+>({ innerProps }: IndicatorProps<Option, IsMulti, Group>) {
+ return <ArrowSpan {...innerProps} />;
+}
- clearIndicator({ innerProps }: IndicatorProps<Option, IsMulti, Group>) {
- return (
- <ClearButton
- className="button-tiny spacer-left spacer-right text-middle"
- iconProps={{ size: 12 }}
- {...innerProps}
- />
- );
- }
+export function clearIndicator<
+ Option extends OptionTypeBase,
+ IsMulti extends boolean = false,
+ Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
+>({ innerProps }: IndicatorProps<Option, IsMulti, Group>) {
+ return (
+ <ClearButton
+ className="button-tiny spacer-left spacer-right text-middle"
+ iconProps={{ size: 12 }}
+ {...innerProps}
+ />
+ );
+}
- multiValueRemove(props: MultiValueRemoveProps<Option, Group>) {
- return <div {...props.innerProps}>×</div>;
- }
+export function loadingIndicator<
+ Option extends OptionTypeBase,
+ IsMulti extends boolean,
+ Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
+>({ innerProps }: LoadingIndicatorProps<Option, IsMulti, Group>) {
+ return (
+ <i className={classNames('deferred-spinner spacer-left spacer-right', innerProps.className)} />
+ );
+}
+export function multiValueRemove<
+ Option extends OptionTypeBase,
+ Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
+>(props: MultiValueRemoveProps<Option, Group>) {
+ return <div {...props.innerProps}>×</div>;
+}
+
+/* Keeping it as a class to simplify a dozen tests */
+export default class Select<
+ Option,
+ IsMulti extends boolean = false,
+ Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
+> extends React.Component<Props<Option, IsMulti, Group>> {
render() {
return (
<ReactSelect
@@ -68,15 +102,55 @@ export default class Select<
styles={selectStyle<Option, IsMulti, Group>()}
components={{
...this.props.components,
- DropdownIndicator: this.dropdownIndicator,
- ClearIndicator: this.clearIndicator,
- MultiValueRemove: this.multiValueRemove
+ DropdownIndicator: dropdownIndicator,
+ ClearIndicator: clearIndicator,
+ MultiValueRemove: multiValueRemove
}}
/>
);
}
}
+export function CreatableSelect<
+ Option,
+ isMulti extends boolean,
+ Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
+>(props: AsyncCreatableProps<Option, isMulti, Group>) {
+ return (
+ <AsyncCreatableReactSelect
+ {...props}
+ styles={selectStyle<Option, isMulti, Group>()}
+ components={{
+ ...props.components,
+ DropdownIndicator: dropdownIndicator,
+ ClearIndicator: clearIndicator,
+ MultiValueRemove: multiValueRemove,
+ LoadingIndicator: loadingIndicator
+ }}
+ />
+ );
+}
+
+export function SearchSelect<
+ Option,
+ isMulti extends boolean,
+ Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
+>(props: Props<Option, isMulti, Group> & AsyncProps<Option, Group>) {
+ return (
+ <AsyncReactSelect
+ {...props}
+ styles={selectStyle<Option, isMulti, Group>()}
+ components={{
+ ...props.components,
+ DropdownIndicator: dropdownIndicator,
+ ClearIndicator: clearIndicator,
+ MultiValueRemove: multiValueRemove,
+ LoadingIndicator: loadingIndicator
+ }}
+ />
+ );
+}
+
export function selectStyle<
Option,
IsMulti extends boolean,
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
deleted file mode 100644
index b43492863b0..00000000000
--- a/server/sonar-web/src/main/js/components/controls/__tests__/SearchSelect-test.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 * as React from 'react';
-import SearchSelect from '../SearchSelect';
-
-jest.mock('lodash', () => {
- const lodash = jest.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({ value: '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__/Select-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/Select-test.tsx
index 57a924e18bb..840cef0a826 100644
--- a/server/sonar-web/src/main/js/components/controls/__tests__/Select-test.tsx
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/Select-test.tsx
@@ -19,9 +19,18 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
-import { components, GroupTypeBase, InputProps, Props } from 'react-select';
+import { components, GroupTypeBase, InputProps, Props as ReactSelectProps } from 'react-select';
+import { LoadingIndicatorProps } from 'react-select/src/components/indicators';
+import { MultiValueRemoveProps } from 'react-select/src/components/MultiValue';
import { mockReactSelectIndicatorProps } from '../../../helpers/mocks/react-select';
-import Select from '../Select';
+import Select, {
+ clearIndicator,
+ CreatableSelect,
+ dropdownIndicator,
+ loadingIndicator,
+ multiValueRemove,
+ SearchSelect
+} from '../Select';
describe('Select', () => {
it('should render correctly', () => {
@@ -33,35 +42,54 @@ describe('Select', () => {
<components.Input {...props} className={`little-spacer-top ${props.className}`} />
);
- const props = {
- isClearable: true,
- isLoading: true,
- components: {
- Input: inputRenderer
- }
- };
- expect(shallowRender(props)).toMatchSnapshot('other props');
+ expect(
+ shallowRender({
+ isClearable: true,
+ isLoading: true,
+ components: {
+ Input: inputRenderer
+ }
+ })
+ ).toMatchSnapshot('other props');
});
it('should render clearIndicator correctly', () => {
- const wrapper = shallowRender();
- const ClearIndicator = wrapper.instance().clearIndicator;
- const clearIndicator = shallow(<ClearIndicator {...mockReactSelectIndicatorProps()} />);
- expect(clearIndicator).toBeDefined();
+ expect(clearIndicator(mockReactSelectIndicatorProps({ value: '' }))).toMatchSnapshot();
});
it('should render dropdownIndicator correctly', () => {
- const wrapper = shallowRender();
- const DropdownIndicator = wrapper.instance().dropdownIndicator;
- const clearIndicator = shallow(<DropdownIndicator {...mockReactSelectIndicatorProps()} />);
- expect(clearIndicator).toBeDefined();
+ expect(dropdownIndicator(mockReactSelectIndicatorProps({ value: '' }))).toMatchSnapshot();
+ });
+
+ it('should render loadingIndicator correctly', () => {
+ expect(
+ loadingIndicator({ innerProps: { className: 'additional-class' } } as LoadingIndicatorProps<
+ {},
+ false
+ >)
+ ).toMatchSnapshot();
+ });
+
+ it('should render multiValueRemove correctly', () => {
+ expect(multiValueRemove({ innerProps: {} } as MultiValueRemoveProps<{}>)).toMatchSnapshot();
});
function shallowRender<
Option,
IsMulti extends boolean = false,
Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
- >(props: Partial<Props<Option, IsMulti, Group>> = {}) {
- return shallow<Select<Option, IsMulti, Group>>(<Select {...props} />);
+ >(props: Partial<ReactSelectProps<Option, IsMulti, Group>> = {}) {
+ return shallow<ReactSelectProps<Option, IsMulti, Group>>(<Select {...props} />);
}
});
+
+it.each([
+ ['CreatableSelect', CreatableSelect],
+ ['SearchSelect', SearchSelect]
+])('should render %s correctly', (_name, Component) => {
+ expect(
+ shallow(<Component />)
+ .dive()
+ .dive()
+ ).toMatchSnapshot();
+});
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
deleted file mode 100644
index 9ee6e2df213..00000000000
--- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchSelect-test.tsx.snap
+++ /dev/null
@@ -1,17 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render Select 1`] = `
-<SelectLegacy
- autoFocus={true}
- escapeClearsValue={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__/Select-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Select-test.tsx.snap
index a8ecb8b67ff..eff8131e9a9 100644
--- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Select-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Select-test.tsx.snap
@@ -1,5 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`Select should render clearIndicator correctly 1`] = `
+<ClearButton
+ className="button-tiny spacer-left spacer-right text-middle"
+ iconProps={
+ Object {
+ "size": 12,
+ }
+ }
+/>
+`;
+
exports[`Select should render complex select component: other props 1`] = `
<StateManager
components={
@@ -70,3 +81,143 @@ exports[`Select should render correctly: default 1`] = `
}
/>
`;
+
+exports[`Select should render dropdownIndicator correctly 1`] = `<Styled(span) />`;
+
+exports[`Select should render loadingIndicator correctly 1`] = `
+<i
+ className="deferred-spinner spacer-left spacer-right additional-class"
+/>
+`;
+
+exports[`Select should render multiValueRemove correctly 1`] = `
+<div>
+ ×
+</div>
+`;
+
+exports[`should render CreatableSelect correctly 1`] = `
+<Creatable
+ allowCreateWhileLoading={false}
+ cacheOptions={false}
+ components={
+ Object {
+ "ClearIndicator": [Function],
+ "DropdownIndicator": [Function],
+ "LoadingIndicator": [Function],
+ "MultiValueRemove": [Function],
+ }
+ }
+ createOptionPosition="last"
+ defaultOptions={false}
+ filterOption={null}
+ formatCreateLabel={[Function]}
+ getNewOptionData={[Function]}
+ getOptionLabel={[Function]}
+ getOptionValue={[Function]}
+ inputValue=""
+ isLoading={false}
+ isValidNewOption={[Function]}
+ menuIsOpen={false}
+ onChange={[Function]}
+ onInputChange={[Function]}
+ onMenuClose={[Function]}
+ onMenuOpen={[Function]}
+ options={Array []}
+ styles={
+ Object {
+ "container": [Function],
+ "control": [Function],
+ "indicatorsContainer": [Function],
+ "input": [Function],
+ "loadingIndicator": [Function],
+ "menu": [Function],
+ "menuList": [Function],
+ "multiValue": [Function],
+ "multiValueLabel": [Function],
+ "multiValueRemove": [Function],
+ "noOptionsMessage": [Function],
+ "option": [Function],
+ "placeholder": [Function],
+ "singleValue": [Function],
+ "valueContainer": [Function],
+ }
+ }
+ value={null}
+/>
+`;
+
+exports[`should render SearchSelect correctly 1`] = `
+<Select
+ aria-live="polite"
+ backspaceRemovesValue={true}
+ blurInputOnSelect={true}
+ cacheOptions={false}
+ captureMenuScroll={false}
+ closeMenuOnScroll={false}
+ closeMenuOnSelect={true}
+ components={
+ Object {
+ "ClearIndicator": [Function],
+ "DropdownIndicator": [Function],
+ "LoadingIndicator": [Function],
+ "MultiValueRemove": [Function],
+ }
+ }
+ controlShouldRenderValue={true}
+ defaultOptions={false}
+ escapeClearsValue={false}
+ filterOption={null}
+ formatGroupLabel={[Function]}
+ getOptionLabel={[Function]}
+ getOptionValue={[Function]}
+ inputValue=""
+ isDisabled={false}
+ isLoading={false}
+ isMulti={false}
+ isOptionDisabled={[Function]}
+ isRtl={false}
+ isSearchable={true}
+ loadingMessage={[Function]}
+ maxMenuHeight={300}
+ menuIsOpen={false}
+ menuPlacement="bottom"
+ menuPosition="absolute"
+ menuShouldBlockScroll={false}
+ menuShouldScrollIntoView={true}
+ minMenuHeight={140}
+ noOptionsMessage={[Function]}
+ onChange={[Function]}
+ onInputChange={[Function]}
+ onMenuClose={[Function]}
+ onMenuOpen={[Function]}
+ openMenuOnClick={true}
+ openMenuOnFocus={false}
+ options={Array []}
+ pageSize={5}
+ placeholder="Select..."
+ screenReaderStatus={[Function]}
+ styles={
+ Object {
+ "container": [Function],
+ "control": [Function],
+ "indicatorsContainer": [Function],
+ "input": [Function],
+ "loadingIndicator": [Function],
+ "menu": [Function],
+ "menuList": [Function],
+ "multiValue": [Function],
+ "multiValueLabel": [Function],
+ "multiValueRemove": [Function],
+ "noOptionsMessage": [Function],
+ "option": [Function],
+ "placeholder": [Function],
+ "singleValue": [Function],
+ "valueContainer": [Function],
+ }
+ }
+ tabIndex="0"
+ tabSelectsValue={true}
+ value={null}
+/>
+`;
diff --git a/server/sonar-web/src/main/js/helpers/mocks/react-select.ts b/server/sonar-web/src/main/js/helpers/mocks/react-select.ts
index 05f49106ed4..d25e6abf791 100644
--- a/server/sonar-web/src/main/js/helpers/mocks/react-select.ts
+++ b/server/sonar-web/src/main/js/helpers/mocks/react-select.ts
@@ -49,6 +49,6 @@ export function mockReactSelectIndicatorProps<
OptionType,
IsMulti extends boolean,
GroupType extends GroupTypeBase<OptionType> = GroupTypeBase<OptionType>
->(): IndicatorProps<OptionType, IsMulti, GroupType> {
+>(_option: OptionType): IndicatorProps<OptionType, IsMulti, GroupType> {
return {} as IndicatorProps<OptionType, IsMulti, GroupType>;
}