Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref(ts) Update MultiSelectControl to typescript and fix types #23766

Merged
merged 2 commits into from
Feb 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions src/sentry/static/sentry/app/components/contextPickerModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import ReactSelect, {components, StylesConfig} from 'react-select';
import {components, StylesConfig} from 'react-select';
import styled from '@emotion/styled';
import createReactClass from 'create-react-class';
import Reflux from 'reflux';
Expand Down Expand Up @@ -100,8 +100,10 @@ class ContextPickerModal extends React.Component<Props> {
}
}

orgSelect: ReactSelect | null = null;
projectSelect: ReactSelect | null = null;
// TODO(ts) The various generics in react-select types make getting this
// right hard.
orgSelect: any | null = null;
projectSelect: any | null = null;

// Performs checks to see if we need to prompt user
// i.e. When there is only 1 org and no project is needed or
Expand Down Expand Up @@ -148,7 +150,7 @@ class ContextPickerModal extends React.Component<Props> {
);
};

doFocus = (ref: ReactSelect | null) => {
doFocus = (ref: any | null) => {
if (!ref || this.props.loading) {
return;
}
Expand Down Expand Up @@ -264,7 +266,7 @@ class ContextPickerModal extends React.Component<Props> {
}
return (
<StyledSelectControl
ref={(ref: ReactSelect) => {
ref={(ref: any) => {
this.projectSelect = ref;
this.focusProjectSelector();
}}
Expand Down Expand Up @@ -310,7 +312,7 @@ class ContextPickerModal extends React.Component<Props> {
{loading && <StyledLoadingIndicator overlay />}
{needOrg && (
<StyledSelectControl
ref={(ref: ReactSelect) => {
ref={(ref: any) => {
this.orgSelect = ref;
if (shouldShowProjectSelector) {
return;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import ReactSelect from 'react-select';

import SelectControl, {ControlProps} from 'app/components/forms/selectControl';
import {SelectValue} from 'app/types';

type Props = Omit<ControlProps, 'onChange'> & {
/**
* Triggered when values change.
*/
onChange?: (value?: SelectValue<any>[] | null) => void;
};

export default React.forwardRef<ReactSelect, Props>(function MultiSelectControl(
props,
ref
) {
return <SelectControl forwardedRef={ref} {...props} multiple />;
});
103 changes: 83 additions & 20 deletions src/sentry/static/sentry/app/components/forms/selectControl.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import React from 'react';
import ReactSelect, {
components as selectComponents,
GroupedOptionsType,
mergeStyles,
OptionsType,
Props as ReactSelectProps,
StylesConfig,
} from 'react-select';
import Async from 'react-select/async';
Expand All @@ -11,12 +14,25 @@ import {withTheme} from 'emotion-theming';

import {IconChevron, IconClose} from 'app/icons';
import space from 'app/styles/space';
import {Choices} from 'app/types';
import {Choices, SelectValue} from 'app/types';
import convertFromSelect2Choices from 'app/utils/convertFromSelect2Choices';
import {Theme} from 'app/utils/theme';

import SelectControlLegacy from './selectControlLegacy';

function isGroupedOptions<OptionType>(
maybe:
| ReturnType<typeof convertFromSelect2Choices>
| GroupedOptionsType<OptionType>
| OptionType[]
| OptionsType<OptionType>
): maybe is GroupedOptionsType<OptionType> {
if (!maybe || maybe.length === 0) {
return false;
}
return (maybe as GroupedOptionsType<OptionType>)[0].options !== undefined;
}

const ClearIndicator = (
props: React.ComponentProps<typeof selectComponents.ClearIndicator>
) => (
Expand All @@ -41,30 +57,62 @@ const MultiValueRemove = (
</selectComponents.MultiValueRemove>
);

type ControlProps = React.ComponentProps<typeof ReactSelect> & {
theme: Theme;
/**
* Ref forwarded into ReactSelect component.
* The any is inherited from react-select.
*/
forwardedRef: React.Ref<ReactSelect>;
export type ControlProps<OptionType = GeneralSelectValue> = Omit<
ReactSelectProps<OptionType>,
'onChange' | 'value'
> & {
/**
* Set to true to prefix selected values with content
*/
inFieldLabel?: string;
/**
* Backwards compatible shim to work with select2 style choice type.
*/
choices?: Choices | ((props: ControlProps) => Choices);
choices?: Choices | ((props: ControlProps<OptionType>) => Choices);
/**
* Use react-select v2. Deprecated, don't make more of this.
*/
deprecatedSelectControl?: boolean;
/**
* Used by MultiSelectControl.
*/
multiple?: boolean;
/**
* Handler for changes. Narrower than the types in react-select.
*/
onChange?: (value?: OptionType | null) => void;
/**
* Unlike react-select which expects an OptionType as its value
* we accept the option.value and resolve the option object.
* Because this type is embedded in the OptionType generic we
* can't have a good type here.
*/
value?: any;
};

/**
* Additional props provided by forwardRef and withTheme()
*/
type WrappedControlProps<OptionType> = ControlProps<OptionType> & {
theme: Theme;
/**
* Ref forwarded into ReactSelect component.
* The any is inherited from react-select.
*/
forwardedRef: React.Ref<ReactSelect>;
};

type LegacyProps = React.ComponentProps<typeof SelectControlLegacy>;

function SelectControl(props: ControlProps) {
// TODO(ts) The exported component uses forwardRef.
// This means we cannot fill the SelectValue generic
// at the call site. We use `any` here to avoid type errors with select
// controls that have custom option structures
type GeneralSelectValue = SelectValue<any>;

function SelectControl<OptionType extends GeneralSelectValue = GeneralSelectValue>(
props: WrappedControlProps<OptionType>
) {
// TODO(epurkhiser): We should remove all SelectControls (and SelectFields,
// SelectAsyncFields, etc) that are using this prop, before we can remove the
// v1 react-select component.
Expand Down Expand Up @@ -262,7 +310,15 @@ function SelectControl(props: ControlProps) {
* because the select component fetches the options finding the mappedValue will fail
* and the component won't work
*/
const flatOptions = choicesOrOptions.flatMap(option => option.options || option);
let flatOptions: any[] = [];
if (isGroupedOptions<OptionType>(choicesOrOptions)) {
flatOptions = choicesOrOptions.flatMap(option => option.options);
} else {
// @ts-ignore The types used in react-select generics (OptionType) don't
// line up well with our option type (SelectValue). We need to do more work
// to get these types to align.
flatOptions = choicesOrOptions.flatMap(option => option);
}
mappedValue =
props.multiple && Array.isArray(value)
? value.map(val => flatOptions.find(option => option.value === val))
Expand Down Expand Up @@ -299,7 +355,7 @@ function SelectControl(props: ControlProps) {
};

return (
<SelectPicker
<SelectPicker<OptionType>
styles={mappedStyles}
components={{...replacedComponents, ...components}}
async={async}
Expand All @@ -309,7 +365,7 @@ function SelectControl(props: ControlProps) {
value={mappedValue}
isMulti={props.multiple || props.multi}
isDisabled={props.isDisabled || props.disabled}
options={choicesOrOptions}
options={options || (choicesOrOptions as OptionsType<OptionType>)}
openMenuOnFocus={props.openMenuOnFocus === undefined ? true : props.openMenuOnFocus}
{...rest}
/>
Expand All @@ -319,7 +375,7 @@ SelectControl.propTypes = SelectControlLegacy.propTypes;

const SelectControlWithTheme = withTheme(SelectControl);

type PickerProps = ControlProps & {
type PickerProps<OptionType> = ControlProps<OptionType> & {
/**
* Enable async option loading.
*/
Expand All @@ -334,7 +390,12 @@ type PickerProps = ControlProps & {
clearable?: boolean;
};

function SelectPicker({async, creatable, forwardedRef, ...props}: PickerProps) {
function SelectPicker<OptionType>({
async,
creatable,
forwardedRef,
...props
}: PickerProps<OptionType>) {
// Pick the right component to use
// Using any here as react-select types also use any
let Component: React.ComponentType<any> | undefined;
Expand All @@ -353,11 +414,13 @@ function SelectPicker({async, creatable, forwardedRef, ...props}: PickerProps) {

SelectPicker.propTypes = SelectControl.propTypes;

const RefForwardedSelectControl = React.forwardRef<ReactSelect, ControlProps>(
function RefForwardedSelectControl(props, ref) {
return <SelectControlWithTheme forwardedRef={ref} {...props} />;
}
);
// The generics need to be filled here as forwardRef can't expose generics.
const RefForwardedSelectControl = React.forwardRef<
ReactSelect<GeneralSelectValue>,
ControlProps<GeneralSelectValue>
>(function RefForwardedSelectControl(props, ref) {
return <SelectControlWithTheme forwardedRef={ref} {...props} />;
});

// TODO(ts): Needed because <SelectField> uses this
RefForwardedSelectControl.propTypes = SelectControl.propTypes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ class InviteMembersModal extends AsyncComponent<Props, State> {
i
)
}
onChangeRole={({value}) => this.setRole(value, i)}
onChangeRole={value => this.setRole(value?.value, i)}
onChangeTeams={opts => this.setTeams(opts ? opts.map(v => v.value) : [], i)}
disableRemove={disableInputs || pendingInvites.length === 1}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {MemberRole, SelectValue, Team} from 'app/types';
import renderEmailValue from './renderEmailValue';
import {InviteStatus} from './types';

type SelectOption = SelectValue<string>;

type Props = {
className?: string;
disabled: boolean;
Expand All @@ -23,9 +25,9 @@ type Props = {
inviteStatus: InviteStatus;
onRemove: () => void;

onChangeEmails: (emails: SelectValue<string>[]) => void;
onChangeRole: (role: SelectValue<string>) => void;
onChangeTeams: (teams: null | SelectValue<string>[]) => void;
onChangeEmails: (emails: SelectOption[]) => void;
onChangeRole: (role: SelectOption) => void;
onChangeTeams: (teams?: SelectOption[] | null) => void;
};

const InviteRowControl = ({
Expand Down
18 changes: 11 additions & 7 deletions src/sentry/static/sentry/app/components/roleSelectControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@ import React from 'react';
import {components, OptionProps} from 'react-select';
import styled from '@emotion/styled';

import SelectControl from 'app/components/forms/selectControl';
import SelectControl, {ControlProps} from 'app/components/forms/selectControl';
import space from 'app/styles/space';
import {MemberRole} from 'app/types';
import theme from 'app/utils/theme';

type Props = React.ComponentProps<typeof SelectControl> & {
roles: MemberRole[];
disableUnallowed: boolean;
value?: string;
};

type OptionType = {
label: string;
value: string;
disabled: boolean;
description: string;
};

type Props = Omit<ControlProps<OptionType>, 'onChange' | 'value'> & {
roles: MemberRole[];
disableUnallowed: boolean;
value?: string;
/**
* Narrower type than SelectControl because there is no empty value
*/
onChange?: (value: OptionType) => void;
};

function RoleSelectControl({roles, disableUnallowed, ...props}: Props) {
return (
<SelectControl
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import ReactSelect from 'react-select';
import styled from '@emotion/styled';
import debounce from 'lodash/debounce';

Expand Down Expand Up @@ -87,7 +86,8 @@ class SelectMembers extends React.Component<Props, State> {
this.unlisteners.forEach(callIfFunction);
}

selectRef = React.createRef<ReactSelect>();
// TODO(ts) This type could be improved when react-select types are better.
selectRef = React.createRef<any>();

unlisteners = [
MemberListStore.listen(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ function isStringList(maybe: string[] | Choices): maybe is string[] {

/**
* Converts arg from a `select2` choices array to a `react-select` `options` array
* This contains some any hacks as this is creates type errors with the generics
* used in SelectControl as the generics conflict with the concrete types here.
*/
const convertFromSelect2Choices = (choices: Input): SelectValue<any>[] | null => {
const convertFromSelect2Choices = (choices: Input): SelectValue<any>[] | undefined => {
// TODO(ts): This is to make sure that this function is backwards compatible, ideally,
// this function only accepts arrays
if (!Array.isArray(choices)) {
return null;
return undefined;
}
if (isStringList(choices)) {
return choices.map(choice => ({value: choice, label: choice}));
Expand Down
Loading