@@ -33,9 +33,9 @@ import { | |||
useDayPicker, | |||
} from 'react-day-picker'; | |||
import { useIntl } from 'react-intl'; | |||
import { InputSelect } from '../../sonar-aligned/components/input'; | |||
import { InteractiveIcon } from '../InteractiveIcon'; | |||
import { ChevronLeftIcon, ChevronRightIcon } from '../icons'; | |||
import { InputSelect } from './InputSelect'; | |||
const YEARS_TO_DISPLAY = 10; | |||
const MONTHS_IN_A_YEAR = 12; |
@@ -18,44 +18,30 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import styled from '@emotion/styled'; | |||
import { GroupBase, OnChangeValue } from 'react-select'; | |||
import tw from 'twin.macro'; | |||
import { themeBorder, themeColor, themeContrast } from '../../helpers/theme'; | |||
import { InputSizeKeys } from '../../types/theme'; | |||
import { InputSelect, LabelValueSelectOption } from './InputSelect'; | |||
import { InputSelect, SelectProps } from '../../sonar-aligned/components/input'; | |||
interface Props<V> { | |||
className?: string; | |||
components?: Parameters<typeof InputSelect>[0]['components']; | |||
type DiscreetProps< | |||
Option, | |||
IsMulti extends boolean = false, | |||
Group extends GroupBase<Option> = GroupBase<Option>, | |||
> = SelectProps<Option, IsMulti, Group> & { | |||
customValue?: JSX.Element; | |||
isDisabled?: boolean; | |||
menuIsOpen?: boolean; | |||
onMenuClose?: () => void; | |||
onMenuOpen?: () => void; | |||
options: Array<LabelValueSelectOption<V>>; | |||
setValue: ({ value }: LabelValueSelectOption<V>) => void; | |||
size?: InputSizeKeys; | |||
value: V; | |||
} | |||
setValue: (value: OnChangeValue<Option, IsMulti>) => void; | |||
}; | |||
export function DiscreetSelect<V>({ | |||
className, | |||
customValue, | |||
onMenuOpen, | |||
options, | |||
size = 'small', | |||
setValue, | |||
value, | |||
...props | |||
}: Props<V>) { | |||
export function DiscreetSelect< | |||
Option, | |||
IsMulti extends boolean = false, | |||
Group extends GroupBase<Option> = GroupBase<Option>, | |||
>({ customValue, size = 'small', setValue, ...props }: DiscreetProps<Option, IsMulti, Group>) { | |||
return ( | |||
<StyledSelect | |||
className={className} | |||
<StyledSelect<Option, IsMulti, Group> | |||
onChange={setValue} | |||
onMenuOpen={onMenuOpen} | |||
options={options} | |||
placeholder={customValue} | |||
size={size} | |||
value={options.find((item) => item.value === value)} | |||
{...props} | |||
/> | |||
); | |||
@@ -121,4 +107,4 @@ const StyledSelect = styled(InputSelect)` | |||
& .react-select__control--menu-is-open { | |||
${tw`sw-border-none`}; | |||
} | |||
`; | |||
` as typeof InputSelect; |
@@ -19,35 +19,26 @@ | |||
*/ | |||
import classNames from 'classnames'; | |||
import { omit } from 'lodash'; | |||
import React, { RefObject } from 'react'; | |||
import React from 'react'; | |||
import { GroupBase, InputProps } from 'react-select'; | |||
import AsyncSelect, { AsyncProps } from 'react-select/async'; | |||
import Select from 'react-select/dist/declarations/src/Select'; | |||
import { INPUT_SIZES } from '../../helpers'; | |||
import { Key } from '../../helpers/keyboard'; | |||
import { SelectProps, selectStyle } from '../../sonar-aligned/components/input'; | |||
import { InputSearch } from './InputSearch'; | |||
import { LabelValueSelectOption, SelectProps, selectStyle } from './InputSelect'; | |||
type SearchSelectProps< | |||
V, | |||
Option extends LabelValueSelectOption<V>, | |||
Option, | |||
IsMulti extends boolean = false, | |||
Group extends GroupBase<Option> = GroupBase<Option>, | |||
> = SelectProps<V, Option, IsMulti, Group> & AsyncProps<Option, IsMulti, Group>; | |||
> = SelectProps<Option, IsMulti, Group> & AsyncProps<Option, IsMulti, Group>; | |||
export function SearchSelect< | |||
V, | |||
Option extends LabelValueSelectOption<V>, | |||
Option, | |||
IsMulti extends boolean = false, | |||
Group extends GroupBase<Option> = GroupBase<Option>, | |||
>({ | |||
size = 'full', | |||
selectRef, | |||
...props | |||
}: SearchSelectProps<V, Option, IsMulti, Group> & { | |||
selectRef?: RefObject<Select<Option, IsMulti, Group>>; | |||
}) { | |||
const styles = selectStyle<V, Option, IsMulti, Group>({ size }); | |||
>({ size = 'full', selectRef, ...props }: SearchSelectProps<Option, IsMulti, Group>) { | |||
const styles = selectStyle<Option, IsMulti, Group>({ size }); | |||
return ( | |||
<AsyncSelect<Option, IsMulti, Group> | |||
{...omit(props, 'className', 'large')} | |||
@@ -82,8 +73,7 @@ export function SearchSelect< | |||
} | |||
export function SearchSelectInput< | |||
V, | |||
Option extends LabelValueSelectOption<V>, | |||
Option, | |||
IsMulti extends boolean = false, | |||
Group extends GroupBase<Option> = GroupBase<Option>, | |||
>(props: InputProps<Option, IsMulti, Group>) { |
@@ -30,10 +30,14 @@ import { AsyncProps } from 'react-select/async'; | |||
import Select from 'react-select/dist/declarations/src/Select'; | |||
import tw from 'twin.macro'; | |||
import { PopupPlacement, PopupZLevel, themeBorder } from '../../helpers'; | |||
import { | |||
IconOption, | |||
LabelValueSelectOption, | |||
SelectProps, | |||
} from '../../sonar-aligned/components/input'; | |||
import { InputSizeKeys } from '../../types/theme'; | |||
import { DropdownToggler } from '../DropdownToggler'; | |||
import { SearchHighlighterContext } from '../SearchHighlighter'; | |||
import { IconOption, LabelValueSelectOption, SelectProps } from './InputSelect'; | |||
import { SearchSelect } from './SearchSelect'; | |||
import { SearchSelectDropdownControl } from './SearchSelectDropdownControl'; | |||
@@ -48,7 +52,7 @@ export interface SearchSelectDropdownProps< | |||
Option extends LabelValueSelectOption<V>, | |||
IsMulti extends boolean = false, | |||
Group extends GroupBase<Option> = GroupBase<Option>, | |||
> extends SelectProps<V, Option, IsMulti, Group>, | |||
> extends SelectProps<Option, IsMulti, Group>, | |||
AsyncProps<Option, IsMulti, Group> { | |||
className?: string; | |||
controlAriaLabel?: string; |
@@ -24,7 +24,7 @@ import { FCProps } from '../../../types/misc'; | |||
import { DiscreetSelect } from '../DiscreetSelect'; | |||
it('should render discreet select and invoke CB on value click', async () => { | |||
const value = 'foo'; | |||
const value = options[0]; | |||
const setValue = jest.fn(); | |||
const user = userEvent.setup(); | |||
@@ -36,24 +36,21 @@ it('should render discreet select and invoke CB on value click', async () => { | |||
expect(setValue).toHaveBeenCalled(); | |||
}); | |||
const options = [ | |||
{ label: 'foo-bar', value: 'foo', default: 1 }, | |||
{ | |||
label: 'bar-foo', | |||
value: 'bar', | |||
Icon: ( | |||
<span role="note" title="Icon"> | |||
Icon | |||
</span> | |||
), | |||
}, | |||
]; | |||
function setupWithProps(props: Partial<FCProps<typeof DiscreetSelect>>) { | |||
return render( | |||
<DiscreetSelect | |||
options={[ | |||
{ label: 'foo-bar', value: 'foo' }, | |||
{ | |||
label: 'bar-foo', | |||
value: 'bar', | |||
Icon: ( | |||
<span role="note" title="Icon"> | |||
Icon | |||
</span> | |||
), | |||
}, | |||
]} | |||
setValue={jest.fn()} | |||
value="foo" | |||
{...props} | |||
/>, | |||
<DiscreetSelect options={options} setValue={jest.fn()} value={options[0]} {...props} />, | |||
); | |||
} |
@@ -20,8 +20,8 @@ | |||
import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import { renderWithContext } from '../../../helpers/testUtils'; | |||
import { LabelValueSelectOption } from '../../../sonar-aligned'; | |||
import { FCProps } from '../../../types/misc'; | |||
import { LabelValueSelectOption } from '../InputSelect'; | |||
import { SearchSelectDropdown } from '../SearchSelectDropdown'; | |||
const defaultOptions = [ |
@@ -26,7 +26,6 @@ export * from './FormField'; | |||
export * from './InputField'; | |||
export * from './InputMultiSelect'; | |||
export * from './InputSearch'; | |||
export * from './InputSelect'; | |||
export * from './MultiSelectMenu'; | |||
export * from './RadioButton'; | |||
export * from './SearchSelect'; |
@@ -24,4 +24,5 @@ export * from './MetricsRatingBadge'; | |||
export * from './Table'; | |||
export * from './ToggleButton'; | |||
export * from './buttons'; | |||
export * from './input'; | |||
export * from './typography'; |
@@ -0,0 +1,110 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2024 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 classNames from 'classnames'; | |||
import { omit } from 'lodash'; | |||
import { useMemo } from 'react'; | |||
import ReactSelect, { GroupBase } from 'react-select'; | |||
import { | |||
ClearIndicator, | |||
DropdownIndicator, | |||
IconOption, | |||
SelectProps, | |||
SingleValue, | |||
selectStyle, | |||
} from './SelectCommon'; | |||
export function InputSelect< | |||
Option, | |||
IsMulti extends boolean = false, | |||
Group extends GroupBase<Option> = GroupBase<Option>, | |||
>({ | |||
size = 'medium', | |||
className, | |||
options, | |||
getOptionLabel, | |||
selectRef, | |||
shouldSortOption = false, | |||
...props | |||
}: SelectProps<Option, IsMulti, Group>) { | |||
const orderedOptions = useMemo(() => { | |||
if (!options || options.length === 0) { | |||
return options; | |||
} | |||
if (shouldSortOption) { | |||
return (options as Option[]).sort((a, b) => { | |||
const nameA = getOptionLabel?.(a).toUpperCase() ?? ''; | |||
const nameB = getOptionLabel?.(b).toUpperCase() ?? ''; | |||
if (nameA < nameB) { | |||
return -1; | |||
} | |||
if (nameA > nameB) { | |||
return 1; | |||
} | |||
return 0; | |||
}); | |||
} | |||
return options; | |||
}, [shouldSortOption, getOptionLabel, options]); | |||
return ( | |||
<ReactSelect<Option, IsMulti, Group> | |||
{...omit(props, 'className', 'large')} | |||
className={classNames('react-select', className)} | |||
classNamePrefix="react-select" | |||
classNames={{ | |||
container: () => 'sw-relative sw-inline-block sw-align-middle', | |||
placeholder: () => 'sw-truncate sw-leading-4', | |||
menu: () => 'sw-z-dropdown-menu sw-ml-1/2 sw-mt-2', | |||
menuList: () => 'sw-overflow-y-auto sw-py-2 sw-max-h-[12.25rem]', | |||
clearIndicator: () => 'sw-p-0', | |||
dropdownIndicator: () => classNames(props.isClearable && 'sw-p-0'), | |||
control: ({ isDisabled }) => | |||
classNames( | |||
'sw-box-border sw-rounded-2 sw-overflow-hidden', | |||
isDisabled && 'sw-pointer-events-none sw-cursor-not-allowed', | |||
), | |||
option: ({ isDisabled }) => | |||
classNames( | |||
'it__select-option sw-py-2 sw-px-3 sw-cursor-pointer', | |||
isDisabled && 'sw-pointer-events-none sw-cursor-not-allowed', | |||
), | |||
...props.classNames, | |||
}} | |||
components={{ | |||
ClearIndicator, | |||
Option: IconOption, | |||
SingleValue, | |||
DropdownIndicator, | |||
IndicatorSeparator: null, | |||
...props.components, | |||
}} | |||
getOptionLabel={getOptionLabel} | |||
isClearable={props.isClearable ?? false} | |||
isSearchable={props.isSearchable ?? false} | |||
onMenuOpen={props.onMenuOpen} | |||
options={orderedOptions} | |||
ref={selectRef} | |||
styles={selectStyle({ size })} | |||
/> | |||
); | |||
} |
@@ -18,11 +18,9 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { useTheme as themeInfo } from '@emotion/react'; | |||
import classNames from 'classnames'; | |||
import { omit } from 'lodash'; | |||
import { ReactNode } from 'react'; | |||
import { RefObject } from 'react'; | |||
import { useIntl } from 'react-intl'; | |||
import ReactSelect, { | |||
import { | |||
ClearIndicatorProps, | |||
GroupBase, | |||
Props as NamedProps, | |||
@@ -30,45 +28,42 @@ import ReactSelect, { | |||
StylesConfig, | |||
components, | |||
} from 'react-select'; | |||
import { INPUT_SIZES } from '../../helpers'; | |||
import { themeBorder, themeColor, themeContrast } from '../../helpers/theme'; | |||
import { InputSizeKeys } from '../../types/theme'; | |||
import { InteractiveIcon } from '../InteractiveIcon'; | |||
import { SearchHighlighter } from '../SearchHighlighter'; | |||
import Select from 'react-select/dist/declarations/src/Select'; | |||
import { InteractiveIcon } from '../../../components/InteractiveIcon'; | |||
import { SearchHighlighter } from '../../../components/SearchHighlighter'; | |||
import { ChevronDownIcon, CloseIcon } from '../../../components/icons'; | |||
import { INPUT_SIZES } from '../../../helpers'; | |||
import { themeBorder, themeColor, themeContrast } from '../../../helpers/theme'; | |||
import { InputSizeKeys } from '../../../types/theme'; | |||
import { ChevronDownIcon, CloseIcon } from '../icons'; | |||
export interface LabelValueSelectOption<V> { | |||
Icon?: ReactNode; | |||
label: string; | |||
value: V; | |||
} | |||
interface ExtensionProps { | |||
export interface ExtensionProps< | |||
Option, | |||
IsMulti extends boolean = false, | |||
Group extends GroupBase<Option> = GroupBase<Option>, | |||
> { | |||
clearLabel?: string; | |||
selectRef?: RefObject<Select<Option, IsMulti, Group>>; | |||
shouldSortOption?: boolean; | |||
size?: InputSizeKeys; | |||
} | |||
export type SelectProps< | |||
V, | |||
Option extends LabelValueSelectOption<V>, | |||
Option, | |||
IsMulti extends boolean = false, | |||
Group extends GroupBase<Option> = GroupBase<Option>, | |||
> = NamedProps<Option, IsMulti, Group> & ExtensionProps; | |||
> = NamedProps<Option, IsMulti, Group> & ExtensionProps<Option, IsMulti, Group>; | |||
export function IconOption< | |||
V, | |||
Option extends LabelValueSelectOption<V>, | |||
Option, | |||
IsMulti extends boolean = false, | |||
Group extends GroupBase<Option> = GroupBase<Option>, | |||
>(props: OptionProps<Option, IsMulti, Group>) { | |||
const { | |||
data: { label, Icon }, | |||
} = props; | |||
const { label, isSelected } = props; | |||
const { Icon } = props.data as { Icon: JSX.Element }; | |||
return ( | |||
<components.Option {...props}> | |||
<div className="sw-flex sw-items-center sw-gap-1"> | |||
<div aria-selected={isSelected} className="sw-flex sw-items-center sw-gap-1" role="option"> | |||
{Icon} | |||
<SearchHighlighter>{label}</SearchHighlighter> | |||
</div> | |||
@@ -76,15 +71,13 @@ export function IconOption< | |||
); | |||
} | |||
function SingleValue< | |||
V, | |||
Option extends LabelValueSelectOption<V>, | |||
export function SingleValue< | |||
Option, | |||
IsMulti extends boolean = false, | |||
Group extends GroupBase<Option> = GroupBase<Option>, | |||
>(props: OptionProps<Option, IsMulti, Group>) { | |||
const { | |||
data: { label, Icon }, | |||
} = props; | |||
const label = props.selectProps.getOptionLabel(props.data); | |||
const { Icon } = props.data as { Icon: JSX.Element }; | |||
return ( | |||
<components.SingleValue {...props}> | |||
@@ -96,14 +89,13 @@ function SingleValue< | |||
); | |||
} | |||
function ClearIndicator< | |||
V, | |||
Option extends LabelValueSelectOption<V>, | |||
export function ClearIndicator< | |||
Option, | |||
IsMulti extends boolean = false, | |||
Group extends GroupBase<Option> = GroupBase<Option>, | |||
>( | |||
props: ClearIndicatorProps<Option, IsMulti, Group> & { | |||
selectProps: SelectProps<V, Option, IsMulti, Group>; | |||
selectProps: SelectProps<Option, IsMulti, Group>; | |||
}, | |||
) { | |||
const intl = useIntl(); | |||
@@ -123,9 +115,8 @@ function ClearIndicator< | |||
); | |||
} | |||
function DropdownIndicator< | |||
V, | |||
Option extends LabelValueSelectOption<V>, | |||
export function DropdownIndicator< | |||
Option, | |||
IsMulti extends boolean = false, | |||
Group extends GroupBase<Option> = GroupBase<Option>, | |||
>(props: OptionProps<Option, IsMulti, Group>) { | |||
@@ -138,55 +129,8 @@ function DropdownIndicator< | |||
); | |||
} | |||
export function InputSelect< | |||
V, | |||
Option extends LabelValueSelectOption<V>, | |||
IsMulti extends boolean = false, | |||
Group extends GroupBase<Option> = GroupBase<Option>, | |||
>({ size = 'medium', className, ...props }: SelectProps<V, Option, IsMulti, Group>) { | |||
return ( | |||
<ReactSelect<Option, IsMulti, Group> | |||
{...omit(props, 'className', 'large')} | |||
className={classNames('react-select', className)} | |||
classNamePrefix="react-select" | |||
classNames={{ | |||
container: () => 'sw-relative sw-inline-block sw-align-middle', | |||
placeholder: () => 'sw-truncate sw-leading-4', | |||
menu: () => 'sw-z-dropdown-menu sw-ml-1/2 sw-mt-2', | |||
menuList: () => 'sw-overflow-y-auto sw-py-2 sw-max-h-[12.25rem]', | |||
clearIndicator: () => 'sw-p-0', | |||
dropdownIndicator: () => classNames(props.isClearable && 'sw-p-0'), | |||
control: ({ isDisabled }) => | |||
classNames( | |||
'sw-box-border sw-rounded-2 sw-overflow-hidden', | |||
isDisabled && 'sw-pointer-events-none sw-cursor-not-allowed', | |||
), | |||
option: ({ isDisabled }) => | |||
classNames( | |||
'it__select-option sw-py-2 sw-px-3 sw-cursor-pointer', | |||
isDisabled && 'sw-pointer-events-none sw-cursor-not-allowed', | |||
), | |||
...props.classNames, | |||
}} | |||
components={{ | |||
ClearIndicator, | |||
Option: IconOption, | |||
SingleValue, | |||
DropdownIndicator, | |||
IndicatorSeparator: null, | |||
...props.components, | |||
}} | |||
isClearable={props.isClearable ?? false} | |||
isSearchable={props.isSearchable ?? false} | |||
onMenuOpen={props.onMenuOpen} | |||
styles={selectStyle({ size })} | |||
/> | |||
); | |||
} | |||
export function selectStyle< | |||
V, | |||
Option extends LabelValueSelectOption<V>, | |||
Option, | |||
IsMulti extends boolean = false, | |||
Group extends GroupBase<Option> = GroupBase<Option>, | |||
>({ size }: { size: InputSizeKeys }): StylesConfig<Option, IsMulti, Group> { | |||
@@ -231,3 +175,9 @@ export function selectStyle< | |||
}), | |||
}; | |||
} | |||
export interface LabelValueSelectOption<V = string> { | |||
Icon?: React.ReactNode; | |||
label: string; | |||
value: V; | |||
} |
@@ -19,8 +19,8 @@ | |||
*/ | |||
import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import { renderWithContext } from '../../../helpers/testUtils'; | |||
import { FCProps } from '../../../types/misc'; | |||
import { renderWithContext } from '../../../../helpers/testUtils'; | |||
import { FCProps } from '../../../../types/misc'; | |||
import { InputSelect } from '../InputSelect'; | |||
it('should render select input and be able to click and change', async () => { | |||
@@ -68,6 +68,18 @@ it('should render select input with disabled prop', () => { | |||
expect(screen.getByRole('combobox')).toBeDisabled(); | |||
}); | |||
it('should render the select options with sorting when shouldSortOption is true and getOptionLabel passed', async () => { | |||
const { user } = setupWithProps({ | |||
shouldSortOption: true, | |||
getOptionLabel: (o: { label: string }) => o.label, | |||
}); | |||
await user.click(screen.getByRole('combobox')); | |||
const options = screen.getAllByRole('option'); | |||
expect(options).toHaveLength(2); | |||
expect(options[0]).toHaveTextContent('bar-foo'); | |||
expect(options[1]).toHaveTextContent('foo-bar'); | |||
}); | |||
function setupWithProps(props: Partial<FCProps<typeof InputSelect>>) { | |||
return renderWithContext( | |||
<InputSelect |
@@ -0,0 +1,22 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2024 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. | |||
*/ | |||
export * from './InputSelect'; | |||
export * from './SelectCommon'; |
@@ -32,7 +32,7 @@ interface StatusFilterProps { | |||
export default function StatusFilter(props: Readonly<StatusFilterProps>) { | |||
const { id, value, onChange } = props; | |||
const options: LabelValueSelectOption<string>[] = [ | |||
const options: LabelValueSelectOption[] = [ | |||
{ value: STATUSES.ALL, label: translate('background_task.status.ALL') }, | |||
{ | |||
value: STATUSES.ALL_EXCEPT_PENDING, | |||
@@ -46,7 +46,7 @@ export default function StatusFilter(props: Readonly<StatusFilterProps>) { | |||
]; | |||
const handleChange = React.useCallback( | |||
({ value }: LabelValueSelectOption<string>) => { | |||
({ value }: LabelValueSelectOption) => { | |||
onChange(value); | |||
}, | |||
[onChange], |
@@ -30,7 +30,7 @@ interface Props { | |||
} | |||
export default class TypesFilter extends React.PureComponent<Props> { | |||
handleChange = ({ value }: LabelValueSelectOption<string>) => { | |||
handleChange = ({ value }: LabelValueSelectOption) => { | |||
this.props.onChange(value); | |||
}; | |||
@@ -43,7 +43,7 @@ export default class TypesFilter extends React.PureComponent<Props> { | |||
}; | |||
}); | |||
const allOptions: LabelValueSelectOption<string>[] = [ | |||
const allOptions: LabelValueSelectOption[] = [ | |||
{ value: ALL_TYPES, label: translate('background_task.type.ALL') }, | |||
...options, | |||
]; |
@@ -224,7 +224,7 @@ export default function CustomRuleFormModal(props: Readonly<Props>) { | |||
); | |||
const StatusField = React.useMemo(() => { | |||
const statusesOptions = RULE_STATUSES.map((status) => ({ | |||
const statusesOptions = RULE_STATUSES.map((status: Status) => ({ | |||
label: translate('rules.status', status), | |||
value: status, | |||
})); |
@@ -304,7 +304,7 @@ export default function AzureProjectCreate({ | |||
function transformToOptions( | |||
projects: AzureProject[], | |||
repositories?: Dict<AzureRepository[]>, | |||
): Array<GroupBase<LabelValueSelectOption<string>>> { | |||
): Array<GroupBase<LabelValueSelectOption>> { | |||
return projects.map(({ name: projectName }) => ({ | |||
label: projectName, | |||
options: | |||
@@ -314,6 +314,6 @@ function transformToOptions( | |||
})); | |||
} | |||
function transformToOption({ name }: AzureRepository): LabelValueSelectOption<string> { | |||
function transformToOption({ name }: AzureRepository): LabelValueSelectOption { | |||
return { value: name, label: name }; | |||
} |
@@ -212,9 +212,6 @@ export default function BitbucketCloudProjectCreate(props: Readonly<Props>) { | |||
); | |||
} | |||
function transformToOption({ | |||
name, | |||
slug, | |||
}: BitbucketCloudRepository): LabelValueSelectOption<string> { | |||
function transformToOption({ name, slug }: BitbucketCloudRepository): LabelValueSelectOption { | |||
return { value: slug, label: name }; | |||
} |
@@ -256,6 +256,6 @@ export default function GitHubProjectCreate(props: Readonly<Props>) { | |||
function transformToOption({ | |||
key, | |||
name, | |||
}: GithubOrganization | GithubRepository): LabelValueSelectOption<string> { | |||
}: GithubOrganization | GithubRepository): LabelValueSelectOption { | |||
return { value: key, label: name }; | |||
} |
@@ -200,6 +200,6 @@ export default function GitlabProjectCreate(props: Readonly<Props>) { | |||
); | |||
} | |||
function transformToOption({ id, name }: GitlabProject): LabelValueSelectOption<string> { | |||
function transformToOption({ id, name }: GitlabProject): LabelValueSelectOption { | |||
return { value: id, label: name }; | |||
} |
@@ -33,10 +33,10 @@ export const MIN_QUERY_LENGTH = 2; | |||
const UNASSIGNED = { value: '', label: translate('unassigned') }; | |||
export interface AssigneeSelectProps { | |||
assignee?: SingleValue<LabelValueSelectOption<string>>; | |||
assignee?: SingleValue<LabelValueSelectOption>; | |||
className?: string; | |||
issues: Issue[]; | |||
onAssigneeSelect: (assignee: SingleValue<LabelValueSelectOption<string>>) => void; | |||
onAssigneeSelect: (assignee: SingleValue<LabelValueSelectOption>) => void; | |||
inputId: string; | |||
} | |||
@@ -54,7 +54,7 @@ interface Props { | |||
interface FormFields { | |||
addTags?: Array<string>; | |||
assignee?: SingleValue<LabelValueSelectOption<string>>; | |||
assignee?: SingleValue<LabelValueSelectOption>; | |||
comment?: string; | |||
notifications?: boolean; | |||
removeTags?: Array<string>; | |||
@@ -126,7 +126,7 @@ export class BulkChangeModal extends React.PureComponent<Props, State> { | |||
return this.props.fetchIssues({ additionalFields: 'actions,transitions', ps: MAX_PAGE_SIZE }); | |||
}; | |||
handleAssigneeSelect = (assignee: SingleValue<LabelValueSelectOption<string>>) => { | |||
handleAssigneeSelect = (assignee: SingleValue<LabelValueSelectOption>) => { | |||
this.setState({ assignee }); | |||
}; | |||
@@ -89,7 +89,7 @@ export default class ApplyTemplate extends React.PureComponent<Props, State> { | |||
} | |||
}; | |||
handlePermissionTemplateChange = ({ value }: LabelValueSelectOption<string>) => { | |||
handlePermissionTemplateChange = ({ value }: LabelValueSelectOption) => { | |||
this.setState({ permissionTemplate: value }); | |||
}; | |||
@@ -45,7 +45,7 @@ export default function ProjectActivityPageFilters(props: ProjectActivityPageFil | |||
const eventTypes = isApp | |||
? Object.values(ApplicationAnalysisEventCategory) | |||
: Object.values(ProjectAnalysisEventCategory); | |||
const options: LabelValueSelectOption<string>[] = eventTypes.map((category) => ({ | |||
const options: LabelValueSelectOption[] = eventTypes.map((category) => ({ | |||
label: translate('event.category', category), | |||
value: category, | |||
})); | |||
@@ -64,7 +64,7 @@ export default function ProjectActivityPageFilters(props: ProjectActivityPageFil | |||
aria-label={translate('project_activity.filter_events')} | |||
className="sw-mr-8 sw-body-sm sw-w-abs-200" | |||
isClearable | |||
onChange={(data: LabelValueSelectOption<string>) => handleCategoryChange(data)} | |||
onChange={(data: LabelValueSelectOption) => handleCategoryChange(data)} | |||
options={options} | |||
placeholder={translate('project_activity.filter_events')} | |||
size="full" |
@@ -57,7 +57,7 @@ export default class PerspectiveSelect extends React.PureComponent<Props> { | |||
<InputSelect | |||
aria-labelledby="aria-projects-perspective" | |||
className="sw-mr-4 sw-body-sm" | |||
onChange={(data: LabelValueSelectOption<string>) => this.handleChange(data)} | |||
onChange={(data: LabelValueSelectOption) => this.handleChange(data)} | |||
options={options} | |||
placeholder={translate('project_activity.filter_events')} | |||
size="small" |
@@ -130,7 +130,7 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S | |||
} | |||
}; | |||
handlePermissionTemplateChange = ({ value }: LabelValueSelectOption<string>) => { | |||
handlePermissionTemplateChange = ({ value }: LabelValueSelectOption) => { | |||
this.setState({ permissionTemplate: value }); | |||
}; | |||
@@ -77,7 +77,9 @@ export default function QualityGatePermissionsAddModalRenderer( | |||
noOptionsMessage={() => translate('no_results')} | |||
onChange={props.onSelection} | |||
loadOptions={props.handleSearch} | |||
getOptionValue={({ value }) => (isUser(value) ? value.login : value.name)} | |||
getOptionValue={({ value }: LabelValueSelectOption<UserBase | UserGroup>) => | |||
isUser(value) ? value.login : value.name | |||
} | |||
controlLabel={renderedSelection} | |||
components={{ | |||
Option, |