diff options
7 files changed, 144 insertions, 73 deletions
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 0b31128098d..8f0bd65cd0a 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -127,6 +127,7 @@ "postcss-calc": "7.0.2", "postcss-custom-properties": "9.1.1", "prettier": "1.19.1", + "react-select-event": "5.4.0", "remark": "11.0.2", "remark-react": "7", "ts-jest": "27.1.3", diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx index ce6879efa16..7ff3701a4ba 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/MetricSelect.tsx @@ -20,7 +20,7 @@ import { sortBy } from 'lodash'; import * as React from 'react'; import withMetricsContext from '../../../app/components/metrics/withMetricsContext'; -import SelectLegacy from '../../../components/controls/SelectLegacy'; +import Select from '../../../components/controls/Select'; import { getLocalizedMetricDomain, translate } from '../../../helpers/l10n'; import { Dict, Metric } from '../../../types/types'; import { getLocalizedMetricNameNoDiffMetric } from '../utils'; @@ -76,13 +76,13 @@ export class MetricSelectComponent extends React.PureComponent<Props> { }); return ( - <SelectLegacy + <Select className="text-middle quality-gate-metric-select" id="condition-metric" onChange={this.handleChange} options={optionsWithDomains} placeholder={translate('search.search_for_metrics')} - value={metric && metric.key} + value={optionsWithDomains.find(o => o.value === metric?.key)} /> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx index ea2f2fbd70c..47c6190c801 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx @@ -17,10 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { identity, omit } from 'lodash'; import * as React from 'react'; +import { components, ControlProps, OptionProps, SingleValueProps } from 'react-select'; import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; import Modal from '../../../components/controls/Modal'; -import SelectLegacy from '../../../components/controls/SelectLegacy'; +import Select from '../../../components/controls/Select'; import GroupIcon from '../../../components/icons/GroupIcon'; import Avatar from '../../../components/ui/Avatar'; import { translate } from '../../../helpers/l10n'; @@ -31,7 +33,7 @@ export interface QualityGatePermissionsAddModalRendererProps { onClose: () => void; onInputChange: (query: string) => void; onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void; - onSelection: (selection: UserBase | Group) => void; + onSelection: (selection: Option) => void; submitting: boolean; loading: boolean; query: string; @@ -39,7 +41,7 @@ export interface QualityGatePermissionsAddModalRendererProps { selection?: UserBase | Group; } -type Option = (UserBase | Group) & { value: string }; +export type Option = (UserBase | Group) & { value: string }; export default function QualityGatePermissionsAddModalRenderer( props: QualityGatePermissionsAddModalRendererProps @@ -50,6 +52,8 @@ export default function QualityGatePermissionsAddModalRenderer( const noResultsText = translate('no_results'); + const options = searchResults.map(r => ({ ...r, value: getValue(r) })); + return ( <Modal contentLabel={header} onRequestClose={props.onClose}> <header className="modal-head"> @@ -59,22 +63,24 @@ export default function QualityGatePermissionsAddModalRenderer( <div className="modal-body"> <div className="modal-field"> <label>{translate('quality_gates.permissions.search')}</label> - <SelectLegacy - autoFocus={true} + <Select className="Select-big" - clearable={false} - // disable default react-select filtering - filterOptions={i => i} + autoFocus={true} + isClearable={false} + isSearchable={true} + placeholder="" isLoading={loading} - noResultsText={noResultsText} + filterOptions={identity} + noOptionsMessage={() => noResultsText} onChange={props.onSelection} onInputChange={props.onInputChange} - optionRenderer={optionRenderer} - options={searchResults.map(r => ({ ...r, value: isUser(r) ? r.login : r.name }))} - placeholder="" - searchable={true} - value={selection} - valueRenderer={optionRenderer} + components={{ + Option: optionRenderer, + SingleValue: singleValueRenderer, + Control: controlRenderer + }} + options={options} + value={options.find(o => o.value === (selection && getValue(selection)))} /> </div> </div> @@ -88,7 +94,11 @@ export default function QualityGatePermissionsAddModalRenderer( ); } -function optionRenderer(option: Option) { +function getValue(option: UserBase | Group) { + return isUser(option) ? option.login : option.name; +} + +export function customOptions(option: Option) { return ( <> {isUser(option) ? ( @@ -101,3 +111,27 @@ function optionRenderer(option: Option) { </> ); } + +function optionRenderer(props: OptionProps<Option, false>) { + return ( + <components.Option {...props} className="Select-option"> + {customOptions(props.data)} + </components.Option> + ); +} + +function singleValueRenderer(props: SingleValueProps<Option>) { + return ( + <components.SingleValue {...props} className="Select-value-label"> + {customOptions(props.data)} + </components.SingleValue> + ); +} + +function controlRenderer(props: ControlProps<Option, false>) { + return ( + <components.Control {...omit(props, ['children'])} className="abs-height-100 Select-control"> + {props.children} + </components.Control> + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx index 8aada4ac303..612607f9c19 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/App-it.tsx @@ -19,6 +19,7 @@ */ import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import selectEvent from 'react-select-event'; import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock'; import { mockAppState } from '../../../../helpers/testMocks'; import { renderApp } from '../../../../helpers/testReactTestingUtils'; @@ -146,8 +147,7 @@ it('should be able to add a condition', async () => { let dialog = within(screen.getByRole('dialog')); await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.new_code' })); - await user.click(dialog.getByRole('combobox')); - await user.click(dialog.getByRole('option', { name: 'Issues' })); + await selectEvent.select(dialog.getByRole('textbox'), ['Issues']); await user.click(dialog.getByRole('textbox', { name: 'quality_gates.conditions.value' })); await user.keyboard('12{Enter}'); @@ -161,9 +161,7 @@ it('should be able to add a condition', async () => { await user.click(await screen.findByText('quality_gates.add_condition')); dialog = within(screen.getByRole('dialog')); - - await user.click(dialog.getByLabelText('quality_gates.conditions.fails_when')); - await user.click(dialog.getByRole('option', { name: 'Info Issues' })); + await selectEvent.select(dialog.getByRole('textbox'), ['Info Issues']); await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.overall_code' })); await user.click(dialog.getByLabelText('quality_gates.conditions.operator')); @@ -183,8 +181,7 @@ it('should be able to add a condition', async () => { dialog = within(screen.getByRole('dialog')); await user.click(dialog.getByRole('radio', { name: 'quality_gates.conditions.overall_code' })); - await user.click(dialog.getByLabelText('quality_gates.conditions.fails_when')); - await user.click(dialog.getByRole('option', { name: 'Maintainability Rating' })); + await selectEvent.select(dialog.getByRole('textbox'), ['Maintainability Rating']); await user.click(dialog.getByLabelText('quality_gates.conditions.value')); await user.click(dialog.getByText('B')); await user.click(dialog.getByRole('button', { name: 'quality_gates.add_condition' })); diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModalRenderer-test.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModalRenderer-test.tsx index 9e10425c133..da50d58ab68 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModalRenderer-test.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGatePermissionsAddModalRenderer-test.tsx @@ -19,9 +19,9 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import SelectLegacy from '../../../../components/controls/SelectLegacy'; import { mockUserBase } from '../../../../helpers/mocks/users'; import QualityGatePermissionsAddModalRenderer, { + customOptions, QualityGatePermissionsAddModalRendererProps } from '../QualityGatePermissionsAddModalRenderer'; @@ -38,12 +38,10 @@ it('should render correctly', () => { }); it('should render options correctly', () => { - const wrapper = shallowRender(); - - const { optionRenderer = () => null } = wrapper.find(SelectLegacy).props(); - - expect(optionRenderer({ avatar: 'A', name: 'name', login: 'login' })).toMatchSnapshot('user'); - expect(optionRenderer({ name: 'group name' })).toMatchSnapshot('group'); + expect( + customOptions({ avatar: 'A', name: 'name', login: 'login', value: 'login' }) + ).toMatchSnapshot('user'); + expect(customOptions({ name: 'group name', value: 'group name' })).toMatchSnapshot('group'); }); function shallowRender(overrides: Partial<QualityGatePermissionsAddModalRendererProps> = {}) { diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModalRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModalRenderer-test.tsx.snap index 5994dbc36f5..f11d813ff89 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModalRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/__snapshots__/QualityGatePermissionsAddModalRenderer-test.tsx.snap @@ -24,20 +24,25 @@ exports[`should render correctly: default 1`] = ` <label> quality_gates.permissions.search </label> - <SelectLegacy + <Select autoFocus={true} className="Select-big" - clearable={false} + components={ + Object { + "Control": [Function], + "Option": [Function], + "SingleValue": [Function], + } + } filterOptions={[Function]} + isClearable={false} isLoading={false} - noResultsText="no_results" + isSearchable={true} + noOptionsMessage={[Function]} onChange={[MockFunction]} onInputChange={[MockFunction]} - optionRenderer={[Function]} options={Array []} placeholder="" - searchable={true} - valueRenderer={[Function]} /> </div> </div> @@ -83,16 +88,23 @@ exports[`should render correctly: query and results 1`] = ` <label> quality_gates.permissions.search </label> - <SelectLegacy + <Select autoFocus={true} className="Select-big" - clearable={false} + components={ + Object { + "Control": [Function], + "Option": [Function], + "SingleValue": [Function], + } + } filterOptions={[Function]} + isClearable={false} isLoading={false} - noResultsText="no_results" + isSearchable={true} + noOptionsMessage={[Function]} onChange={[MockFunction]} onInputChange={[MockFunction]} - optionRenderer={[Function]} options={ Array [ Object { @@ -106,8 +118,6 @@ exports[`should render correctly: query and results 1`] = ` ] } placeholder="" - searchable={true} - valueRenderer={[Function]} /> </div> </div> @@ -153,25 +163,25 @@ exports[`should render correctly: selection 1`] = ` <label> quality_gates.permissions.search </label> - <SelectLegacy + <Select autoFocus={true} className="Select-big" - clearable={false} + components={ + Object { + "Control": [Function], + "Option": [Function], + "SingleValue": [Function], + } + } filterOptions={[Function]} + isClearable={false} isLoading={false} - noResultsText="no_results" + isSearchable={true} + noOptionsMessage={[Function]} onChange={[MockFunction]} onInputChange={[MockFunction]} - optionRenderer={[Function]} options={Array []} placeholder="" - searchable={true} - value={ - Object { - "login": "userlogin", - } - } - valueRenderer={[Function]} /> </div> </div> @@ -217,20 +227,25 @@ exports[`should render correctly: short query 1`] = ` <label> quality_gates.permissions.search </label> - <SelectLegacy + <Select autoFocus={true} className="Select-big" - clearable={false} + components={ + Object { + "Control": [Function], + "Option": [Function], + "SingleValue": [Function], + } + } filterOptions={[Function]} + isClearable={false} isLoading={false} - noResultsText="no_results" + isSearchable={true} + noOptionsMessage={[Function]} onChange={[MockFunction]} onInputChange={[MockFunction]} - optionRenderer={[Function]} options={Array []} placeholder="" - searchable={true} - valueRenderer={[Function]} /> </div> </div> @@ -276,25 +291,25 @@ exports[`should render correctly: submitting 1`] = ` <label> quality_gates.permissions.search </label> - <SelectLegacy + <Select autoFocus={true} className="Select-big" - clearable={false} + components={ + Object { + "Control": [Function], + "Option": [Function], + "SingleValue": [Function], + } + } filterOptions={[Function]} + isClearable={false} isLoading={false} - noResultsText="no_results" + isSearchable={true} + noOptionsMessage={[Function]} onChange={[MockFunction]} onInputChange={[MockFunction]} - optionRenderer={[Function]} options={Array []} placeholder="" - searchable={true} - value={ - Object { - "login": "userlogin", - } - } - valueRenderer={[Function]} /> </div> </div> diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index b268bd105fb..0e24bb8630a 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -2722,6 +2722,22 @@ __metadata: languageName: node linkType: hard +"@testing-library/dom@npm:>=7": + version: 8.12.0 + resolution: "@testing-library/dom@npm:8.12.0" + dependencies: + "@babel/code-frame": ^7.10.4 + "@babel/runtime": ^7.12.5 + "@types/aria-query": ^4.2.0 + aria-query: ^5.0.0 + chalk: ^4.1.0 + dom-accessibility-api: ^0.5.9 + lz-string: ^1.4.4 + pretty-format: ^27.0.2 + checksum: 2bbf5fa5c1e883571c440ccee76c0568fa5153b43c097456dd7146797256687352bfca9db574e0e78a022ce14722a6acaaba5f680ee16b95e12405501713d34d + languageName: node + linkType: hard + "@testing-library/jest-dom@npm:5.16.2": version: 5.16.2 resolution: "@testing-library/jest-dom@npm:5.16.2" @@ -3612,6 +3628,7 @@ __metadata: react-redux: 5.1.1 react-router: 3.2.6 react-select: 4.3.1 + react-select-event: 5.4.0 react-select-legacy: "npm:react-select@1.2.1" react-virtualized: 9.22.3 redux: 4.1.2 @@ -10600,6 +10617,15 @@ __metadata: languageName: node linkType: hard +"react-select-event@npm:5.4.0": + version: 5.4.0 + resolution: "react-select-event@npm:5.4.0" + dependencies: + "@testing-library/dom": ">=7" + checksum: 244b1bdff5374b56a7d8156859a10dfd3191573ac419a8f91a64283cde48517df83c90f2df1891fc74bceb548d21ace9a9cb87e252bf2f83f78e305ea577af53 + languageName: node + linkType: hard + "react-select-legacy@npm:react-select@1.2.1": version: 1.2.1 resolution: "react-select@npm:1.2.1" |