]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16088 Migrate SelectLegacy in projectQualityGate and projectQualityProfile
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 29 Mar 2022 16:09:24 +0000 (18:09 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 31 Mar 2022 20:02:58 +0000 (20:02 +0000)
16 files changed:
server/sonar-web/src/main/js/apps/projectQualityGate/ProjectQualityGateAppRenderer.tsx
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/ProjectQualityGateAppRenderer-test.tsx
server/sonar-web/src/main/js/apps/projectQualityGate/__tests__/__snapshots__/ProjectQualityGateAppRenderer-test.tsx.snap
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/AddLanguageModal.tsx
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/LanguageProfileSelectOption.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/SetQualityProfileModal.tsx
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/AddLanguageModal-test.tsx
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/LanguageProfileSelectOption-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/SetQualityProfileModal-test.tsx
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/AddLanguageModal-test.tsx.snap
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/LanguageProfileSelectOption-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/SetQualityProfileModal-test.tsx.snap
server/sonar-web/src/main/js/components/common/DisableableSelectOption.tsx
server/sonar-web/src/main/js/components/common/__tests__/DisableableSelectOption-test.tsx
server/sonar-web/src/main/js/components/controls/Select.tsx
server/sonar-web/src/main/js/helpers/mocks/quality-profiles.ts [new file with mode: 0644]

index 15cb243a3495654b63bf842b7068356a6e29b05c..d765f530a84e1b8381bb36d652a77e657426fd89 100644 (file)
@@ -21,13 +21,14 @@ import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import { FormattedMessage } from 'react-intl';
 import { Link } from 'react-router';
+import { components, OptionProps } from 'react-select';
 import A11ySkipTarget from '../../app/components/a11y/A11ySkipTarget';
 import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
 import DisableableSelectOption from '../../components/common/DisableableSelectOption';
 import { SubmitButton } from '../../components/controls/buttons';
 import HelpTooltip from '../../components/controls/HelpTooltip';
 import Radio from '../../components/controls/Radio';
-import SelectLegacy from '../../components/controls/SelectLegacy';
+import Select, { BasicSelectOption } from '../../components/controls/Select';
 import { Alert } from '../../components/ui/Alert';
 import { translate } from '../../helpers/l10n';
 import { isDiffMetric } from '../../helpers/measures';
@@ -49,6 +50,37 @@ function hasConditionOnNewCode(qualityGate: QualityGate): boolean {
   return !!qualityGate.conditions?.some(condition => isDiffMetric(condition.metric));
 }
 
+interface QualityGateOption extends BasicSelectOption {
+  isDisabled: boolean;
+}
+
+function renderQualitygateOption(props: OptionProps<QualityGateOption, false>) {
+  return (
+    <components.Option {...props}>
+      <div>
+        <DisableableSelectOption
+          className="abs-width-100"
+          option={props.data}
+          disabledReason={translate('project_quality_gate.no_condition.reason')}
+          disableTooltipOverlay={() => (
+            <FormattedMessage
+              id="project_quality_gate.no_condition"
+              defaultMessage={translate('project_quality_gate.no_condition')}
+              values={{
+                link: (
+                  <Link to={{ pathname: `/quality_gates/show/${props.data.value}` }}>
+                    {translate('project_quality_gate.no_condition.link')}
+                  </Link>
+                )
+              }}
+            />
+          )}
+        />
+      </div>
+    </components.Option>
+  );
+}
+
 export default function ProjectQualityGateAppRenderer(props: ProjectQualityGateAppRendererProps) {
   const { allQualityGates, currentQualityGate, loading, selectedQualityGateId, submitting } = props;
   const defaultQualityGate = allQualityGates?.find(g => g.isDefault);
@@ -74,8 +106,8 @@ export default function ProjectQualityGateAppRenderer(props: ProjectQualityGateA
 
   const selectedQualityGate = allQualityGates.find(qg => qg.id === selectedQualityGateId);
 
-  const options = allQualityGates.map(g => ({
-    disabled: g.conditions === undefined || g.conditions.length === 0,
+  const options: QualityGateOption[] = allQualityGates.map(g => ({
+    isDisabled: g.conditions === undefined || g.conditions.length === 0,
     label: g.name,
     value: g.id
   }));
@@ -140,40 +172,29 @@ export default function ProjectQualityGateAppRenderer(props: ProjectQualityGateA
               className="display-flex-start"
               checked={!usesDefault}
               disabled={submitting}
-              onCheck={value => props.onSelect(value)}
+              onCheck={value => {
+                if (usesDefault) {
+                  props.onSelect(value);
+                }
+              }}
               value={!usesDefault ? selectedQualityGateId : currentQualityGate.id}>
               <div className="spacer-left">
                 <div className="little-spacer-bottom">
                   {translate('project_quality_gate.always_use_specific')}
                 </div>
                 <div className="display-flex-center">
-                  <SelectLegacy
-                    className="abs-width-300"
-                    clearable={false}
-                    disabled={submitting || usesDefault}
-                    onChange={({ value }: { value: string }) => props.onSelect(value)}
+                  <Select
+                    className="abs-width-300 it__project-quality-gate-select"
+                    components={{
+                      Option: renderQualitygateOption
+                    }}
+                    isClearable={usesDefault}
+                    isDisabled={submitting || usesDefault}
+                    onChange={({ value }: QualityGateOption) => {
+                      props.onSelect(value);
+                    }}
                     options={options}
-                    optionRenderer={option => (
-                      <DisableableSelectOption
-                        className="abs-width-100"
-                        option={option}
-                        disabledReason={translate('project_quality_gate.no_condition.reason')}
-                        disableTooltipOverlay={() => (
-                          <FormattedMessage
-                            id="project_quality_gate.no_condition"
-                            defaultMessage={translate('project_quality_gate.no_condition')}
-                            values={{
-                              link: (
-                                <Link to={{ pathname: `/quality_gates/show/${option.value}` }}>
-                                  {translate('project_quality_gate.no_condition.link')}
-                                </Link>
-                              )
-                            }}
-                          />
-                        )}
-                      />
-                    )}
-                    value={selectedQualityGateId}
+                    value={options.find(o => o.value === selectedQualityGateId)}
                   />
                 </div>
               </div>
index 190b3ccde02b6b4e2ed9042d462802ac566ee4b1..bbcf0ae7e0e2ef42af28dba26442dcaaad709027 100644 (file)
@@ -20,7 +20,7 @@
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import Radio from '../../../components/controls/Radio';
-import SelectLegacy from '../../../components/controls/SelectLegacy';
+import Select from '../../../components/controls/Select';
 import { mockQualityGate } from '../../../helpers/mocks/quality-gates';
 import { mockCondition } from '../../../helpers/testMocks';
 import { submit } from '../../../helpers/testUtils';
@@ -57,11 +57,11 @@ it('should render correctly', () => {
 it('should render select options correctly', () => {
   return new Promise<void>(resolve => {
     const wrapper = shallowRender();
-    const render = wrapper.find(SelectLegacy).props().optionRenderer;
+    const render = wrapper.find(Select).props().components.Option;
 
     expect(render).toBeDefined();
 
-    expect(render!({ value: '1', label: 'Gate 1' })).toMatchSnapshot('default');
+    expect(render({ data: { value: '1', label: 'Gate 1' } })).toMatchSnapshot('default');
     resolve();
   });
 });
@@ -87,7 +87,7 @@ it('should correctly handle changes', () => {
     .onCheck('1');
   expect(onSelect).toHaveBeenLastCalledWith('1');
 
-  wrapper.find(SelectLegacy).props().onChange!({ value: '2' });
+  wrapper.find(Select).props().onChange!({ value: '2' });
   expect(onSelect).toHaveBeenLastCalledWith('2');
 });
 
index 6eafb239034043854862d661f905a994bf768555..2d639fb2fd966b3341d06d680b22f740ccc6c6dd 100644 (file)
@@ -108,32 +108,35 @@ exports[`should render correctly: always use system default 1`] = `
             <div
               className="display-flex-center"
             >
-              <SelectLegacy
-                className="abs-width-300"
-                clearable={false}
-                disabled={true}
+              <Select
+                className="abs-width-300 it__project-quality-gate-select"
+                components={
+                  Object {
+                    "Option": [Function],
+                  }
+                }
+                isClearable={true}
+                isDisabled={true}
                 onChange={[Function]}
-                optionRenderer={[Function]}
                 options={
                   Array [
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "1",
                     },
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "2",
                     },
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "3",
                     },
                   ]
                 }
-                value="-1"
               />
             </div>
           </div>
@@ -259,32 +262,42 @@ exports[`should render correctly: default 1`] = `
             <div
               className="display-flex-center"
             >
-              <SelectLegacy
-                className="abs-width-300"
-                clearable={false}
-                disabled={false}
+              <Select
+                className="abs-width-300 it__project-quality-gate-select"
+                components={
+                  Object {
+                    "Option": [Function],
+                  }
+                }
+                isClearable={false}
+                isDisabled={false}
                 onChange={[Function]}
-                optionRenderer={[Function]}
                 options={
                   Array [
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "1",
                     },
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "2",
                     },
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "3",
                     },
                   ]
                 }
-                value="1"
+                value={
+                  Object {
+                    "isDisabled": false,
+                    "label": "qualitygate",
+                    "value": "1",
+                  }
+                }
               />
             </div>
           </div>
@@ -416,32 +429,42 @@ exports[`should render correctly: show new code warning 1`] = `
             <div
               className="display-flex-center"
             >
-              <SelectLegacy
-                className="abs-width-300"
-                clearable={false}
-                disabled={false}
+              <Select
+                className="abs-width-300 it__project-quality-gate-select"
+                components={
+                  Object {
+                    "Option": [Function],
+                  }
+                }
+                isClearable={false}
+                isDisabled={false}
                 onChange={[Function]}
-                optionRenderer={[Function]}
                 options={
                   Array [
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "1",
                     },
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "2",
                     },
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "3",
                     },
                   ]
                 }
-                value="3"
+                value={
+                  Object {
+                    "isDisabled": false,
+                    "label": "qualitygate",
+                    "value": "3",
+                  }
+                }
               />
             </div>
           </div>
@@ -597,32 +620,35 @@ exports[`should render correctly: show warning 1`] = `
             <div
               className="display-flex-center"
             >
-              <SelectLegacy
-                className="abs-width-300"
-                clearable={false}
-                disabled={false}
+              <Select
+                className="abs-width-300 it__project-quality-gate-select"
+                components={
+                  Object {
+                    "Option": [Function],
+                  }
+                }
+                isClearable={false}
+                isDisabled={false}
                 onChange={[Function]}
-                optionRenderer={[Function]}
                 options={
                   Array [
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "1",
                     },
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "2",
                     },
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "3",
                     },
                   ]
                 }
-                value="5"
               />
             </div>
           </div>
@@ -754,32 +780,35 @@ exports[`should render correctly: show warning if not using default 1`] = `
             <div
               className="display-flex-center"
             >
-              <SelectLegacy
-                className="abs-width-300"
-                clearable={false}
-                disabled={true}
+              <Select
+                className="abs-width-300 it__project-quality-gate-select"
+                components={
+                  Object {
+                    "Option": [Function],
+                  }
+                }
+                isClearable={true}
+                isDisabled={true}
                 onChange={[Function]}
-                optionRenderer={[Function]}
                 options={
                   Array [
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "1",
                     },
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "2",
                     },
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "3",
                     },
                   ]
                 }
-                value="-1"
               />
             </div>
           </div>
@@ -911,32 +940,42 @@ exports[`should render correctly: submitting 1`] = `
             <div
               className="display-flex-center"
             >
-              <SelectLegacy
-                className="abs-width-300"
-                clearable={false}
-                disabled={true}
+              <Select
+                className="abs-width-300 it__project-quality-gate-select"
+                components={
+                  Object {
+                    "Option": [Function],
+                  }
+                }
+                isClearable={false}
+                isDisabled={true}
                 onChange={[Function]}
-                optionRenderer={[Function]}
                 options={
                   Array [
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "1",
                     },
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "2",
                     },
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "qualitygate",
                       "value": "3",
                     },
                   ]
                 }
-                value="1"
+                value={
+                  Object {
+                    "isDisabled": false,
+                    "label": "qualitygate",
+                    "value": "1",
+                  }
+                }
               />
             </div>
           </div>
@@ -958,15 +997,26 @@ exports[`should render correctly: submitting 1`] = `
 `;
 
 exports[`should render select options correctly: default 1`] = `
-<DisableableSelectOption
-  className="abs-width-100"
-  disableTooltipOverlay={[Function]}
-  disabledReason="project_quality_gate.no_condition.reason"
-  option={
+<Option
+  data={
     Object {
       "label": "Gate 1",
       "value": "1",
     }
   }
-/>
+>
+  <div>
+    <DisableableSelectOption
+      className="abs-width-100"
+      disableTooltipOverlay={[Function]}
+      disabledReason="project_quality_gate.no_condition.reason"
+      option={
+        Object {
+          "label": "Gate 1",
+          "value": "1",
+        }
+      }
+    />
+  </div>
+</Option>
 `;
index 84b26dcef781b8d3bd543d4c3aaff5d2ec490001..dea0dea79232fed1893b8e64855aefd8a50e30ea 100644 (file)
  */
 import { difference } from 'lodash';
 import * as React from 'react';
-import { Link } from 'react-router';
 import { Profile } from '../../../api/quality-profiles';
 import withLanguagesContext from '../../../app/components/languages/withLanguagesContext';
-import DisableableSelectOption from '../../../components/common/DisableableSelectOption';
 import { ButtonLink, SubmitButton } from '../../../components/controls/buttons';
-import SelectLegacy from '../../../components/controls/SelectLegacy';
+import Select, { BasicSelectOption } from '../../../components/controls/Select';
 import SimpleModal from '../../../components/controls/SimpleModal';
 import { translate } from '../../../helpers/l10n';
-import { getQualityProfileUrl } from '../../../helpers/urls';
 import { Languages } from '../../../types/languages';
 import { Dict } from '../../../types/types';
+import LanguageProfileSelectOption, { ProfileOption } from './LanguageProfileSelectOption';
 
 export interface AddLanguageModalProps {
   languages: Languages;
@@ -49,17 +47,18 @@ export function AddLanguageModal(props: AddLanguageModalProps) {
 
   const header = translate('project_quality_profile.add_language_modal.title');
 
-  const languageOptions = difference(
+  const languageOptions: BasicSelectOption[] = difference(
     Object.keys(profilesByLanguage),
     unavailableLanguages
   ).map(l => ({ value: l, label: languages[l].name }));
 
-  const profileOptions =
+  const profileOptions: ProfileOption[] =
     language !== undefined
       ? profilesByLanguage[language].map(p => ({
           value: p.key,
           label: p.name,
-          disabled: p.activeRuleCount === 0
+          language,
+          isDisabled: p.activeRuleCount === 0
         }))
       : [];
 
@@ -86,14 +85,14 @@ export function AddLanguageModal(props: AddLanguageModalProps) {
                     {translate('project_quality_profile.add_language_modal.choose_language')}
                   </label>
                 </div>
-                <SelectLegacy
+                <Select
                   className="abs-width-300"
-                  clearable={false}
-                  disabled={submitting}
+                  isDisabled={submitting}
                   id="language"
-                  onChange={({ value }: { value: string }) => setSelected({ language: value })}
+                  onChange={({ value }: BasicSelectOption) => {
+                    setSelected({ language: value, key: undefined });
+                  }}
                   options={languageOptions}
-                  value={language}
                 />
               </div>
 
@@ -103,38 +102,16 @@ export function AddLanguageModal(props: AddLanguageModalProps) {
                     {translate('project_quality_profile.add_language_modal.choose_profile')}
                   </label>
                 </div>
-                <SelectLegacy
+                <Select
                   className="abs-width-300"
-                  clearable={false}
-                  disabled={submitting || !language}
+                  isDisabled={submitting || !language}
                   id="profiles"
-                  onChange={({ value }: { value: string }) => setSelected({ language, key: value })}
+                  onChange={({ value }: ProfileOption) => setSelected({ language, key: value })}
                   options={profileOptions}
-                  optionRenderer={option => (
-                    <DisableableSelectOption
-                      option={option}
-                      disabledReason={translate(
-                        'project_quality_profile.add_language_modal.no_active_rules'
-                      )}
-                      disableTooltipOverlay={() => (
-                        <>
-                          <p>
-                            {translate(
-                              'project_quality_profile.add_language_modal.profile_unavailable_no_active_rules'
-                            )}
-                          </p>
-                          {option.label && language && (
-                            <Link to={getQualityProfileUrl(option.label, language)}>
-                              {translate(
-                                'project_quality_profile.add_language_modal.go_to_profile'
-                              )}
-                            </Link>
-                          )}
-                        </>
-                      )}
-                    />
-                  )}
-                  value={key}
+                  components={{
+                    Option: LanguageProfileSelectOption
+                  }}
+                  value={profileOptions.find(o => o.value === key) ?? null}
                 />
               </div>
             </div>
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/LanguageProfileSelectOption.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/LanguageProfileSelectOption.tsx
new file mode 100644 (file)
index 0000000..376f4b9
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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 * as React from 'react';
+import { Link } from 'react-router';
+import { components, OptionProps } from 'react-select';
+import DisableableSelectOption from '../../../components/common/DisableableSelectOption';
+import { BasicSelectOption } from '../../../components/controls/Select';
+import { translate } from '../../../helpers/l10n';
+import { getQualityProfileUrl } from '../../../helpers/urls';
+
+export interface ProfileOption extends BasicSelectOption {
+  language: string;
+  isDisabled: boolean;
+}
+
+export type LanguageProfileSelectOptionProps = OptionProps<ProfileOption, false>;
+
+export default function LanguageProfileSelectOption(props: LanguageProfileSelectOptionProps) {
+  const option = props.data;
+
+  return (
+    <components.Option {...props}>
+      <div>
+        <DisableableSelectOption
+          option={option}
+          disabledReason={translate('project_quality_profile.add_language_modal.no_active_rules')}
+          disableTooltipOverlay={() => (
+            <>
+              <p>
+                {translate(
+                  'project_quality_profile.add_language_modal.profile_unavailable_no_active_rules'
+                )}
+              </p>
+              {option.label && option.language && (
+                <Link to={getQualityProfileUrl(option.label, option.language)}>
+                  {translate('project_quality_profile.add_language_modal.go_to_profile')}
+                </Link>
+              )}
+            </>
+          )}
+        />
+      </div>
+    </components.Option>
+  );
+}
index c63244bc985dfd87dc7fe731862a518b586fec6e..4bdd151ffaf3af8cf9b18bd995bfff05fef31688 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { Link } from 'react-router';
 import { Profile } from '../../../api/quality-profiles';
-import DisableableSelectOption from '../../../components/common/DisableableSelectOption';
 import { ButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import Radio from '../../../components/controls/Radio';
-import SelectLegacy from '../../../components/controls/SelectLegacy';
+import Select from '../../../components/controls/Select';
 import SimpleModal from '../../../components/controls/SimpleModal';
 import { Alert } from '../../../components/ui/Alert';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
-import { getQualityProfileUrl } from '../../../helpers/urls';
 import { Component } from '../../../types/types';
 import BuiltInQualityProfileBadge from '../../quality-profiles/components/BuiltInQualityProfileBadge';
 import { USE_SYSTEM_DEFAULT } from '../constants';
+import LanguageProfileSelectOption, { ProfileOption } from './LanguageProfileSelectOption';
 
 export interface SetQualityProfileModalProps {
   availableProfiles: Profile[];
@@ -58,10 +56,11 @@ export default function SetQualityProfileModal(props: SetQualityProfileModalProp
     'project_quality_profile.change_lang_X_profile',
     currentProfile.languageName
   );
-  const profileOptions = availableProfiles.map(p => ({
+  const profileOptions: ProfileOption[] = availableProfiles.map(p => ({
     value: p.key,
     label: p.name,
-    disabled: p.activeRuleCount === 0
+    language: currentProfile.language,
+    isDisabled: p.activeRuleCount === 0
   }));
   const hasSelectedSysDefault = selected === USE_SYSTEM_DEFAULT;
   const hasChanged = usesDefault ? !hasSelectedSysDefault : selected !== currentProfile.key;
@@ -120,41 +119,19 @@ export default function SetQualityProfileModal(props: SetQualityProfileModalProp
                       {translate('project_quality_profile.always_use_specific')}
                     </div>
                     <div className="display-flex-center">
-                      <SelectLegacy
+                      <Select
                         className="abs-width-300"
-                        clearable={false}
-                        disabled={submitting || hasSelectedSysDefault}
-                        onChange={({ value }: { value: string }) => setSelected(value)}
+                        isDisabled={submitting || hasSelectedSysDefault}
+                        onChange={({ value }: ProfileOption) => setSelected(value)}
                         options={profileOptions}
-                        optionRenderer={option => (
-                          <DisableableSelectOption
-                            option={option}
-                            disabledReason={translate(
-                              'project_quality_profile.add_language_modal.no_active_rules'
-                            )}
-                            disableTooltipOverlay={() => (
-                              <>
-                                <p>
-                                  {translate(
-                                    'project_quality_profile.add_language_modal.profile_unavailable_no_active_rules'
-                                  )}
-                                </p>
-                                {option.label && (
-                                  <Link
-                                    to={getQualityProfileUrl(
-                                      option.label,
-                                      currentProfile.language
-                                    )}>
-                                    {translate(
-                                      'project_quality_profile.add_language_modal.go_to_profile'
-                                    )}
-                                  </Link>
-                                )}
-                              </>
-                            )}
-                          />
+                        components={{
+                          Option: LanguageProfileSelectOption
+                        }}
+                        value={profileOptions.find(
+                          option =>
+                            option.value ===
+                            (!hasSelectedSysDefault ? selected : currentProfile.key)
                         )}
-                        value={!hasSelectedSysDefault ? selected : currentProfile.key}
                       />
                     </div>
                   </div>
index ca1f7e0aa65fc43570eb1bff9f5696a0c4f13038..f22c24be7ee412a27e41545bc57556b659eec52c 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { shallow, ShallowWrapper } from 'enzyme';
 import * as React from 'react';
-import SelectLegacy from '../../../../components/controls/SelectLegacy';
+import Select from '../../../../components/controls/Select';
 import SimpleModal from '../../../../components/controls/SimpleModal';
 import { mockQualityProfile } from '../../../../helpers/testMocks';
 import { AddLanguageModal, AddLanguageModalProps } from '../AddLanguageModal';
@@ -28,27 +28,6 @@ it('should render correctly', () => {
   expect(diveIntoSimpleModal(shallowRender())).toMatchSnapshot('default');
 });
 
-it('should render select options correctly', () => {
-  return new Promise<void>((resolve, reject) => {
-    const wrapper = shallowRender();
-
-    const langOnChange = getLanguageSelect(wrapper).props().onChange;
-    if (!langOnChange) {
-      reject();
-      return;
-    }
-    langOnChange({ value: 'js' });
-
-    const render = getProfileSelect(wrapper).props().optionRenderer;
-    if (!render) {
-      reject();
-      return;
-    }
-    expect(render({ value: 'bar', label: 'Profile 1' })).toMatchSnapshot('default');
-    resolve();
-  });
-});
-
 it('should correctly handle changes', () => {
   const onSubmit = jest.fn();
   const wrapper = shallowRender({ onSubmit });
@@ -72,7 +51,7 @@ it('should correctly handle changes', () => {
   profileSelect = getProfileSelect(wrapper);
   expect(profileSelect.props().options).toHaveLength(2);
   expect(profileSelect.props().options).toEqual(
-    expect.arrayContaining([expect.objectContaining({ disabled: true })])
+    expect.arrayContaining([expect.objectContaining({ isDisabled: true })])
   );
 
   // Choose 1 profile.
@@ -95,13 +74,13 @@ function diveIntoSimpleModal(wrapper: ShallowWrapper) {
 
 function getLanguageSelect(wrapper: ShallowWrapper) {
   return diveIntoSimpleModal(wrapper)
-    .find(SelectLegacy)
+    .find(Select)
     .at(0);
 }
 
 function getProfileSelect(wrapper: ShallowWrapper) {
   return diveIntoSimpleModal(wrapper)
-    .find(SelectLegacy)
+    .find(Select)
     .at(1);
 }
 
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/LanguageProfileSelectOption-test.tsx b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/LanguageProfileSelectOption-test.tsx
new file mode 100644 (file)
index 0000000..06289c3
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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 DisableableSelectOption from '../../../../components/common/DisableableSelectOption';
+import { mockProfileOption } from '../../../../helpers/mocks/quality-profiles';
+import LanguageProfileSelectOption, {
+  LanguageProfileSelectOptionProps,
+  ProfileOption
+} from '../LanguageProfileSelectOption';
+
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot('default');
+});
+
+describe('tooltip', () => {
+  it('should render correctly', () => {
+    expect(renderTooltipOverly()).toMatchSnapshot('default');
+    expect(renderTooltipOverly({ label: undefined })).toMatchSnapshot('no link');
+  });
+});
+
+function renderTooltipOverly(overrides: Partial<ProfileOption> = {}) {
+  const wrapper = shallowRender(overrides);
+  const { disableTooltipOverlay } = wrapper.find(DisableableSelectOption).props();
+  return disableTooltipOverlay();
+}
+
+function shallowRender(overrides: Partial<ProfileOption> = {}) {
+  // satisfy the required props that the option actually gets from the select
+  const optionProps = {} as LanguageProfileSelectOptionProps;
+  return shallow(
+    <LanguageProfileSelectOption {...optionProps} data={{ ...mockProfileOption(), ...overrides }} />
+  );
+}
index 3fdd0c8dda4467a070069a0cec3966ee08a7125e..911cf66c1410106c2ea3c0ae38fc39da4a7c157b 100644 (file)
@@ -20,7 +20,7 @@
 import { shallow, ShallowWrapper } from 'enzyme';
 import * as React from 'react';
 import Radio from '../../../../components/controls/Radio';
-import SelectLegacy from '../../../../components/controls/SelectLegacy';
+import Select from '../../../../components/controls/Select';
 import SimpleModal from '../../../../components/controls/SimpleModal';
 import { mockComponent } from '../../../../helpers/mocks/component';
 import { mockQualityProfile } from '../../../../helpers/testMocks';
@@ -32,19 +32,6 @@ it('should render correctly', () => {
   expect(shallowRender({ component: mockComponent() })).toMatchSnapshot('needs reanalysis');
 });
 
-it('should render select options correctly', () => {
-  return new Promise<void>((resolve, reject) => {
-    const wrapper = shallowRender();
-    const render = wrapper.find(SelectLegacy).props().optionRenderer;
-    if (!render) {
-      reject();
-      return;
-    }
-    expect(render({ value: 'bar', label: 'Profile 1' })).toMatchSnapshot('default');
-    resolve();
-  });
-});
-
 it('should correctly handle changes', () => {
   const onSubmit = jest.fn();
   const wrapper = shallowRender({ onSubmit }, false);
@@ -66,7 +53,7 @@ it('should correctly handle changes', () => {
   expect(onSubmit).toHaveBeenLastCalledWith('foo', 'foo');
 
   const change = diveIntoSimpleModal(wrapper)
-    .find(SelectLegacy)
+    .find(Select)
     .props().onChange;
 
   expect(change).toBeDefined();
index 7e6c947970ab8e9385b24d036c639f03b758349f..7e7cc0e443a7795045668a0a4b8dea98012a3416 100644 (file)
@@ -28,11 +28,10 @@ Array [
             project_quality_profile.add_language_modal.choose_language
           </label>
         </div>
-        <SelectLegacy
+        <Select
           className="abs-width-300"
-          clearable={false}
-          disabled={false}
           id="language"
+          isDisabled={false}
           onChange={[Function]}
           options={
             Array [
@@ -61,14 +60,18 @@ Array [
             project_quality_profile.add_language_modal.choose_profile
           </label>
         </div>
-        <SelectLegacy
+        <Select
           className="abs-width-300"
-          clearable={false}
-          disabled={true}
+          components={
+            Object {
+              "Option": [Function],
+            }
+          }
           id="profiles"
+          isDisabled={true}
           onChange={[Function]}
-          optionRenderer={[Function]}
           options={Array []}
+          value={null}
         />
       </div>
     </div>
@@ -90,16 +93,3 @@ Array [
   </form>,
 ]
 `;
-
-exports[`should render select options correctly: default 1`] = `
-<DisableableSelectOption
-  disableTooltipOverlay={[Function]}
-  disabledReason="project_quality_profile.add_language_modal.no_active_rules"
-  option={
-    Object {
-      "label": "Profile 1",
-      "value": "bar",
-    }
-  }
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/LanguageProfileSelectOption-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectQualityProfiles/components/__tests__/__snapshots__/LanguageProfileSelectOption-test.tsx.snap
new file mode 100644 (file)
index 0000000..3c3a2ec
--- /dev/null
@@ -0,0 +1,60 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: default 1`] = `
+<Option
+  data={
+    Object {
+      "isDisabled": false,
+      "label": "Profile 1",
+      "language": "Java",
+      "value": "profile-1",
+    }
+  }
+>
+  <div>
+    <DisableableSelectOption
+      disableTooltipOverlay={[Function]}
+      disabledReason="project_quality_profile.add_language_modal.no_active_rules"
+      option={
+        Object {
+          "isDisabled": false,
+          "label": "Profile 1",
+          "language": "Java",
+          "value": "profile-1",
+        }
+      }
+    />
+  </div>
+</Option>
+`;
+
+exports[`tooltip should render correctly: default 1`] = `
+<React.Fragment>
+  <p>
+    project_quality_profile.add_language_modal.profile_unavailable_no_active_rules
+  </p>
+  <Link
+    onlyActiveOnIndex={false}
+    style={Object {}}
+    to={
+      Object {
+        "pathname": "/profiles/show",
+        "query": Object {
+          "language": "Java",
+          "name": "Profile 1",
+        },
+      }
+    }
+  >
+    project_quality_profile.add_language_modal.go_to_profile
+  </Link>
+</React.Fragment>
+`;
+
+exports[`tooltip should render correctly: no link 1`] = `
+<React.Fragment>
+  <p>
+    project_quality_profile.add_language_modal.profile_unavailable_no_active_rules
+  </p>
+</React.Fragment>
+`;
index 6455bc57976441d20978ee3e3f1287008c72e553..cdca51949dd0dad8f71f03841617fbd426dd8c26 100644 (file)
@@ -68,27 +68,39 @@ Array [
             <div
               className="display-flex-center"
             >
-              <SelectLegacy
+              <Select
                 className="abs-width-300"
-                clearable={false}
-                disabled={false}
+                components={
+                  Object {
+                    "Option": [Function],
+                  }
+                }
+                isDisabled={false}
                 onChange={[Function]}
-                optionRenderer={[Function]}
                 options={
                   Array [
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "name",
+                      "language": "js",
                       "value": "foo",
                     },
                     Object {
-                      "disabled": true,
+                      "isDisabled": true,
                       "label": "name",
+                      "language": "js",
                       "value": "bar",
                     },
                   ]
                 }
-                value="foo"
+                value={
+                  Object {
+                    "isDisabled": false,
+                    "label": "name",
+                    "language": "js",
+                    "value": "foo",
+                  }
+                }
               />
             </div>
           </div>
@@ -182,27 +194,39 @@ Array [
             <div
               className="display-flex-center"
             >
-              <SelectLegacy
+              <Select
                 className="abs-width-300"
-                clearable={false}
-                disabled={true}
+                components={
+                  Object {
+                    "Option": [Function],
+                  }
+                }
+                isDisabled={true}
                 onChange={[Function]}
-                optionRenderer={[Function]}
                 options={
                   Array [
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "name",
+                      "language": "js",
                       "value": "foo",
                     },
                     Object {
-                      "disabled": true,
+                      "isDisabled": true,
                       "label": "name",
+                      "language": "js",
                       "value": "bar",
                     },
                   ]
                 }
-                value="foo"
+                value={
+                  Object {
+                    "isDisabled": false,
+                    "label": "name",
+                    "language": "js",
+                    "value": "foo",
+                  }
+                }
               />
             </div>
           </div>
@@ -296,27 +320,39 @@ Array [
             <div
               className="display-flex-center"
             >
-              <SelectLegacy
+              <Select
                 className="abs-width-300"
-                clearable={false}
-                disabled={false}
+                components={
+                  Object {
+                    "Option": [Function],
+                  }
+                }
+                isDisabled={false}
                 onChange={[Function]}
-                optionRenderer={[Function]}
                 options={
                   Array [
                     Object {
-                      "disabled": false,
+                      "isDisabled": false,
                       "label": "name",
+                      "language": "js",
                       "value": "foo",
                     },
                     Object {
-                      "disabled": true,
+                      "isDisabled": true,
                       "label": "name",
+                      "language": "js",
                       "value": "bar",
                     },
                   ]
                 }
-                value="foo"
+                value={
+                  Object {
+                    "isDisabled": false,
+                    "label": "name",
+                    "language": "js",
+                    "value": "foo",
+                  }
+                }
               />
             </div>
           </div>
@@ -346,16 +382,3 @@ Array [
   </form>,
 ]
 `;
-
-exports[`should render select options correctly: default 1`] = `
-<DisableableSelectOption
-  disableTooltipOverlay={[Function]}
-  disabledReason="project_quality_profile.add_language_modal.no_active_rules"
-  option={
-    Object {
-      "label": "Profile 1",
-      "value": "bar",
-    }
-  }
-/>
-`;
index cb96d5790f2cc5f5d76d21e281a005bd240506a3..b3bfbd34d34f1cb965f3d513344cbcb2c7363970 100644 (file)
@@ -23,14 +23,14 @@ import Tooltip from '../../components/controls/Tooltip';
 export interface DisableableSelectOptionProps {
   className?: string;
   disabledReason?: string;
-  option: { label?: string; value?: string | number | boolean; disabled?: boolean };
+  option: { label?: string; value?: string | number | boolean; isDisabled?: boolean };
   disableTooltipOverlay: () => React.ReactNode;
 }
 
 export default function DisableableSelectOption(props: DisableableSelectOptionProps) {
   const { option, disableTooltipOverlay, disabledReason, className = '' } = props;
   const label = option.label || option.value;
-  return option.disabled ? (
+  return option.isDisabled ? (
     <Tooltip overlay={disableTooltipOverlay()} placement="left">
       <span className={className}>
         {label}
index 4c6b5f4e7c4d461e0513147df78cfc1a4a9a4c90..b7655941a45b2c6d84f8944c1ff385d449c825c9 100644 (file)
@@ -24,12 +24,12 @@ import DisableableSelectOption, { DisableableSelectOptionProps } from '../Disabl
 it('should render correctly', () => {
   expect(shallowRender()).toMatchSnapshot('default');
   expect(shallowRender({ option: { value: 'baz' } })).toMatchSnapshot('no label');
-  expect(shallowRender({ option: { label: 'Bar', value: 'bar', disabled: true } })).toMatchSnapshot(
-    'disabled'
-  );
+  expect(
+    shallowRender({ option: { label: 'Bar', value: 'bar', isDisabled: true } })
+  ).toMatchSnapshot('disabled');
   expect(
     shallowRender({
-      option: { label: 'Bar', value: 'bar', disabled: true },
+      option: { label: 'Bar', value: 'bar', isDisabled: true },
       disabledReason: 'bar baz'
     })
   ).toMatchSnapshot('disabled, with explanation');
index 1017fb39a38f36a21754ab68e7cfa3d48744ca70..4273c2fbbb05dd8fde682aeea6cd55f6f725539f 100644 (file)
@@ -100,7 +100,7 @@ export function selectStyle<
       border: `1px solid ${state.isFocused ? colors.blue : colors.gray80}`,
       borderCollapse: 'separate',
       borderRadius: '2px',
-      backgroundColor: '#fff',
+      backgroundColor: state.isDisabled ? colors.disableGrayBg : '#fff',
       boxSizing: 'border-box',
       color: `${colors.baseFontColor}`,
       cursor: 'default',
@@ -144,9 +144,9 @@ export function selectStyle<
         display: 'flex'
       };
     },
-    indicatorsContainer: () => ({
+    indicatorsContainer: (_provided, state) => ({
       position: 'relative',
-      cursor: 'pointer',
+      cursor: state.isDisabled ? 'default' : 'pointer',
       textAlign: 'end',
       verticalAlign: 'middle',
       width: '20px',
@@ -217,10 +217,10 @@ export function selectStyle<
       lineHeight: '20px',
       padding: '0 8px',
       boxSizing: 'border-box',
-      color: `${colors.baseFontColor}`,
-      backgroundColor: state.isFocused ? `${colors.barBackgroundColor}` : 'white',
+      color: state.isDisabled ? colors.disableGrayText : colors.baseFontColor,
+      backgroundColor: state.isFocused ? colors.barBackgroundColor : colors.white,
       fontSize: `${sizes.smallFontSize}`,
-      cursor: 'pointer',
+      cursor: state.isDisabled ? 'default' : 'pointer',
       whiteSpace: 'nowrap',
       overflow: 'hidden',
       textOverflow: 'ellipsis'
diff --git a/server/sonar-web/src/main/js/helpers/mocks/quality-profiles.ts b/server/sonar-web/src/main/js/helpers/mocks/quality-profiles.ts
new file mode 100644 (file)
index 0000000..20d2a14
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * 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 { ProfileOption } from '../../apps/projectQualityProfiles/components/LanguageProfileSelectOption';
+
+export function mockProfileOption(overrides: Partial<ProfileOption> = {}): ProfileOption {
+  return {
+    value: 'profile-1',
+    label: 'Profile 1',
+    language: 'Java',
+    isDisabled: false,
+    ...overrides
+  };
+}