diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bcc7ed21e7..9fca531bc43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `` can now dynamically get country flag and group all countrys #8996 ### Fixed: +- Typesafety of the ` diff --git a/packages/manager/src/components/EnhancedSelect/Select.styles.ts b/packages/manager/src/components/EnhancedSelect/Select.styles.ts index 36eddbd1c2a..3dc88dd5a7b 100644 --- a/packages/manager/src/components/EnhancedSelect/Select.styles.ts +++ b/packages/manager/src/components/EnhancedSelect/Select.styles.ts @@ -1,345 +1,318 @@ -import { createStyles } from '@mui/styles'; +import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; -export type ClassNames = - | 'root' - | 'input' - | 'noOptionsMessage' - | 'divider' - | 'suggestionRoot' - | 'highlight' - | 'suggestionItem' - | 'suggestionIcon' - | 'suggestionTitle' - | 'suggestionDescription' - | 'resultContainer' - | 'tagContainer' - | 'selectedMenuItem' - | 'medium' - | 'small' - | 'noMarginTop' - | 'inline' - | 'hideLabel' - | 'algoliaRoot' - | 'label' - | 'source' - | 'icon' - | 'row' - | 'finalLink'; - -export const styles = (theme: Theme) => - createStyles({ - '@keyframes dash': { - to: { - 'stroke-dashoffset': 0, - }, +export const useStyles = makeStyles((theme: Theme) => ({ + '@keyframes dash': { + to: { + 'stroke-dashoffset': 0, }, - root: { - width: '100%', - position: 'relative', - '& .react-select__control': { - borderRadius: 0, - boxShadow: 'none', - border: `1px solid transparent`, - backgroundColor: theme.bg.white, - minHeight: `calc(${theme.spacing(5)} - 2)`, - '&:hover': { - border: `1px dotted #ccc`, - cursor: 'text', - }, - '&--is-focused, &--is-focused:hover': { - border: `1px dotted #999`, - }, + }, + root: { + width: '100%', + position: 'relative', + '& .react-select__control': { + borderRadius: 0, + boxShadow: 'none', + border: `1px solid transparent`, + backgroundColor: theme.bg.white, + minHeight: `calc(${theme.spacing(5)} - 2)`, + '&:hover': { + border: `1px dotted #ccc`, + cursor: 'text', }, - '& .react-select__value-container': { - width: '100%', - '& > div': { - width: '100%', - }, - '&.react-select__value-container--is-multi': { - '& > div, & .react-select__input': { - width: 'auto', - }, - }, + '&--is-focused, &--is-focused:hover': { + border: `1px dotted #999`, }, - '& .react-select__input': { + }, + '& .react-select__value-container': { + width: '100%', + '& > div': { width: '100%', - color: theme.palette.text.primary, - }, - '& .react-select__menu': { - margin: '-1px 0 0 0', - borderRadius: 0, - boxShadow: 'none', - border: `1px solid ${theme.palette.primary.main}`, - maxWidth: 415, - zIndex: 100, }, - '& .react-select__group': { - width: '100%', - '&:last-child': { - paddingBottom: 0, + '&.react-select__value-container--is-multi': { + '& > div, & .react-select__input': { + width: 'auto', }, }, - '& .react-select__group-heading': { - textTransform: 'initial', - fontSize: '1rem', - color: theme.color.headline, - fontFamily: theme.font.bold, - paddingLeft: 10, - paddingRight: 10, - }, - '& .react-select__menu-list': { - zIndex: 100, - padding: theme.spacing(0.5), - backgroundColor: theme.bg.white, - height: '101%', - overflow: 'auto', - maxHeight: 285, - '&::-webkit-scrollbar': { - appearance: 'none', - }, - '&::-webkit-scrollbar:vertical': { - width: 8, - }, - '&::-webkit-scrollbar-thumb': { - borderRadius: 8, - backgroundColor: '#ccc', - }, - }, - '& .react-select__option': { - transition: theme.transitions.create(['background-color', 'color']), - color: theme.palette.text.primary, - backgroundColor: theme.bg.white, - cursor: 'pointer', - padding: '10px', - fontSize: '0.9rem', - [theme.breakpoints.only('xs')]: { - fontSize: '1rem', - }, - '& svg': { - marginTop: 2, - }, - }, - '& .react-select__option--is-focused': { - backgroundColor: theme.palette.primary.main, - color: 'white', - }, - '& .react-select__option--is-selected': { - color: theme.palette.primary.main, - '&.react-select__option--is-focused': { - backgroundColor: theme.bg.white, - }, - }, - '& .react-select__option--is-disabled': { - opacity: 0.5, - cursor: 'initial', - }, - '& .react-select__single-value': { - color: theme.palette.text.primary, - overflow: 'hidden', - }, - '& .react-select__indicator-separator': { - display: 'none', - }, - '& .react-select__multi-value': { - borderRadius: 4, - backgroundColor: theme.bg.lightBlue1, - alignItems: 'center', - }, - '& .react-select__multi-value__label': { - color: theme.palette.text.primary, - fontSize: '.8rem', - height: 20, - marginTop: 2, - marginBottom: 2, - marginRight: 4, - paddingLeft: 6, - paddingRight: 0, + }, + '& .react-select__input': { + width: '100%', + color: theme.palette.text.primary, + }, + '& .react-select__menu': { + margin: '-1px 0 0 0', + borderRadius: 0, + boxShadow: 'none', + border: `1px solid ${theme.palette.primary.main}`, + maxWidth: 415, + zIndex: 100, + }, + '& .react-select__group': { + width: '100%', + '&:last-child': { + paddingBottom: 0, }, - '& .react-select__clear-indicator': { - padding: 0, - '& svg': { - color: theme.color.grey4, - '&:hover': { - color: theme.palette.primary.main, - }, - }, + }, + '& .react-select__group-heading': { + textTransform: 'initial', + fontSize: '1rem', + color: theme.color.headline, + fontFamily: theme.font.bold, + paddingLeft: 10, + paddingRight: 10, + }, + '& .react-select__menu-list': { + zIndex: 100, + padding: theme.spacing(0.5), + backgroundColor: theme.bg.white, + height: '101%', + overflow: 'auto', + maxHeight: 285, + '&::-webkit-scrollbar': { + appearance: 'none', }, - '& .react-select__multi-value__remove': { - backgroundColor: 'transparent', - borderRadius: '50%', - padding: 2, - marginLeft: 4, - marginRight: 4, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - '& svg': { - color: theme.palette.text.primary, - width: 12, - height: 12, - }, - '&:hover': { - backgroundColor: theme.palette.primary.main, - '& svg': { - color: 'white', - }, - }, + '&::-webkit-scrollbar:vertical': { + width: 8, }, - '& .react-select__dropdown-indicator': {}, - '& [class*="MuiFormHelperText-error"]': { - paddingBottom: theme.spacing(1), + '&::-webkit-scrollbar-thumb': { + borderRadius: 8, + backgroundColor: '#ccc', }, }, - input: { + '& .react-select__option': { + transition: theme.transitions.create(['background-color', 'color']), + color: theme.palette.text.primary, + backgroundColor: theme.bg.white, + cursor: 'pointer', + padding: '10px', fontSize: '0.9rem', [theme.breakpoints.only('xs')]: { fontSize: '1rem', }, - padding: 0, - display: 'flex', - color: theme.palette.text.primary, - cursor: 'pointer', - minHeight: `calc(${theme.spacing(5)} - 6)`, - // From the AutoSizeInput documentation: (https://github.com/JedWatson/react-input-autosize/blob/master/README.md#csp-and-the-ie-clear-indicator) - // "The input will automatically inject a stylesheet that hides IE/Edge's "clear" indicator, - // which otherwise breaks the UI. This has the downside of being incompatible with some CSP policies. - // To work around this, you can pass the injectStyles={false} prop, but if you do this I strongly - // recommend targeting the input element in your own stylesheet with the following rule:" - '::-ms-clear': { display: 'none' }, - }, - noOptionsMessage: { - padding: `${theme.spacing(1)} ${theme.spacing(2)}`, + '& svg': { + marginTop: 2, + }, }, - divider: { - height: theme.spacing(2), + '& .react-select__option--is-focused': { + backgroundColor: theme.palette.primary.main, + color: 'white', }, - suggestionRoot: { - cursor: 'pointer', - width: 'calc(100% + 2px)', - alignItems: 'space-between', - justifyContent: 'space-between', - borderBottom: `1px solid ${theme.palette.divider}`, - [theme.breakpoints.up('md')]: { - display: 'flex', - }, - '&:last-child': { - borderBottom: 0, + '& .react-select__option--is-selected': { + color: theme.palette.primary.main, + '&.react-select__option--is-focused': { + backgroundColor: theme.bg.white, }, }, - highlight: { - color: theme.palette.primary.main, + '& .react-select__option--is-disabled': { + opacity: 0.5, + cursor: 'initial', }, - suggestionItem: { - padding: theme.spacing(), + '& .react-select__single-value': { + color: theme.palette.text.primary, + overflow: 'hidden', }, - suggestionIcon: { - display: 'flex', + '& .react-select__indicator-separator': { + display: 'none', + }, + '& .react-select__multi-value': { + borderRadius: 4, + backgroundColor: theme.bg.lightBlue1, alignItems: 'center', - justifyContent: 'center', - marginLeft: theme.spacing(1.5), }, - suggestionTitle: { - fontSize: '1rem', + '& .react-select__multi-value__label': { color: theme.palette.text.primary, - wordBreak: 'break-all', - fontWeight: 600, - }, - suggestionDescription: { - color: theme.color.headline, - fontSize: '.75rem', + fontSize: '.8rem', + height: 20, marginTop: 2, + marginBottom: 2, + marginRight: 4, + paddingLeft: 6, + paddingRight: 0, }, - resultContainer: { - display: 'flex', - flexFlow: 'row nowrap', + '& .react-select__clear-indicator': { + padding: 0, + '& svg': { + color: theme.color.grey4, + '&:hover': { + color: theme.palette.primary.main, + }, + }, }, - tagContainer: { + '& .react-select__multi-value__remove': { + backgroundColor: 'transparent', + borderRadius: '50%', + padding: 2, + marginLeft: 4, + marginRight: 4, display: 'flex', - flexWrap: 'wrap', - paddingRight: 8, - justifyContent: 'flex-end', alignItems: 'center', - '& > div': { - margin: '2px', - }, - }, - selectedMenuItem: { - backgroundColor: `${theme.bg.main} !important`, - '& .tag': { - backgroundColor: theme.bg.lightBlue1, + justifyContent: 'center', + '& svg': { color: theme.palette.text.primary, - '&:hover': { - backgroundColor: theme.palette.primary.main, + width: 12, + height: 12, + }, + '&:hover': { + backgroundColor: theme.palette.primary.main, + '& svg': { color: 'white', }, }, }, - medium: { - minHeight: 40, + '& .react-select__dropdown-indicator': {}, + '& [class*="MuiFormHelperText-error"]': { + paddingBottom: theme.spacing(1), }, - small: { - minHeight: 35, - minWidth: 'auto', + }, + input: { + fontSize: '0.9rem', + [theme.breakpoints.only('xs')]: { + fontSize: '1rem', }, - inline: { - display: 'inline-flex', - flexDirection: 'row', - alignItems: 'center', - '& label': { - marginRight: theme.spacing(1), - whiteSpace: 'nowrap', - position: 'relative', - top: 1, - }, + padding: 0, + display: 'flex', + color: theme.palette.text.primary, + cursor: 'pointer', + minHeight: `calc(${theme.spacing(5)} - 6)`, + // From the AutoSizeInput documentation: (https://github.com/JedWatson/react-input-autosize/blob/master/README.md#csp-and-the-ie-clear-indicator) + // "The input will automatically inject a stylesheet that hides IE/Edge's "clear" indicator, + // which otherwise breaks the UI. This has the downside of being incompatible with some CSP policies. + // To work around this, you can pass the injectStyles={false} prop, but if you do this I strongly + // recommend targeting the input element in your own stylesheet with the following rule:" + '::-ms-clear': { display: 'none' }, + }, + noOptionsMessage: { + padding: `${theme.spacing(1)} ${theme.spacing(2)}`, + }, + divider: { + height: theme.spacing(2), + }, + suggestionRoot: { + cursor: 'pointer', + width: 'calc(100% + 2px)', + alignItems: 'space-between', + justifyContent: 'space-between', + borderBottom: `1px solid ${theme.palette.divider}`, + [theme.breakpoints.up('md')]: { + display: 'flex', }, - hideLabel: { - '& label': { ...theme.visually.hidden }, + '&:last-child': { + borderBottom: 0, }, - algoliaRoot: { - width: '100%', - cursor: 'pointer', - padding: `calc(${theme.spacing(1)} / 2 + 2)`, - '& em': { - fontStyle: 'normal', - color: theme.color.blueDTwhite, - }, + }, + highlight: { + color: theme.palette.primary.main, + }, + suggestionItem: { + padding: theme.spacing(), + }, + suggestionIcon: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginLeft: theme.spacing(1.5), + }, + suggestionTitle: { + fontSize: '1rem', + color: theme.palette.text.primary, + wordBreak: 'break-all', + fontWeight: 600, + }, + suggestionDescription: { + color: theme.color.headline, + fontSize: '.75rem', + marginTop: 2, + }, + resultContainer: { + display: 'flex', + flexFlow: 'row nowrap', + }, + tagContainer: { + display: 'flex', + flexWrap: 'wrap', + paddingRight: 8, + justifyContent: 'flex-end', + alignItems: 'center', + '& > div': { + margin: '2px', }, - label: { - display: 'inline', + }, + selectedMenuItem: { + backgroundColor: `${theme.bg.main} !important`, + '& .tag': { + backgroundColor: theme.bg.lightBlue1, color: theme.palette.text.primary, - maxWidth: '95%', + '&:hover': { + backgroundColor: theme.palette.primary.main, + color: 'white', + }, }, - icon: { - display: 'inline-block', - width: 12, - height: 12, + }, + medium: { + minHeight: 40, + }, + small: { + minHeight: 35, + minWidth: 'auto', + }, + inline: { + display: 'inline-flex', + flexDirection: 'row', + alignItems: 'center', + '& label': { + marginRight: theme.spacing(1), + whiteSpace: 'nowrap', position: 'relative', - top: 5, - marginLeft: theme.spacing(0.5), - marginRight: theme.spacing(0.5), - color: theme.palette.primary.main, - }, - source: { - marginTop: `calc(${theme.spacing(1)} / 4)`, - color: theme.color.headline, - paddingLeft: theme.spacing(1), - margin: 0, - }, - row: { - display: 'flex', - width: '100%', - alignItems: 'center', - justifyContent: 'space-between', - paddingLeft: theme.spacing(1), + top: 1, }, - finalLink: { - display: 'flex', - alignItems: 'center', - fontSize: '1.2em', - paddingLeft: theme.spacing(1), + }, + hideLabel: { + '& label': { ...theme.visually.hidden }, + }, + algoliaRoot: { + width: '100%', + cursor: 'pointer', + padding: `calc(${theme.spacing(1)} / 2 + 2)`, + '& em': { + fontStyle: 'normal', + color: theme.color.blueDTwhite, }, - }); + }, + label: { + display: 'inline', + color: theme.palette.text.primary, + maxWidth: '95%', + }, + icon: { + display: 'inline-block', + width: 12, + height: 12, + position: 'relative', + top: 5, + marginLeft: theme.spacing(0.5), + marginRight: theme.spacing(0.5), + color: theme.palette.primary.main, + }, + source: { + marginTop: `calc(${theme.spacing(1)} / 4)`, + color: theme.color.headline, + paddingLeft: theme.spacing(1), + margin: 0, + }, + row: { + display: 'flex', + width: '100%', + alignItems: 'center', + justifyContent: 'space-between', + paddingLeft: theme.spacing(1), + }, + finalLink: { + display: 'flex', + alignItems: 'center', + fontSize: '1.2em', + paddingLeft: theme.spacing(1), + }, +})); // @todo @tdt: Replace the class name based styles above with these. They're (mostly) copied over, // as they're needed for one specific case: where a Select component appears on a Dialog. To reduce diff --git a/packages/manager/src/components/EnhancedSelect/Select.tsx b/packages/manager/src/components/EnhancedSelect/Select.tsx index 3e4d3a4a7e2..1cc43122ae1 100644 --- a/packages/manager/src/components/EnhancedSelect/Select.tsx +++ b/packages/manager/src/components/EnhancedSelect/Select.tsx @@ -1,14 +1,15 @@ -import classNames from 'classnames'; import * as React from 'react'; -import ReactSelect, { Props as SelectProps } from 'react-select'; +import classNames from 'classnames'; +import ReactSelect, { + ActionMeta, + NamedProps as SelectProps, + ValueType, +} from 'react-select'; import CreatableSelect, { - Props as CreatableSelectProps, + CreatableProps as CreatableSelectProps, } from 'react-select/creatable'; -import { withStyles, WithStyles, withTheme, WithTheme } from '@mui/styles'; import { Props as TextFieldProps } from 'src/components/TextField'; import { convertToKebabCase } from 'src/utilities/convertToKebobCase'; -/* TODO will be refactoring enhanced select to be an abstraction. -Styles added in this file and the below imports will be utilized for the abstraction. */ import DropdownIndicator from './components/DropdownIndicator'; import Input from './components/Input'; import LoadingIndicator from './components/LoadingIndicator'; @@ -19,7 +20,8 @@ import NoOptionsMessage from './components/NoOptionsMessage'; import Option from './components/Option'; import Control from './components/SelectControl'; import Placeholder from './components/SelectPlaceholder'; -import { ClassNames, styles, reactSelectStyles } from './Select.styles'; +import { reactSelectStyles, useStyles } from './Select.styles'; +import { Theme, useTheme } from '@mui/material'; export interface Item { value: T; @@ -32,17 +34,6 @@ export interface GroupType { options: Item[]; } -export interface SelectState { - data: any; - isDisabled: boolean; - isFocused: boolean; - isSelected: boolean; -} - -interface ActionMeta { - action: string; -} - export interface NoOptionsMessageProps { inputValue: string; } @@ -62,26 +53,18 @@ const _components = { Input, }; -interface OwnProps { - // Set this prop to `true` when using a to the - // document body directly, so the overflow is visible over the edge of the modal. - overflowPortal?: boolean; -} - -type CombinedProps = OwnProps & - WithStyles & - BaseSelectProps & - CreatableProps & - WithTheme; - // We extend TexFieldProps to still be able to pass // the required label to Select and not duplicated it to TextFieldProps interface ModifiedTextFieldProps extends Omit { label?: string; } -export interface BaseSelectProps - extends Omit, 'onChange' | 'value' | 'onFocus'> { +export interface BaseSelectProps< + I extends Item, + IsMulti extends boolean = false, + Clearable extends boolean = false +> extends Omit, 'onChange'>, + CreatableSelectProps { classes?: any; /* textFieldProps isn't native to react-select @@ -97,18 +80,18 @@ export interface BaseSelectProps /** * We require label for accessibility purpose */ - label: string; - /** alias for isDisabled */ - disabled?: boolean; - /** retyped this */ - value?: Item | Item[] | null; - /** making this required */ - onChange: (selected: Item | Item[] | null, actionMeta?: ActionMeta) => void; - /** alias for onCreateOption */ - createNew?: (inputValue: string) => void; - loadOptions?: (inputValue: string) => Promise | undefined; - onFocus?: any; + label?: string; + + /** onChange is called when the user selectes a new value / new values */ + onChange: Clearable extends true // if the Select is NOT clearable, the value passed in the onChange function must be defined + ? Exclude['onChange'], undefined> + : ( + value: Exclude, null | undefined>, + action: ActionMeta + ) => void; + /** the rest are props we've added ourselves */ + disabled?: boolean; medium?: boolean; small?: boolean; noMarginTop?: boolean; @@ -120,11 +103,54 @@ export interface BaseSelectProps required?: boolean; creatable?: boolean; variant?: 'creatable'; + isClearable?: Clearable; + // Set this prop to `true` when using a to the + // document body directly, so the overflow is visible over the edge of the modal. + overflowPortal?: boolean; } -interface CreatableProps extends CreatableSelectProps {} +const Select = < + I extends Item, + IsMulti extends boolean = false, + Clearable extends boolean = false +>( + props: BaseSelectProps +) => { + const theme = useTheme(); + const classes = useStyles(); + const { + className, + components, + errorText, + filterOption, + label, + isClearable, + isMulti, + isLoading, + placeholder, + onChange, + onInputChange, + options, + value, + noOptionsMessage, + onMenuClose, + onBlur, + blurInputOnSelect, + medium, + small, + noMarginTop, + textFieldProps, + inline, + hideLabel, + errorGroup, + onFocus, + inputId, + overflowPortal, + required, + creatable, + ...restOfProps + } = props; -class Select extends React.PureComponent { // React-Select changed the behavior of clearing isMulti Selects in v3. // Previously, once the Select was empty, the value was `[]`. Now, it is `null`. // This breaks many of our components, which rely on e.g. mapping through the value (which is @@ -132,157 +158,112 @@ class Select extends React.PureComponent { // // This essentially reverts the behavior of the v3 React-Select update. Long term, we should // probably re-write our component handlers to expect EITHER an array OR `null`. - _onChange = (selected: Item | Item[] | null, actionMeta?: ActionMeta) => { - const { isMulti, onChange } = this.props; - + const _onChange = ( + selected: ValueType, + actionMeta: ActionMeta + ) => { if (isMulti && !selected) { - return onChange([], actionMeta); + // @ts-expect-error I'm sorry, but trust me I made this component much better + onChange([], actionMeta); + } else { + // @ts-expect-error I'm sorry, but trust me I made this component much better + onChange(selected, actionMeta); } - - onChange(selected, actionMeta); }; - render() { - const { - classes, - className, - components, - createNew, - disabled, - errorText, - filterOption, - label, - loadOptions, - isClearable, - isMulti, - isLoading, - placeholder, - onChange, - onInputChange, - options, - value, - noOptionsMessage, - onMenuClose, - onBlur, - blurInputOnSelect, - medium, - small, - noMarginTop, - textFieldProps, - inline, - hideLabel, - errorGroup, - onFocus, - inputId, - overflowPortal, - theme, - required, - creatable, - ...restOfProps - } = this.props; - - /* - * By default, we use the built-in Option component from React-Select, along with several Material-UI based - * components (listed in the _components variable above). To customize the select in a particular instance - * (for example, to render more complicated options for search bars), provide the component to use in a prop - * Object. Specify the name of the component to override as the object key, with the component to use in its - * place as the value. Full list of available components to override is available at - * http://react-select.com/components#replaceable-components. As an example, to provide a custom option component, use: - * to the document body directly, none of our CSS - // targeting will work, so we have to supply the styles as a prop. - restOfProps.styles = reactSelectStyles(theme); - } + if (creatable) { + restOfProps.variant = 'creatable'; + } - return ( - 'No results')} - menuPlacement={this.props.menuPlacement || 'auto'} - onMenuClose={onMenuClose} - onFocus={onFocus} - /> - ); + if (overflowPortal) { + restOfProps.menuPortalTarget = document.body; + // Since we're attaching the - Boolean(option.disabledMessage) - } - styles={styles || selectStyles} - textFieldProps={{ - tooltipText: helperText, - }} - required={required} - {...restOfReactSelectProps} - /> - - ); -}); + return ( +
+ - - ) : null} - - ); + // Add "Show All" to the list of options if the consumer has so specified. + if (showAll) { + finalOptions.push({ label: 'Show All', value: Infinity }); } -} -export default withStyles(styles)(PaginationFooter); + const defaultPagination = finalOptions.find((eachOption) => { + return eachOption.value === pageSize; + }); + + // If "Show All" is currently selected, pageSize is `Infinity`. + const isShowingAll = pageSize === Infinity; + + return ( + + + {!isShowingAll && ( + + )} + + {!fixedSize ? ( + + { errorText={error} value={value} onChange={onChange} - createNew={createTag} + onCreateOption={createTag} noOptionsMessage={getEmptyMessage} - disabled={disabled} + isDisabled={disabled} menuPlacement={menuPlacement} /> ); diff --git a/packages/manager/src/components/TagsPanel/TagsPanel.tsx b/packages/manager/src/components/TagsPanel/TagsPanel.tsx index d521b74be59..12a3452d12c 100644 --- a/packages/manager/src/components/TagsPanel/TagsPanel.tsx +++ b/packages/manager/src/components/TagsPanel/TagsPanel.tsx @@ -137,7 +137,6 @@ const TagsPanel = (props: TagsPanelProps) => { const [tagError, setTagError] = React.useState(''); const [isCreatingTag, setIsCreatingTag] = React.useState(false); - const [tagInputValue, setTagInputValue] = React.useState(''); const [tagsLoading, setTagsLoading] = React.useState(false); const { data: profile } = useProfile(); @@ -225,8 +224,6 @@ const TagsPanel = (props: TagsPanelProps) => { setTagsLoading(true); updateTags([...tags, value.label].sort()) .then(() => { - // set the input value to blank on submit - setTagInputValue(''); if (userTags) { updateTagsSuggestionsData([...userTags, value], queryClient); } @@ -255,7 +252,6 @@ const TagsPanel = (props: TagsPanelProps) => { placeholder="Create or Select a Tag" label="Create or Select a Tag" hideLabel - value={tagInputValue} createOptionPosition="first" className={classes.selectTag} escapeClearsValue diff --git a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/UpdateContactInformationForm/UpdateContactInformationForm.tsx b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/UpdateContactInformationForm/UpdateContactInformationForm.tsx index d880c94c060..b73552bb0fa 100644 --- a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/UpdateContactInformationForm/UpdateContactInformationForm.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/UpdateContactInformationForm/UpdateContactInformationForm.tsx @@ -228,9 +228,7 @@ const UpdateContactInformationForm = ({ onClose, focusEmail }: Props) => { label="Country" errorText={errorMap.country} isClearable={false} - onChange={(item: Item) => - formik.setFieldValue('country', item.value) - } + onChange={(item) => formik.setFieldValue('country', item.value)} options={countryResults} placeholder="Select a Country" required={flags.regionDropdown} @@ -251,9 +249,7 @@ const UpdateContactInformationForm = ({ onClose, focusEmail }: Props) => { label={`${formik.values.country === 'US' ? 'State' : 'Province'}`} errorText={errorMap.state} isClearable={false} - onChange={(item: Item) => - formik.setFieldValue('state', item.value) - } + onChange={(item) => formik.setFieldValue('state', item.value)} options={filteredRegionResults} placeholder={ formik.values.country === 'US' ? 'state' : 'province' @@ -262,10 +258,12 @@ const UpdateContactInformationForm = ({ onClose, focusEmail }: Props) => { value={ filteredRegionResults.find( ({ value }) => value === formik.values.state - ) ?? '' + ) ?? null } textFieldProps={{ - 'data-qa-contact-state-province': true, + dataAttrs: { + 'data-qa-contact-state-province': true, + }, }} /> ) : ( diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/MaintenanceWindow.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/MaintenanceWindow.tsx index 368a5b768bb..fc1a171962f 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/MaintenanceWindow.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/MaintenanceWindow.tsx @@ -80,7 +80,7 @@ export const MaintenanceWindow = (props: Props) => { const [ modifiedWeekSelectionMap, setModifiedWeekSelectionMap, - ] = React.useState([]); + ] = React.useState[]>([]); const { classes } = useStyles(); const { enqueueSnackbar } = useSnackbar(); @@ -90,7 +90,10 @@ export const MaintenanceWindow = (props: Props) => { database.id ); - const weekSelectionModifier = (day: string, weekSelectionMap: Item[]) => { + const weekSelectionModifier = ( + day: string, + weekSelectionMap: Item[] + ) => { const modifiedMap = weekSelectionMap.map((weekSelectionElement) => { return { label: `${weekSelectionElement.label} ${day} of each month`, @@ -139,14 +142,6 @@ export const MaintenanceWindow = (props: Props) => { }); }; - const scheduledUpdateDay = daySelectionMap.find( - (thisOption) => thisOption.value === database.updates?.day_of_week - ); - - const scheduledUpdateHour = hourSelectionMap.find( - (thisOption) => thisOption.value === database.updates?.hour_of_day - ); - const utcOffsetInHours = timezone ? DateTime.fromISO(new Date().toISOString(), { zone: timezone }).offset / 60 : DateTime.now().offset / 60; @@ -205,11 +200,13 @@ export const MaintenanceWindow = (props: Props) => { }, }} options={daySelectionMap} - defaultValue={scheduledUpdateDay?.value ?? 1} + defaultValue={daySelectionMap.find( + (option) => option.value === 1 + )} value={daySelectionMap.find( (thisOption) => thisOption.value === values.day_of_week )} - onChange={(e: Item) => { + onChange={(e) => { setFormTouched(true); setFieldValue('day_of_week', e.value); weekSelectionModifier(e.label, weekSelectionMap); @@ -225,7 +222,6 @@ export const MaintenanceWindow = (props: Props) => { isClearable={false} menuPlacement="top" name="Day of Week" - error={touched.day_of_week && Boolean(errors.day_of_week)} errorText={touched.day_of_week ? errors.day_of_week : undefined} noMarginTop /> @@ -239,20 +235,21 @@ export const MaintenanceWindow = (props: Props) => { }, }} options={hourSelectionMap} - defaultValue={scheduledUpdateHour?.value ?? 20} + defaultValue={hourSelectionMap.find( + (option) => option.value === 20 + )} value={hourSelectionMap.find( (thisOption) => thisOption.value === values.hour_of_day )} - onChange={(e: Item) => { + onChange={(e) => { setFormTouched(true); - setFieldValue('hour_of_day', +e.value); + setFieldValue('hour_of_day', e.value); }} label="Time of Day (UTC)" placeholder="Choose a time" isClearable={false} menuPlacement="top" name="Time of Day" - error={touched.hour_of_day && Boolean(errors.hour_of_day)} errorText={ touched.hour_of_day ? errors.hour_of_day : undefined } @@ -331,16 +328,15 @@ export const MaintenanceWindow = (props: Props) => { value={modifiedWeekSelectionMap.find( (thisOption) => thisOption.value === values.week_of_month )} - onChange={(e: Item) => { + onChange={(e) => { setFormTouched(true); - setFieldValue('week_of_month', +e.value); + setFieldValue('week_of_month', e.value); }} label="Repeats on" placeholder="Repeats on" isClearable={false} menuPlacement="top" name="Repeats on" - error={touched.week_of_month && Boolean(errors.week_of_month)} errorText={ touched.week_of_month ? errors.week_of_month : undefined } diff --git a/packages/manager/src/features/Domains/DomainRecordDrawer.tsx b/packages/manager/src/features/Domains/DomainRecordDrawer.tsx index f2207c4d3d7..5b8b0dbf865 100644 --- a/packages/manager/src/features/Domains/DomainRecordDrawer.tsx +++ b/packages/manager/src/features/Domains/DomainRecordDrawer.tsx @@ -297,7 +297,7 @@ class DomainRecordDrawer extends React.Component { options={rateOptions} label="Expire Rate" defaultValue={defaultRate} - onChange={(e: Item) => this.setExpireSec(+e.value)} + onChange={(e) => this.setExpireSec(e.value)} isClearable={false} textFieldProps={{ dataAttrs: { @@ -350,7 +350,7 @@ class DomainRecordDrawer extends React.Component { options={MSSelectOptions} label={label} defaultValue={defaultOption} - onChange={(e: Item) => fn(+e.value)} + onChange={(e) => fn(e.value)} isClearable={false} textFieldProps={{ dataAttrs: { @@ -385,7 +385,7 @@ class DomainRecordDrawer extends React.Component { options={protocolOptions} label="Protocol" defaultValue={defaultProtocol} - onChange={(e: Item) => this.setProtocol(e.value)} + onChange={(e) => this.setProtocol(e.value)} isClearable={false} textFieldProps={{ dataAttrs: { diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.tsx index c290899b5e2..a37e9d7ce24 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.tsx @@ -262,7 +262,7 @@ const FirewallRuleForm: React.FC = React.memo( // This is an edge case; if there's an error for the Ports field // but CUSTOM isn't selected, the error won't be visible to the user. - const generalPortError = !hasCustomInput && errors.ports; + const generalPortError = !hasCustomInput ? errors.ports : undefined; // Set form field errors for each error we have (except "addresses" errors, which are handled // by IP Error state). @@ -459,11 +459,7 @@ const FirewallRuleForm: React.FC = React.memo( name="protocol" placeholder="Select a protocol..." aria-label="Select rule protocol." - value={ - values.protocol - ? { label: values.protocol, value: values.protocol } - : undefined - } + value={protocolOptions.find((p) => p.value === values.protocol)} errorText={errors.protocol} options={protocolOptions} onChange={handleProtocolChange} diff --git a/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx b/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx index 1b0c2c9ef9a..3c2132e8dc6 100644 --- a/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx +++ b/packages/manager/src/features/Help/Panels/AlgoliaSearchBar.tsx @@ -169,7 +169,6 @@ class AlgoliaSearchBar extends React.Component { hideLabel className={classes.enhancedSelectWrapper} styles={selectStyles} - value={false} />
diff --git a/packages/manager/src/features/Images/ImageSelect.tsx b/packages/manager/src/features/Images/ImageSelect.tsx index c7db72e7c93..64eb19adc6a 100644 --- a/packages/manager/src/features/Images/ImageSelect.tsx +++ b/packages/manager/src/features/Images/ImageSelect.tsx @@ -22,23 +22,30 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -interface Props { +interface BaseProps { images: Image[]; imageError?: string; imageFieldError?: string; - isMulti?: boolean; - helperText?: string; - value?: Item | Item[]; disabled?: boolean; - onSelect: (selected: Item | Item[]) => void; label?: string; required?: boolean; anyAllOption?: boolean; + helperText?: string; } -type CombinedProps = Props; +interface Props extends BaseProps { + isMulti?: false; + value?: Item; + onSelect: (selected: Item) => void; +} + +interface MultiProps extends BaseProps { + isMulti: true; + value?: Item[]; + onSelect: (selected: Item[]) => void; +} -export const ImageSelect: React.FC = (props) => { +export const ImageSelect = (props: Props | MultiProps) => { const { helperText, images, diff --git a/packages/manager/src/features/Longview/shared/TimeRangeSelect.tsx b/packages/manager/src/features/Longview/shared/TimeRangeSelect.tsx index a506d3c6a4f..703b2d78763 100644 --- a/packages/manager/src/features/Longview/shared/TimeRangeSelect.tsx +++ b/packages/manager/src/features/Longview/shared/TimeRangeSelect.tsx @@ -8,7 +8,11 @@ import Select, { } from 'src/components/EnhancedSelect/Select'; import { useMutatePreferences, usePreferences } from 'src/queries/preferences'; -interface Props extends Omit { +interface Props + extends Omit< + BaseSelectProps, false>, + 'onChange' | 'defaultValue' + > { handleStatsChange?: (start: number, end: number) => void; defaultValue?: Labels; } diff --git a/packages/manager/src/features/Managed/Contacts/ContactsDrawer.tsx b/packages/manager/src/features/Managed/Contacts/ContactsDrawer.tsx index 7c789becb78..ea340e5ed09 100644 --- a/packages/manager/src/features/Managed/Contacts/ContactsDrawer.tsx +++ b/packages/manager/src/features/Managed/Contacts/ContactsDrawer.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import ActionsPanel from 'src/components/ActionsPanel'; import Button from 'src/components/Button'; import Drawer from 'src/components/Drawer'; -import Select, { Item } from 'src/components/EnhancedSelect/Select'; +import Select from 'src/components/EnhancedSelect/Select'; import Grid from 'src/components/Grid'; import Notice from 'src/components/Notice'; import TextField from 'src/components/TextField'; @@ -191,14 +191,14 @@ const ContactsDrawer: React.FC = (props) => { value: values.group, label: values.group, } - : '' + : null } options={groups.map((group) => ({ label: group.groupName, value: group.groupName, }))} - onChange={(selectedGroup: Item) => - setFieldValue('group', selectedGroup.value) + onChange={(selectedGroup) => + setFieldValue('group', selectedGroup?.value) } errorText={errors.group} /> diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.tsx index 78956a88a55..a87f26c7c24 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.tsx @@ -8,7 +8,6 @@ import { makeStyles } from '@mui/styles'; import { Theme } from '@mui/material/styles'; import Typography from 'src/components/core/Typography'; import EnhancedSelect from 'src/components/EnhancedSelect'; -import { Item } from 'src/components/EnhancedSelect/Select'; import ExternalLink from 'src/components/ExternalLink'; import Notice from 'src/components/Notice'; import { Toggle } from 'src/components/Toggle'; @@ -156,11 +155,11 @@ const AccessSelect: React.FC = (props) => { options={_options} isLoading={accessLoading} disabled={accessLoading} - onChange={(selected: Item | null) => { + onChange={(selected) => { if (selected) { setUpdateAccessSuccess(false); setUpdateAccessError(''); - setSelectedACL(selected.value); + setSelectedACL(selected.value as ACLType); } }} value={_options.find( diff --git a/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx b/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx index f48a3fe867f..a7474cdd5cd 100644 --- a/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx +++ b/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx @@ -203,7 +203,6 @@ export const CreateAPITokenDrawer = (props: Props) => { onChange={handleExpiryChange} value={expiryList.find((item) => item.value === form.values.expiry)} name="expiry" - labelId="expiry" label="Expiry" isClearable={false} /> diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx index 3e572cc1c76..5532b4af34a 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/PhoneVerification/PhoneVerification.tsx @@ -14,7 +14,7 @@ import Box from 'src/components/core/Box'; import FormHelperText from 'src/components/core/FormHelperText'; import InputAdornment from 'src/components/core/InputAdornment'; import Typography from 'src/components/core/Typography'; -import Select, { Item } from 'src/components/EnhancedSelect/Select'; +import Select from 'src/components/EnhancedSelect/Select'; import { LinkButton } from 'src/components/LinkButton'; import TextField from 'src/components/TextField'; import { @@ -167,11 +167,12 @@ export const PhoneVerification = () => { marginLeft: '-1px !important', marginTop: '0px !important', }), - singleValue: (provided: React.CSSProperties) => ({ - ...provided, - textAlign: 'center', - fontSize: '20px', - }), + singleValue: (provided: React.CSSProperties) => + ({ + ...provided, + textAlign: 'center', + fontSize: '20px', + } as const), }; const selectedCountry = countries.find( @@ -249,6 +250,7 @@ export const PhoneVerification = () => { })} > = (props) => { const { data: grants } = useGrants(); const [overwrite, setOverwrite] = React.useState(false); - const [selectedLinode, setSelectedLinode] = React.useState('none'); + const [selectedLinodeId, setSelectedLinodeId] = React.useState( + linodeID + ); const [errors, setErrors] = React.useState([]); const reset = () => { setOverwrite(false); - setSelectedLinode('none'); + setSelectedLinodeId(null); setErrors([]); }; const restoreToLinode = () => { - if (!selectedLinode || selectedLinode === 'none') { + if (!selectedLinodeId) { setErrors([ ...errors, ...[{ field: 'linode_id', reason: 'You must select a Linode' }], @@ -73,7 +75,7 @@ export const RestoreToLinodeDrawer: React.FC = (props) => { scrollErrorIntoView(); return; } - restoreBackup(linodeID, Number(backupID), Number(selectedLinode), overwrite) + restoreBackup(linodeID, Number(backupID), selectedLinodeId, overwrite) .then(() => { reset(); onSubmit(); @@ -84,10 +86,6 @@ export const RestoreToLinodeDrawer: React.FC = (props) => { }); }; - const handleSelectLinode = (e: Item) => { - setSelectedLinode(e.value); - }; - const handleToggleOverwrite = () => { setOverwrite((prevOverwrite) => !prevOverwrite); }; @@ -108,7 +106,7 @@ export const RestoreToLinodeDrawer: React.FC = (props) => { const overwriteError = hasErrorFor('overwrite'); const generalError = hasErrorFor('none'); - const readOnly = canEditLinode(grants, Number(selectedLinode)); + const readOnly = canEditLinode(grants, selectedLinodeId ?? -1); const selectError = Boolean(linodeError) || readOnly; const linodeOptions = linodesData @@ -138,9 +136,11 @@ export const RestoreToLinodeDrawer: React.FC = (props) => { 'data-qa-select-linode': true, }, }} - defaultValue={selectedLinode || ''} + defaultValue={linodeOptions.find( + (option) => option.value === selectedLinodeId + )} options={linodeOptions} - onChange={handleSelectLinode} + onChange={(item: Item) => setSelectedLinodeId(item.value)} errorText={linodeError} placeholder="Select a Linode" isClearable={false} diff --git a/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/DeviceSelection.tsx b/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/DeviceSelection.tsx index a3c507cf9dd..ba9d7a8632c 100644 --- a/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/DeviceSelection.tsx +++ b/packages/manager/src/features/linodes/LinodesDetail/LinodeRescue/DeviceSelection.tsx @@ -101,9 +101,8 @@ const DeviceSelection: React.FC = (props) => {