]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16086 migrate selectlegacy in issues
authorJeremy Davis <jeremy.davis@sonarsource.com>
Mon, 28 Mar 2022 12:18:27 +0000 (14:18 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 30 Mar 2022 13:38:10 +0000 (13:38 +0000)
server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap
server/sonar-web/src/main/js/components/controls/Select.tsx

index 0690a1223a973fc45ee6699db1e66d0415badc73..7754785e2afb3e9ddbe3253af613b0e362a7ca21 100644 (file)
@@ -20,6 +20,7 @@
 import { pickBy, sortBy } from 'lodash';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
+import { components, OptionProps, SingleValueProps } from 'react-select';
 import { bulkChangeIssues, searchIssueTags } from '../../../api/issues';
 import FormattingTips from '../../../components/common/FormattingTips';
 import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
@@ -28,7 +29,7 @@ import HelpTooltip from '../../../components/controls/HelpTooltip';
 import Modal from '../../../components/controls/Modal';
 import Radio from '../../../components/controls/Radio';
 import SearchSelect from '../../../components/controls/SearchSelect';
-import SelectLegacy from '../../../components/controls/SelectLegacy';
+import Select, { BasicSelectOption } from '../../../components/controls/Select';
 import Tooltip from '../../../components/controls/Tooltip';
 import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
 import SeverityHelper from '../../../components/shared/SeverityHelper';
@@ -198,7 +199,7 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
     this.setState({ comment: event.currentTarget.value });
   };
 
-  handleSelectFieldChange = (field: 'severity' | 'type') => (data: { value: string } | null) => {
+  handleSelectFieldChange = (field: 'severity' | 'type') => (data: BasicSelectOption | null) => {
     if (data) {
       this.setState<keyof FormFields>({ [field]: data.value });
     } else {
@@ -347,25 +348,33 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
     }
 
     const types: IssueType[] = ['BUG', 'VULNERABILITY', 'CODE_SMELL'];
-    const options = types.map(type => ({ label: translate('issue.type', type), value: type }));
+    const options: BasicSelectOption[] = types.map(type => ({
+      label: translate('issue.type', type),
+      value: type
+    }));
 
-    const optionRenderer = (option: { label: string; value: string }) => (
-      <>
+    const typeRenderer = (option: BasicSelectOption) => (
+      <div className="display-flex-center">
         <IssueTypeIcon query={option.value} />
         <span className="little-spacer-left">{option.label}</span>
-      </>
+      </div>
     );
 
     const input = (
-      <SelectLegacy
+      <Select
         className="input-super-large"
-        clearable={true}
+        isClearable={true}
+        isSearchable={false}
+        components={{
+          Option: (props: OptionProps<BasicSelectOption, false>) => (
+            <components.Option {...props}>{typeRenderer(props.data)}</components.Option>
+          ),
+          SingleValue: (props: SingleValueProps<BasicSelectOption>) => (
+            <components.SingleValue {...props}>{typeRenderer(props.data)}</components.SingleValue>
+          )
+        }}
         onChange={this.handleSelectFieldChange('type')}
-        optionRenderer={optionRenderer}
         options={options}
-        searchable={false}
-        value={this.state.type}
-        valueRenderer={optionRenderer}
       />
     );
 
@@ -380,21 +389,30 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
     }
 
     const severities = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];
-    const options = severities.map(severity => ({
+    const options: BasicSelectOption[] = severities.map(severity => ({
       label: translate('severity', severity),
       value: severity
     }));
 
     const input = (
-      <SelectLegacy
+      <Select
         className="input-super-large"
-        clearable={true}
+        isClearable={true}
+        isSearchable={false}
         onChange={this.handleSelectFieldChange('severity')}
-        optionRenderer={(option: { value: string }) => <SeverityHelper severity={option.value} />}
+        components={{
+          Option: (props: OptionProps<BasicSelectOption, false>) => (
+            <components.Option {...props}>
+              {<SeverityHelper className="display-flex-center" severity={props.data.value} />}
+            </components.Option>
+          ),
+          SingleValue: (props: SingleValueProps<BasicSelectOption>) => (
+            <components.SingleValue {...props}>
+              {<SeverityHelper className="display-flex-center" severity={props.data.value} />}
+            </components.SingleValue>
+          )
+        }}
         options={options}
-        searchable={false}
-        value={this.state.severity}
-        valueRenderer={(option: { value: string }) => <SeverityHelper severity={option.value} />}
       />
     );
 
index dca300b5dae0698e1a3f77c875b09c71dac4bf25..6e91c7258cb2ff597edf7565bb0203dda7e09b71 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { SelectComponentsProps } from 'react-select/src/Select';
 import { searchIssueTags } from '../../../../api/issues';
 import { SubmitButton } from '../../../../components/controls/buttons';
-import SelectLegacy from '../../../../components/controls/SelectLegacy';
+import Select from '../../../../components/controls/Select';
 import { mockIssue } from '../../../../helpers/testMocks';
 import { change, waitAndUpdate } from '../../../../helpers/testUtils';
 import { Issue } from '../../../../types/types';
@@ -96,6 +97,19 @@ it('should properly handle the search for tags', async () => {
   expect(searchIssueTags).toBeCalled();
 });
 
+it.each([
+  ['type', 'set_type'],
+  ['severity', 'set_severity']
+])('should render select for %s', async (_field, action) => {
+  const wrapper = getWrapper([mockIssue(false, { actions: [action] })]);
+  await waitAndUpdate(wrapper);
+
+  const { Option, SingleValue } = wrapper.find<SelectComponentsProps>(Select).props().components;
+
+  expect(Option({ data: { label: 'label', value: 'value' } })).toMatchSnapshot('Option');
+  expect(SingleValue({ data: { label: 'label', value: 'value' } })).toMatchSnapshot('SingleValue');
+});
+
 it('should disable the submit button unless some change is configured', async () => {
   const wrapper = getWrapper([mockIssue(false, { actions: ['set_severity', 'comment'] })]);
   await waitAndUpdate(wrapper);
@@ -108,7 +122,7 @@ it('should disable the submit button unless some change is configured', async ()
     expect(wrapper.find(SubmitButton).props().disabled).toBe(true);
 
     const { onChange } = wrapper
-      .find(SelectLegacy)
+      .find(Select)
       .at(0)
       .props();
     if (!onChange) {
index 38a1e700faaf3152e7c539a3cd93e39788480b26..d79ff90dddbf87e393de05ab1c3b2b14f8e58ce8 100644 (file)
@@ -150,3 +150,83 @@ Array [
   },
 ]
 `;
+
+exports[`should render select for severity: Option 1`] = `
+<Option
+  data={
+    Object {
+      "label": "label",
+      "value": "value",
+    }
+  }
+>
+  <SeverityHelper
+    className="display-flex-center"
+    severity="value"
+  />
+</Option>
+`;
+
+exports[`should render select for severity: SingleValue 1`] = `
+<SingleValue
+  data={
+    Object {
+      "label": "label",
+      "value": "value",
+    }
+  }
+>
+  <SeverityHelper
+    className="display-flex-center"
+    severity="value"
+  />
+</SingleValue>
+`;
+
+exports[`should render select for type: Option 1`] = `
+<Option
+  data={
+    Object {
+      "label": "label",
+      "value": "value",
+    }
+  }
+>
+  <div
+    className="display-flex-center"
+  >
+    <IssueTypeIcon
+      query="value"
+    />
+    <span
+      className="little-spacer-left"
+    >
+      label
+    </span>
+  </div>
+</Option>
+`;
+
+exports[`should render select for type: SingleValue 1`] = `
+<SingleValue
+  data={
+    Object {
+      "label": "label",
+      "value": "value",
+    }
+  }
+>
+  <div
+    className="display-flex-center"
+  >
+    <IssueTypeIcon
+      query="value"
+    />
+    <span
+      className="little-spacer-left"
+    >
+      label
+    </span>
+  </div>
+</SingleValue>
+`;
index 05eea3b355e22e90e7a80b6b9540b0752546837e..1017fb39a38f36a21754ab68e7cfa3d48744ca70 100644 (file)
@@ -33,6 +33,11 @@ const ArrowSpan = styled.span`
   width: 0;
 `;
 
+export interface BasicSelectOption {
+  label: string;
+  value: string;
+}
+
 export default class Select<
   Option,
   IsMulti extends boolean = false,
@@ -86,13 +91,13 @@ export function selectStyle<
       textAlign: 'left',
       width: '100%'
     }),
-    control: () => ({
+    control: (_provided, state) => ({
       position: 'relative',
       display: 'flex',
       width: '100%',
       minHeight: `${sizes.controlHeight}`,
       lineHeight: `calc(${sizes.controlHeight} - 2px)`,
-      border: `1px solid ${colors.gray80}`,
+      border: `1px solid ${state.isFocused ? colors.blue : colors.gray80}`,
       borderCollapse: 'separate',
       borderRadius: '2px',
       backgroundColor: '#fff',
@@ -204,6 +209,7 @@ export function selectStyle<
       overflowY: 'auto'
     }),
     placeholder: () => ({
+      position: 'absolute',
       color: '#666'
     }),
     option: (_provided, state) => ({