import { useState } from 'react'; import type { AriaAttributes } from 'react'; import { GroupBase, OptionsOrGroups, SelectComponentsConfig, } from 'react-select'; import _ from 'lodash'; import { FilterOptionOption } from 'react-select/dist/declarations/src/filters'; import { AutomationTestingProps } from '@/types'; import { Creatable, Select as ReactSelect, } from '@@/form-components/ReactSelect'; export interface Option { value: TValue; label: string; disabled?: boolean; [key: string]: unknown; } export interface GroupOption { label: string; options: Option[]; } type Options = OptionsOrGroups< Option, GroupBase> >; interface SharedProps extends AutomationTestingProps, Pick { name?: string; inputId?: string; size?: 'sm' | 'md'; placeholder?: string; disabled?: boolean; isClearable?: boolean; bindToBody?: boolean; isLoading?: boolean; noOptionsMessage?: () => string; loadingMessage?: () => string; filterOption?: ( option: FilterOptionOption>, rawInput: string ) => boolean; getOptionValue?: (option: TValue) => string; } interface MultiProps extends SharedProps { value: readonly TValue[]; onChange(value: TValue[]): void; options: Options; isMulti: true; components?: SelectComponentsConfig< Option, true, GroupBase> >; formatCreateLabel?: (input: string) => string; onCreateOption?: (input: string) => void; isCreatable?: boolean; } interface SingleProps extends SharedProps { value: TValue; onChange(value: TValue | null): void; options: Options; isMulti?: never; components?: SelectComponentsConfig< Option, false, GroupBase> >; } export type PortainerSelectProps = | MultiProps | SingleProps; export function PortainerSelect( props: PortainerSelectProps ) { return isMultiProps(props) ? ( // eslint-disable-next-line react/jsx-props-no-spreading ) : ( // eslint-disable-next-line react/jsx-props-no-spreading ); } function isMultiProps( props: PortainerSelectProps ): props is MultiProps { return 'isMulti' in props && !!props.isMulti; } export function SingleSelect({ name, options, onChange, value, 'data-cy': dataCy, disabled, inputId, placeholder, isClearable, bindToBody, filterOption, components, isLoading, noOptionsMessage, loadingMessage, isMulti, size, getOptionValue, ...aria }: SingleProps) { const selectedValue = value || (typeof value === 'number' && value === 0) || (typeof value === 'string' && value === '') ? _.first(findSelectedOptions(options, value, getOptionValue)) : null; return ( > name={name} isClearable={isClearable} getOptionLabel={(option) => option.label} getOptionValue={(option) => getOptionValue ? getOptionValue(option.value) : String(option.value) } options={options} value={selectedValue} onChange={(option) => onChange(option ? option.value : null)} isOptionDisabled={(option) => !!option.disabled} data-cy={dataCy} inputId={inputId} placeholder={placeholder} isDisabled={disabled} menuPortalTarget={bindToBody ? document.body : undefined} filterOption={filterOption} components={components} isLoading={isLoading} noOptionsMessage={noOptionsMessage} size={size} loadingMessage={loadingMessage} // eslint-disable-next-line react/jsx-props-no-spreading {...aria} /> ); } function isSingleValue( value: TValue | readonly TValue[] ): value is TValue { return !Array.isArray(value); } function findSelectedOptions( options: Options, value: TValue | readonly TValue[], getOptionValue: (option: TValue) => string | TValue = (v: TValue) => v ) { const valueArr = isSingleValue(value) ? [getOptionValue(value)] : value.map((v) => getOptionValue(v)); const values = _.compact( options.flatMap((option) => { if (isGroup(option)) { return option.options.find((opt) => valueArr.includes(getOptionValue(opt.value)) ); } if (valueArr.includes(getOptionValue(option.value))) { return option; } return null; }) ); return values; } export function MultiSelect({ name, value, onChange, options, 'data-cy': dataCy, inputId, placeholder, disabled, isClearable, bindToBody, filterOption, components, isLoading, noOptionsMessage, loadingMessage, formatCreateLabel, onCreateOption, isCreatable, size, getOptionValue, ...aria }: Omit, 'isMulti'>) { const [inputValue, setInputValue] = useState(''); const selectedOptions = findSelectedOptions(options, value, getOptionValue); const SelectComponent = isCreatable ? Creatable : ReactSelect; return ( option.label} getOptionValue={(option) => getOptionValue ? getOptionValue(option.value) : String(option.value) } isOptionDisabled={(option) => !!option.disabled} options={options} value={selectedOptions} closeMenuOnSelect={false} onChange={(newValue) => { onChange(newValue.map((option) => option.value)); setInputValue(''); }} data-cy={dataCy} id={dataCy} inputId={inputId} placeholder={placeholder} isDisabled={disabled} menuPortalTarget={bindToBody ? document.body : undefined} filterOption={filterOption} components={components} isLoading={isLoading} noOptionsMessage={noOptionsMessage} loadingMessage={loadingMessage} formatCreateLabel={formatCreateLabel} onCreateOption={onCreateOption} inputValue={inputValue} onInputChange={(textInput) => setInputValue(textInput)} onBlur={handleBlur} size={size} // eslint-disable-next-line react/jsx-props-no-spreading {...aria} /> ); function handleBlur() { const trimmed = inputValue.trim(); if (!trimmed || value.includes(trimmed as TValue)) { setInputValue(''); return; } if (onCreateOption && isCreatable) { onCreateOption(trimmed); } else { onChange([...value, trimmed as TValue]); } setInputValue(''); } } function isGroup( option: Option | GroupBase> ): option is GroupBase> { return 'options' in option; }