]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16090 Removing select legacy from quality-profiles
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Thu, 17 Mar 2022 16:47:59 +0000 (17:47 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 28 Mar 2022 20:02:53 +0000 (20:02 +0000)
20 files changed:
server/sonar-web/src/main/js/apps/quality-profiles/compare/ComparisonForm.tsx
server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/ComparisonForm-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/compare/__tests__/__snapshots__/ComparisonForm-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeParentForm.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ChangeParentForm-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsFormSelect-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ChangeParentForm-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsForm-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsFormSelect-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListHeader.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/ProfilesListHeader-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/CreateProfileForm-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/ProfilesListHeader-test.tsx.snap
server/sonar-web/src/main/js/components/controls/Select.tsx
server/sonar-web/src/main/js/components/controls/__tests__/Select-test.tsx
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Select-test.tsx.snap
server/sonar-web/src/main/js/helpers/mocks/react-select.ts

index 87b1845116158c65d889b52cca7afe939bfa7adb..66951094fee0081297821552576eacb94d03a509 100644 (file)
@@ -18,7 +18,8 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import SelectLegacy from '../../../components/controls/SelectLegacy';
+import { components, OptionProps, SingleValueProps } from 'react-select';
+import Select from '../../../components/controls/Select';
 import Tooltip from '../../../components/controls/Tooltip';
 import { translate } from '../../../helpers/l10n';
 import { Profile } from '../types';
@@ -30,44 +31,74 @@ interface Props {
   withKey?: string;
 }
 
+interface Option {
+  value: string;
+  label: string;
+  isDefault: boolean | undefined;
+}
 export default class ComparisonForm extends React.PureComponent<Props> {
   handleChange = (option: { value: string }) => {
     this.props.onCompare(option.value);
   };
 
+  optionRenderer(
+    options: Option[],
+    props: OptionProps<Omit<Option, 'label' | 'isDefault'>, false>
+  ) {
+    const { data } = props;
+    return <components.Option {...props}>{renderValue(data, options)}</components.Option>;
+  }
+
+  singleValueRenderer = (
+    options: Option[],
+    props: SingleValueProps<Omit<typeof options[0], 'label' | 'isDefault'>>
+  ) => (
+    <components.SingleValue {...props}>{renderValue(props.data, options)}</components.SingleValue>
+  );
+
   render() {
     const { profile, profiles, withKey } = this.props;
     const options = profiles
       .filter(p => p.language === profile.language && p !== profile)
       .map(p => ({ value: p.key, label: p.name, isDefault: p.isDefault }));
 
-    function renderValue(p: typeof options[0]) {
-      return (
-        <div>
-          <span>{p.label}</span>
-          {p.isDefault && (
-            <Tooltip overlay={translate('quality_profiles.list.default.help')}>
-              <span className=" spacer-left badge">{translate('default')}</span>
-            </Tooltip>
-          )}
-        </div>
-      );
-    }
-
     return (
       <div className="display-inline-block">
-        <label className="spacer-right">{translate('quality_profiles.compare_with')}</label>
-        <SelectLegacy
+        <label htmlFor="quality-profiles-comparision-input" className="spacer-right">
+          {translate('quality_profiles.compare_with')}
+        </label>
+        <Select
           className="input-large"
-          clearable={false}
+          autoFocus={true}
+          isClearable={false}
+          id="quality-profiles-comparision"
+          inputId="quality-profiles-comparision-input"
           onChange={this.handleChange}
           options={options}
-          valueRenderer={renderValue}
-          optionRenderer={renderValue}
-          placeholder={translate('select_verb')}
-          value={withKey}
+          isSearchable={true}
+          components={{
+            Option: this.optionRenderer.bind(this, options),
+            SingleValue: this.singleValueRenderer.bind(null, options)
+          }}
+          value={options.filter(o => o.value === withKey)}
         />
       </div>
     );
   }
 }
+
+function renderValue(p: Omit<Option, 'label' | 'isDefault'>, options: Option[]) {
+  const selectedOption = options.find(o => o.value === p.value);
+  if (selectedOption !== undefined) {
+    return (
+      <div>
+        <span>{selectedOption.label}</span>
+        {selectedOption.isDefault && (
+          <Tooltip overlay={translate('quality_profiles.list.default.help')}>
+            <span className="spacer-left badge">{translate('default')}</span>
+          </Tooltip>
+        )}
+      </div>
+    );
+  }
+}
index 4226404b743036520ed5ea52e34f7f0ba1b76964..6d55832dfd3d14e9ccbf9629014cdec67c0e6cff 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import SelectLegacy from '../../../../components/controls/SelectLegacy';
+import { mockReactSelectOptionProps } from '../../../../helpers/mocks/react-select';
+import Select from '../../../../components/controls/Select';
 import { mockQualityProfile } from '../../../../helpers/testMocks';
 import ComparisonForm from '../ComparisonForm';
 
 it('should render Select with right options', () => {
+  const output = shallowRender().find(Select);
+
+  expect(output.length).toBe(1);
+  expect(output.prop('value')).toEqual([
+    { isDefault: true, value: 'another', label: 'another name' }
+  ]);
+  expect(output.prop('options')).toEqual([
+    { isDefault: true, value: 'another', label: 'another name' }
+  ]);
+});
+
+it('should render option correctly', () => {
+  const wrapper = shallowRender();
+  const mockOptions = [
+    {
+      value: 'val',
+      label: 'label',
+      isDefault: undefined
+    }
+  ];
+  const OptionRenderer = wrapper.instance().optionRenderer.bind(null, mockOptions);
+  expect(
+    shallow(<OptionRenderer {...mockReactSelectOptionProps({ value: 'test' })} />)
+  ).toMatchSnapshot('option render');
+});
+
+it('should render value correctly', () => {
+  const wrapper = shallowRender();
+  const mockOptions = [
+    {
+      value: 'val',
+      label: 'label',
+      isDefault: true
+    }
+  ];
+  const ValueRenderer = wrapper.instance().singleValueRenderer.bind(null, mockOptions);
+  expect(
+    shallow(<ValueRenderer {...mockReactSelectOptionProps({ value: 'test' })} />)
+  ).toMatchSnapshot('value render');
+});
+
+function shallowRender(overrides: Partial<ComparisonForm['props']> = {}) {
   const profile = mockQualityProfile();
   const profiles = [
     profile,
-    mockQualityProfile({ key: 'another', name: 'another name' }),
+    mockQualityProfile({ key: 'another', name: 'another name', isDefault: true }),
     mockQualityProfile({ key: 'java', name: 'java', language: 'java' })
   ];
 
-  const profileDefault = { value: 'c', label: 'c name', isDefault: true };
-
-  const output = shallow(
+  return shallow<ComparisonForm>(
     <ComparisonForm
       onCompare={() => true}
       profile={profile}
       profiles={profiles}
       withKey="another"
+      {...overrides}
     />
-  ).find(SelectLegacy);
-  expect(output.props().valueRenderer!(profileDefault)).toMatchSnapshot('Render default for value');
-  expect(output.props().valueRenderer!(profile)).toMatchSnapshot('Render for value');
-
-  expect(output.props().optionRenderer!(profileDefault)).toMatchSnapshot(
-    'Render default for option'
   );
-  expect(output.props().optionRenderer!(profile)).toMatchSnapshot('Render for option');
-
-  expect(output.length).toBe(1);
-  expect(output.prop('value')).toBe('another');
-  expect(output.prop('options')).toEqual([
-    { isDefault: false, value: 'another', label: 'another name' }
-  ]);
-});
+}
index ebf9047b0b9ce5f474e630bb67b246b43e473327..70cd9a811d3eeafad86aa088e95b574d1dbd1261 100644 (file)
@@ -1,47 +1,21 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`should render Select with right options: Render default for option 1`] = `
-<div>
-  <span>
-    c name
-  </span>
-  <Tooltip
-    overlay="quality_profiles.list.default.help"
-  >
-    <span
-      className=" spacer-left badge"
-    >
-      default
-    </span>
-  </Tooltip>
-</div>
+exports[`should render option correctly: option render 1`] = `
+<Option
+  data={
+    Object {
+      "value": "test",
+    }
+  }
+/>
 `;
 
-exports[`should render Select with right options: Render default for value 1`] = `
-<div>
-  <span>
-    c name
-  </span>
-  <Tooltip
-    overlay="quality_profiles.list.default.help"
-  >
-    <span
-      className=" spacer-left badge"
-    >
-      default
-    </span>
-  </Tooltip>
-</div>
-`;
-
-exports[`should render Select with right options: Render for option 1`] = `
-<div>
-  <span />
-</div>
-`;
-
-exports[`should render Select with right options: Render for value 1`] = `
-<div>
-  <span />
-</div>
+exports[`should render value correctly: value render 1`] = `
+<SingleValue
+  data={
+    Object {
+      "value": "test",
+    }
+  }
+/>
 `;
index e43d57b89769f21417278d912358467805f4856d..ddca5097076b985242f2c52c8113a1dd1abd41bc 100644 (file)
  */
 import { sortBy } from 'lodash';
 import * as React from 'react';
+import Select from '../../../components/controls/Select';
 import { changeProfileParent } from '../../../api/quality-profiles';
 import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import Modal from '../../../components/controls/Modal';
-import SelectLegacy from '../../../components/controls/SelectLegacy';
 import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
 import { translate } from '../../../helpers/l10n';
@@ -92,6 +92,8 @@ export default class ChangeParentForm extends React.PureComponent<Props, State>
       this.state.selected == null ||
       this.state.selected === this.props.profile.parentKey;
 
+    const selectedValue = this.state.selected ?? (this.props.profile.parentKey || '');
+
     return (
       <Modal
         contentLabel={translate('quality_profiles.change_parent')}
@@ -104,21 +106,21 @@ export default class ChangeParentForm extends React.PureComponent<Props, State>
           <div className="modal-body">
             <MandatoryFieldsExplanation className="modal-field" />
             <div className="modal-field">
-              <label htmlFor="change-profile-parent">
+              <label htmlFor="change-profile-parent-input">
                 {translate('quality_profiles.parent')}
                 <MandatoryFieldMarker />
               </label>
-              <SelectLegacy
-                clearable={false}
-                id="change-profile-parent"
+              <Select
+                className="width-100"
+                autoFocus={true}
                 name="parentKey"
+                isClearable={false}
+                id="change-profile-parent"
+                inputId="change-profile-parent-input"
                 onChange={this.handleSelectChange}
                 options={options}
-                value={
-                  this.state.selected != null
-                    ? this.state.selected
-                    : this.props.profile.parentKey || ''
-                }
+                isSearchable={true}
+                value={options.filter(o => o.value === selectedValue)}
               />
             </div>
           </div>
index d9c829ff98b51966aca34a1b2608684d50cf6b87..96035b0c7c4dcfa2cb539920832449b24103d5ca 100644 (file)
@@ -129,7 +129,9 @@ export default class ProfilePermissionsForm extends React.PureComponent<Props, S
         <form onSubmit={this.handleFormSubmit}>
           <div className="modal-body">
             <div className="modal-field">
-              <label>{translate('quality_profiles.search_description')}</label>
+              <label htmlFor="change-profile-permission-input">
+                {translate('quality_profiles.search_description')}
+              </label>
               <ProfilePermissionsFormSelect
                 onChange={this.handleValueChange}
                 onSearch={this.handleSearch}
index a88e17f798fcfd5731eaee2228e56467aa8591d4..4ffb8d67b0b902074bacbb89671a9339af17f299 100644 (file)
  * 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, identity } from 'lodash';
+import { debounce, identity, omit } from 'lodash';
 import * as React from 'react';
-import SelectLegacy from '../../../components/controls/SelectLegacy';
+import { components, ControlProps, OptionProps, SingleValueProps } from 'react-select';
+import Select from '../../../components/controls/Select';
 import GroupIcon from '../../../components/icons/GroupIcon';
 import Avatar from '../../../components/ui/Avatar';
 import { translate } from '../../../helpers/l10n';
@@ -83,29 +84,65 @@ export default class ProfilePermissionsFormSelect extends React.PureComponent<Pr
     }
   };
 
+  optionRenderer(props: OptionProps<OptionWithValue, false>) {
+    const { data } = props;
+    return (
+      <components.Option {...props} className="Select-option">
+        {customOptions(data)}
+      </components.Option>
+    );
+  }
+
+  singleValueRenderer = (props: SingleValueProps<OptionWithValue>) => (
+    <components.SingleValue {...props} className="Select-value-label">
+      {customOptions(props.data)}
+    </components.SingleValue>
+  );
+
+  controlRenderer = (props: ControlProps<OptionWithValue, false>) => (
+    <components.Control {...omit(props, ['children'])} className="abs-height-100 Select-control">
+      {props.children}
+    </components.Control>
+  );
+
   render() {
     const noResultsText = translate('no_results');
-
+    const { selected } = this.props;
     // create a uniq string both for users and groups
     const options = this.state.searchResults.map(r => ({ ...r, value: getStringValue(r) }));
 
+    // when user input is empty the options shows only top 30 names
+    // the below code add the selected user so that it appears too
+    if (
+      selected !== undefined &&
+      options.find(o => o.value === getStringValue(selected)) === undefined
+    ) {
+      options.unshift({ ...selected, value: getStringValue(selected) });
+    }
+
     return (
-      <SelectLegacy
+      <Select
+        className="Select-big width-100"
         autoFocus={true}
-        className="Select-big"
-        clearable={false}
-        // disable default react-select filtering
-        filterOptions={identity}
-        isLoading={this.state.loading}
-        noResultsText={noResultsText}
+        isClearable={false}
+        id="change-profile-permission"
+        inputId="change-profile-permission-input"
         onChange={this.props.onChange}
         onInputChange={this.handleInputChange}
-        optionRenderer={optionRenderer}
-        options={options}
         placeholder=""
-        searchable={true}
-        value={this.props.selected && getStringValue(this.props.selected)}
-        valueRenderer={optionRenderer}
+        noOptionsMessage={() => noResultsText}
+        isLoading={this.state.loading}
+        options={options}
+        isSearchable={true}
+        filterOptions={identity}
+        components={{
+          Option: this.optionRenderer,
+          SingleValue: this.singleValueRenderer,
+          Control: this.controlRenderer
+        }}
+        value={options.filter(
+          o => o.value === (this.props.selected && getStringValue(this.props.selected))
+        )}
       />
     );
   }
@@ -119,7 +156,7 @@ function getStringValue(option: Option) {
   return isUser(option) ? `user:${option.login}` : `group:${option.name}`;
 }
 
-function optionRenderer(option: OptionWithValue) {
+function customOptions(option: OptionWithValue) {
   return isUser(option) ? (
     <>
       <Avatar hash={option.avatar} name={option.name} size={16} />
index 137927a66cc7b1abae67f8aaff7c4a735dfb86cc..10f195e901b114500f0e77820ec1f01465cc6def 100644 (file)
@@ -33,7 +33,7 @@ it('should render correctly', () => {
   expect(shallowRender()).toMatchSnapshot();
 });
 
-it("should handle form' submit correcty", async () => {
+it('should handle form submit correcty', async () => {
   const onChange = jest.fn();
 
   const wrapper = shallowRender({ onChange });
@@ -43,6 +43,14 @@ it("should handle form' submit correcty", async () => {
   expect(onChange).toHaveBeenCalled();
 });
 
+it('should handle select change correcty', async () => {
+  const wrapper = shallowRender();
+  wrapper.instance().handleSelectChange({ value: 'val' });
+  await waitAndUpdate(wrapper);
+
+  expect(wrapper.instance().state.selected).toEqual('val');
+});
+
 function shallowRender(props?: Partial<ChangeParentForm['props']>) {
   return shallow<ChangeParentForm>(
     <ChangeParentForm
index 9b4fbf37955ae7303d1be2fb282a04bc2e56a943..1060121d1a261f17eb8cdd88985d655cd6c52296 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import {
+  mockReactSelectControlProps,
+  mockReactSelectOptionProps
+} from '../../../../helpers/mocks/react-select';
 import ProfilePermissionsFormSelect from '../ProfilePermissionsFormSelect';
 
 jest.mock('lodash', () => {
@@ -62,3 +66,38 @@ it('searches', () => {
   wrapper.prop<Function>('onInputChange')('foo');
   expect(onSearch).not.toBeCalled();
 });
+
+it('should render option correctly', () => {
+  const wrapper = shallowRender();
+  const OptionRenderer = wrapper.instance().optionRenderer;
+  expect(
+    shallow(<OptionRenderer {...mockReactSelectOptionProps({ value: 'test', name: 'name' })} />)
+  ).toMatchSnapshot('option renderer');
+});
+
+it('should render value correctly', () => {
+  const wrapper = shallowRender();
+  const ValueRenderer = wrapper.instance().singleValueRenderer;
+  expect(
+    shallow(<ValueRenderer {...mockReactSelectOptionProps({ value: 'test', name: 'name' })} />)
+  ).toMatchSnapshot('value renderer');
+});
+
+it('should render control correctly', () => {
+  const wrapper = shallowRender();
+  const ControlRenderer = wrapper.instance().controlRenderer;
+  expect(shallow(<ControlRenderer {...mockReactSelectControlProps()} />)).toMatchSnapshot(
+    'control renderer'
+  );
+});
+
+function shallowRender(overrides: Partial<ProfilePermissionsFormSelect['props']> = {}) {
+  return shallow<ProfilePermissionsFormSelect>(
+    <ProfilePermissionsFormSelect
+      onChange={jest.fn()}
+      onSearch={jest.fn(() => Promise.resolve([]))}
+      selected={{ name: 'lambda' }}
+      {...overrides}
+    />
+  );
+}
index d1e22c888d76b0a45df8e706eea4a41b6034badc..c62edcee6cb93432d823407371ee9dca6319712e 100644 (file)
@@ -27,14 +27,18 @@ exports[`should render correctly 1`] = `
         className="modal-field"
       >
         <label
-          htmlFor="change-profile-parent"
+          htmlFor="change-profile-parent-input"
         >
           quality_profiles.parent
           <MandatoryFieldMarker />
         </label>
-        <SelectLegacy
-          clearable={false}
+        <Select
+          autoFocus={true}
+          className="width-100"
           id="change-profile-parent"
+          inputId="change-profile-parent-input"
+          isClearable={false}
+          isSearchable={true}
           name="parentKey"
           onChange={[Function]}
           options={
@@ -61,7 +65,14 @@ exports[`should render correctly 1`] = `
               },
             ]
           }
-          value=""
+          value={
+            Array [
+              Object {
+                "label": "none",
+                "value": "",
+              },
+            ]
+          }
         />
       </div>
     </div>
index 8db35b9f7ee0c30272fdb27c84d0aaa2395a709b..f76052be05dfc22bf108179c3afc7b195980043b 100644 (file)
@@ -21,7 +21,9 @@ exports[`correctly adds groups 1`] = `
       <div
         className="modal-field"
       >
-        <label>
+        <label
+          htmlFor="change-profile-permission-input"
+        >
           quality_profiles.search_description
         </label>
         <ProfilePermissionsFormSelect
@@ -79,7 +81,9 @@ exports[`correctly adds users 1`] = `
       <div
         className="modal-field"
       >
-        <label>
+        <label
+          htmlFor="change-profile-permission-input"
+        >
           quality_profiles.search_description
         </label>
         <ProfilePermissionsFormSelect
@@ -139,7 +143,9 @@ exports[`should render correctly: default 1`] = `
       <div
         className="modal-field"
       >
-        <label>
+        <label
+          htmlFor="change-profile-permission-input"
+        >
           quality_profiles.search_description
         </label>
         <ProfilePermissionsFormSelect
@@ -187,7 +193,9 @@ exports[`should render correctly: submitting 1`] = `
       <div
         className="modal-field"
       >
-        <label>
+        <label
+          htmlFor="change-profile-permission-input"
+        >
           quality_profiles.search_description
         </label>
         <ProfilePermissionsFormSelect
index 45609ea0bc489d93828b05a5bb6bc2482b05a661..1d8007c3944105f17969e7ceeced0da5e6f744f8 100644 (file)
@@ -1,20 +1,89 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`renders 1`] = `
-<SelectLegacy
+<Select
   autoFocus={true}
-  className="Select-big"
-  clearable={false}
+  className="Select-big width-100"
+  components={
+    Object {
+      "Control": [Function],
+      "Option": [Function],
+      "SingleValue": [Function],
+    }
+  }
   filterOptions={[Function]}
+  id="change-profile-permission"
+  inputId="change-profile-permission-input"
+  isClearable={false}
   isLoading={true}
-  noResultsText="no_results"
+  isSearchable={true}
+  noOptionsMessage={[Function]}
   onChange={[MockFunction]}
   onInputChange={[Function]}
-  optionRenderer={[Function]}
-  options={Array []}
+  options={
+    Array [
+      Object {
+        "name": "lambda",
+        "value": "group:lambda",
+      },
+    ]
+  }
   placeholder=""
-  searchable={true}
-  value="group:lambda"
-  valueRenderer={[Function]}
+  value={
+    Array [
+      Object {
+        "name": "lambda",
+        "value": "group:lambda",
+      },
+    ]
+  }
 />
 `;
+
+exports[`should render control correctly: control renderer 1`] = `
+<Control
+  className="abs-height-100 Select-control"
+/>
+`;
+
+exports[`should render option correctly: option renderer 1`] = `
+<Option
+  className="Select-option"
+  data={
+    Object {
+      "name": "name",
+      "value": "test",
+    }
+  }
+>
+  <GroupIcon
+    size={16}
+  />
+  <strong
+    className="spacer-left"
+  >
+    name
+  </strong>
+</Option>
+`;
+
+exports[`should render value correctly: value renderer 1`] = `
+<SingleValue
+  className="Select-value-label"
+  data={
+    Object {
+      "name": "name",
+      "value": "test",
+    }
+  }
+>
+  <GroupIcon
+    size={16}
+  />
+  <strong
+    className="spacer-left"
+  >
+    name
+  </strong>
+</SingleValue>
+`;
index 880e553f8ff1a4abadd215e38e512d5e255be85e..f364f007cbf94769212880ce888fd38fa87e6ea3 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { sortBy } from 'lodash';
 import * as React from 'react';
+import Select from '../../../components/controls/Select';
 import {
   changeProfileParent,
   createQualityProfile,
@@ -26,7 +27,6 @@ import {
 } from '../../../api/quality-profiles';
 import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import Modal from '../../../components/controls/Modal';
-import SelectLegacy from '../../../components/controls/SelectLegacy';
 import { Location } from '../../../components/hoc/withRouter';
 import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
@@ -137,6 +137,17 @@ export default class CreateProfileForm extends React.PureComponent<Props, State>
         }))
       ];
     }
+    const languagesOptions = languages.map(l => ({
+      label: l.name,
+      value: l.key
+    }));
+
+    const isParentProfileClearable = () => {
+      if (this.state.parent !== undefined && this.state.parent !== '') {
+        return true;
+      }
+      return false;
+    };
 
     return (
       <Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
@@ -170,34 +181,39 @@ export default class CreateProfileForm extends React.PureComponent<Props, State>
                 />
               </div>
               <div className="modal-field">
-                <label htmlFor="create-profile-language">
+                <label htmlFor="create-profile-language-input">
                   {translate('language')}
                   <MandatoryFieldMarker />
                 </label>
-                <SelectLegacy
-                  clearable={false}
+                <Select
+                  className="width-100"
+                  autoFocus={true}
                   id="create-profile-language"
+                  inputId="create-profile-language-input"
                   name="language"
+                  isClearable={false}
                   onChange={this.handleLanguageChange}
-                  options={languages.map(l => ({
-                    label: l.name,
-                    value: l.key
-                  }))}
-                  value={selectedLanguage}
+                  options={languagesOptions}
+                  isSearchable={true}
+                  value={languagesOptions.filter(o => o.value === selectedLanguage)}
                 />
               </div>
               {selectedLanguage && profiles.length && (
                 <div className="modal-field">
-                  <label htmlFor="create-profile-parent">
+                  <label htmlFor="create-profile-parent-input">
                     {translate('quality_profiles.parent')}
                   </label>
-                  <SelectLegacy
-                    clearable={true}
+                  <Select
+                    className="width-100"
+                    autoFocus={true}
                     id="create-profile-parent"
+                    inputId="create-profile-parent-input"
                     name="parentKey"
+                    isClearable={isParentProfileClearable()}
                     onChange={this.handleParentChange}
                     options={profiles}
-                    value={this.state.parent || ''}
+                    isSearchable={true}
+                    value={profiles.filter(o => o.value === (this.state.parent || ''))}
                   />
                 </div>
               )}
index 56cf8682b73b4120f3180166272f0baed4eeae4a..24ba281de89e8679df41e10153b4240a47a0bf83 100644 (file)
@@ -18,7 +18,8 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import SelectLegacy from '../../../components/controls/SelectLegacy';
+import { components, OptionProps } from 'react-select';
+import Select from '../../../components/controls/Select';
 import { Router, withRouter } from '../../../components/hoc/withRouter';
 import { translate } from '../../../helpers/l10n';
 import { getProfilesForLanguagePath, PROFILE_PATH } from '../utils';
@@ -36,6 +37,11 @@ export class ProfilesListHeader extends React.PureComponent<Props> {
     router.replace(!option ? PROFILE_PATH : getProfilesForLanguagePath(option.value));
   };
 
+  optionRenderer = (props: OptionProps<{ value: string }, false>) => (
+    // This class is added for the integration test.
+    <components.Option {...props} className="Select-option" />
+  );
+
   render() {
     const { currentFilter, languages } = this.props;
     if (languages.length < 2) {
@@ -47,17 +53,24 @@ export class ProfilesListHeader extends React.PureComponent<Props> {
       value: language.key
     }));
 
-    const currentLanguage = currentFilter && options.find(l => l.value === currentFilter);
-
     return (
       <header className="quality-profiles-list-header clearfix">
-        <span className="spacer-right">{translate('quality_profiles.filter_by')}:</span>
-        <SelectLegacy
+        <label htmlFor="quality-profiles-filter-input" className="spacer-right">
+          {translate('quality_profiles.filter_by')}:
+        </label>
+        <Select
           className="input-medium"
-          clearable={true}
+          autoFocus={true}
+          id="quality-profiles-filter"
+          inputId="quality-profiles-filter-input"
+          isClearable={true}
           onChange={this.handleChange}
+          components={{
+            Option: this.optionRenderer
+          }}
           options={options}
-          value={currentLanguage}
+          isSearchable={true}
+          value={options.filter(o => o.value === currentFilter)}
         />
       </header>
     );
index 67a1d8a0cf5e746f287465c298eb149b0020dbd9..b44c5630ac989579d71a50b4a9b43a8e92b070fb 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { mockReactSelectOptionProps } from '../../../../helpers/mocks/react-select';
 import { mockRouter } from '../../../../helpers/testMocks';
 import { ProfilesListHeader } from '../ProfilesListHeader';
 
@@ -27,8 +28,16 @@ it('should render correctly', () => {
   expect(wrapper).toMatchSnapshot();
 });
 
+it('should render option correctly', () => {
+  const wrapper = shallowRender();
+  const OptionRendererer = wrapper.instance().optionRenderer;
+  expect(
+    shallow(<OptionRendererer {...mockReactSelectOptionProps({ value: 'val' })} />)
+  ).toMatchSnapshot('option renderer');
+});
+
 function shallowRender(props: Partial<ProfilesListHeader['props']> = {}) {
-  return shallow(
+  return shallow<ProfilesListHeader>(
     <ProfilesListHeader
       languages={[
         { key: 'js', name: 'JavaScript' },
index 9dd1840dadb8d65658d4e7e1e4447493cf7fedbf..02f89e687ff012d5fb1db036dd84631ee86a5435 100644 (file)
@@ -48,14 +48,18 @@ exports[`should render correctly: default 1`] = `
         className="modal-field"
       >
         <label
-          htmlFor="create-profile-language"
+          htmlFor="create-profile-language-input"
         >
           language
           <MandatoryFieldMarker />
         </label>
-        <SelectLegacy
-          clearable={false}
+        <Select
+          autoFocus={true}
+          className="width-100"
           id="create-profile-language"
+          inputId="create-profile-language-input"
+          isClearable={false}
+          isSearchable={true}
           name="language"
           onChange={[Function]}
           options={
@@ -70,20 +74,31 @@ exports[`should render correctly: default 1`] = `
               },
             ]
           }
-          value="css"
+          value={
+            Array [
+              Object {
+                "label": "CSS",
+                "value": "css",
+              },
+            ]
+          }
         />
       </div>
       <div
         className="modal-field"
       >
         <label
-          htmlFor="create-profile-parent"
+          htmlFor="create-profile-parent-input"
         >
           quality_profiles.parent
         </label>
-        <SelectLegacy
-          clearable={true}
+        <Select
+          autoFocus={true}
+          className="width-100"
           id="create-profile-parent"
+          inputId="create-profile-parent-input"
+          isClearable={false}
+          isSearchable={true}
           name="parentKey"
           onChange={[Function]}
           options={
@@ -98,7 +113,14 @@ exports[`should render correctly: default 1`] = `
               },
             ]
           }
-          value=""
+          value={
+            Array [
+              Object {
+                "label": "none",
+                "value": "",
+              },
+            ]
+          }
         />
       </div>
       <input
@@ -175,14 +197,18 @@ exports[`should render correctly: with query filter 1`] = `
         className="modal-field"
       >
         <label
-          htmlFor="create-profile-language"
+          htmlFor="create-profile-language-input"
         >
           language
           <MandatoryFieldMarker />
         </label>
-        <SelectLegacy
-          clearable={false}
+        <Select
+          autoFocus={true}
+          className="width-100"
           id="create-profile-language"
+          inputId="create-profile-language-input"
+          isClearable={false}
+          isSearchable={true}
           name="language"
           onChange={[Function]}
           options={
@@ -197,20 +223,31 @@ exports[`should render correctly: with query filter 1`] = `
               },
             ]
           }
-          value="js"
+          value={
+            Array [
+              Object {
+                "label": "JavaScript",
+                "value": "js",
+              },
+            ]
+          }
         />
       </div>
       <div
         className="modal-field"
       >
         <label
-          htmlFor="create-profile-parent"
+          htmlFor="create-profile-parent-input"
         >
           quality_profiles.parent
         </label>
-        <SelectLegacy
-          clearable={true}
+        <Select
+          autoFocus={true}
+          className="width-100"
           id="create-profile-parent"
+          inputId="create-profile-parent-input"
+          isClearable={false}
+          isSearchable={true}
           name="parentKey"
           onChange={[Function]}
           options={
@@ -225,7 +262,14 @@ exports[`should render correctly: with query filter 1`] = `
               },
             ]
           }
-          value=""
+          value={
+            Array [
+              Object {
+                "label": "none",
+                "value": "",
+              },
+            ]
+          }
         />
       </div>
       <div
index 951c3ef3e312a371b4750c0ebabf5c59f3d5d044..ea74d1544c7e34ab013d867e09b2e317ca349a80 100644 (file)
@@ -4,15 +4,25 @@ exports[`should render correctly 1`] = `
 <header
   className="quality-profiles-list-header clearfix"
 >
-  <span
+  <label
     className="spacer-right"
+    htmlFor="quality-profiles-filter-input"
   >
     quality_profiles.filter_by
     :
-  </span>
-  <SelectLegacy
+  </label>
+  <Select
+    autoFocus={true}
     className="input-medium"
-    clearable={true}
+    components={
+      Object {
+        "Option": [Function],
+      }
+    }
+    id="quality-profiles-filter"
+    inputId="quality-profiles-filter-input"
+    isClearable={true}
+    isSearchable={true}
     onChange={[Function]}
     options={
       Array [
@@ -26,6 +36,18 @@ exports[`should render correctly 1`] = `
         },
       ]
     }
+    value={Array []}
   />
 </header>
 `;
+
+exports[`should render option correctly: option renderer 1`] = `
+<Option
+  className="Select-option"
+  data={
+    Object {
+      "value": "val",
+    }
+  }
+/>
+`;
index 131050a156abc9ee2f41cfd1cd60b7cb3c26a56d..e8b963ef9183b3966e096fc9f9ef5986887bde79 100644 (file)
@@ -22,6 +22,7 @@ import * as React from 'react';
 import ReactSelect, { GroupTypeBase, IndicatorProps, Props, StylesConfig } from 'react-select';
 import { MultiValueRemoveProps } from 'react-select/src/components/MultiValue';
 import { colors, others, sizes, zIndexes } from '../../app/theme';
+import { ClearButton } from './buttons';
 
 const ArrowSpan = styled.span`
   border-color: #999 transparent transparent;
@@ -32,30 +33,43 @@ const ArrowSpan = styled.span`
   width: 0;
 `;
 
-export default function Select<
+export default class Select<
   Option,
   IsMulti extends boolean = false,
   Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
->(props: Props<Option, IsMulti, Group>) {
-  function DropdownIndicator({ innerProps }: IndicatorProps<Option, IsMulti, Group>) {
+> extends React.PureComponent<Props<Option, IsMulti, Group>> {
+  dropdownIndicator({ innerProps }: IndicatorProps<Option, IsMulti, Group>) {
     return <ArrowSpan {...innerProps} />;
   }
 
-  function MultiValueRemove(props: MultiValueRemoveProps<Option, Group>) {
+  clearIndicator({ 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>;
   }
 
-  return (
-    <ReactSelect
-      {...props}
-      styles={selectStyle<Option, IsMulti, Group>()}
-      components={{
-        ...props.components,
-        DropdownIndicator,
-        MultiValueRemove
-      }}
-    />
-  );
+  render() {
+    return (
+      <ReactSelect
+        {...this.props}
+        styles={selectStyle<Option, IsMulti, Group>()}
+        components={{
+          ...this.props.components,
+          DropdownIndicator: this.dropdownIndicator,
+          ClearIndicator: this.clearIndicator,
+          MultiValueRemove: this.multiValueRemove
+        }}
+      />
+    );
+  }
 }
 
 export function selectStyle<
@@ -74,7 +88,7 @@ export function selectStyle<
     }),
     control: () => ({
       position: 'relative',
-      display: 'table',
+      display: 'flex',
       width: '100%',
       minHeight: `${sizes.controlHeight}`,
       lineHeight: `calc(${sizes.controlHeight} - 2px)`,
@@ -101,9 +115,6 @@ export function selectStyle<
       textOverflow: 'ellipsis',
       whiteSpace: 'nowrap'
     }),
-    input: () => ({
-      paddingLeft: '1px'
-    }),
     valueContainer: (_provided, state) => {
       if (state.hasValue && state.isMulti) {
         return {
@@ -128,13 +139,13 @@ export function selectStyle<
       };
     },
     indicatorsContainer: () => ({
-      cursor: 'pointer',
-      display: 'table-cell',
       position: 'relative',
-      textAlign: 'center',
+      cursor: 'pointer',
+      textAlign: 'end',
       verticalAlign: 'middle',
       width: '20px',
-      paddingRight: '5px'
+      paddingRight: '5px',
+      flex: 1
     }),
     multiValue: () => ({
       display: 'inline-block',
@@ -206,6 +217,23 @@ export function selectStyle<
       whiteSpace: 'nowrap',
       overflow: 'hidden',
       textOverflow: 'ellipsis'
+    }),
+    input: () => ({
+      padding: '0px',
+      margin: '0px',
+      height: '100%',
+      display: 'flex',
+      alignItems: 'center',
+      paddingLeft: '1px'
+    }),
+    loadingIndicator: () => ({
+      position: 'absolute',
+      padding: '8px',
+      fontSize: '4px'
+    }),
+    noOptionsMessage: () => ({
+      color: `${colors.gray60}`,
+      padding: '8px 10px'
     })
   };
 }
index 785a1b66b1e65f29e679585ea0810d75a33d3fe7..57a924e18bbe43cb20dd76ceb846c895ab2bc683 100644 (file)
@@ -19,7 +19,8 @@
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { GroupTypeBase, Props } from 'react-select';
+import { components, GroupTypeBase, InputProps, Props } from 'react-select';
+import { mockReactSelectIndicatorProps } from '../../../helpers/mocks/react-select';
 import Select from '../Select';
 
 describe('Select', () => {
@@ -27,11 +28,40 @@ describe('Select', () => {
     expect(shallowRender()).toMatchSnapshot('default');
   });
 
+  it('should render complex select component', () => {
+    const inputRenderer = (props: InputProps) => (
+      <components.Input {...props} className={`little-spacer-top ${props.className}`} />
+    );
+
+    const props = {
+      isClearable: true,
+      isLoading: true,
+      components: {
+        Input: inputRenderer
+      }
+    };
+    expect(shallowRender(props)).toMatchSnapshot('other props');
+  });
+
+  it('should render clearIndicator correctly', () => {
+    const wrapper = shallowRender();
+    const ClearIndicator = wrapper.instance().clearIndicator;
+    const clearIndicator = shallow(<ClearIndicator {...mockReactSelectIndicatorProps()} />);
+    expect(clearIndicator).toBeDefined();
+  });
+
+  it('should render dropdownIndicator correctly', () => {
+    const wrapper = shallowRender();
+    const DropdownIndicator = wrapper.instance().dropdownIndicator;
+    const clearIndicator = shallow(<DropdownIndicator {...mockReactSelectIndicatorProps()} />);
+    expect(clearIndicator).toBeDefined();
+  });
+
   function shallowRender<
     Option,
     IsMulti extends boolean = false,
     Group extends GroupTypeBase<Option> = GroupTypeBase<Option>
   >(props: Partial<Props<Option, IsMulti, Group>> = {}) {
-    return shallow<Props<Option, IsMulti, Group>>(<Select {...props} />);
+    return shallow<Select<Option, IsMulti, Group>>(<Select {...props} />);
   }
 });
index e0a4f475588f2d19dd5775dfea09b4c102c3c2ff..a8ecb8b67ffd0d8e92f6cd0dcb978f1af919327f 100644 (file)
@@ -1,9 +1,47 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`Select should render complex select component: other props 1`] = `
+<StateManager
+  components={
+    Object {
+      "ClearIndicator": [Function],
+      "DropdownIndicator": [Function],
+      "Input": [Function],
+      "MultiValueRemove": [Function],
+    }
+  }
+  defaultInputValue=""
+  defaultMenuIsOpen={false}
+  defaultValue={null}
+  isClearable={true}
+  isLoading={true}
+  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],
+    }
+  }
+/>
+`;
+
 exports[`Select should render correctly: default 1`] = `
 <StateManager
   components={
     Object {
+      "ClearIndicator": [Function],
       "DropdownIndicator": [Function],
       "MultiValueRemove": [Function],
     }
@@ -17,11 +55,13 @@ exports[`Select should render correctly: default 1`] = `
       "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],
index d28479fefc45d7e2be94bc2b28cc8d8a756cb9ad..05f49106ed496e089fa435cc8417f8a06f69297f 100644 (file)
@@ -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 { GroupTypeBase, OptionProps } from 'react-select';
+import { ControlProps, GroupTypeBase, IndicatorProps, InputProps, OptionProps } from 'react-select';
 
 export function mockReactSelectOptionProps<
   OptionType,
@@ -32,3 +32,23 @@ export function mockReactSelectOptionProps<
     data
   } as OptionProps<OptionType, IsMulti, GroupType>;
 }
+
+export function mockReactSelectInputProps(): InputProps {
+  return {} as InputProps;
+}
+
+export function mockReactSelectControlProps<
+  OptionType,
+  IsMulti extends boolean,
+  GroupType extends GroupTypeBase<OptionType> = GroupTypeBase<OptionType>
+>(): ControlProps<OptionType, IsMulti, GroupType> {
+  return {} as ControlProps<OptionType, IsMulti, GroupType>;
+}
+
+export function mockReactSelectIndicatorProps<
+  OptionType,
+  IsMulti extends boolean,
+  GroupType extends GroupTypeBase<OptionType> = GroupTypeBase<OptionType>
+>(): IndicatorProps<OptionType, IsMulti, GroupType> {
+  return {} as IndicatorProps<OptionType, IsMulti, GroupType>;
+}