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

Fix select option types #122

Merged
merged 1 commit into from
Apr 23, 2024
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ComponentProps } from 'react';
import { Control, Controller, FieldValues, Path } from 'react-hook-form';
import MultiSelect, { MultiSelectProps as SelectProps } from './MultiSelect';
import { Option } from 'types/inputs';
import MultiSelect, { MultiSelectProps } from './MultiSelect';

type ControlRules<T extends FieldValues> = ComponentProps<
typeof Controller<T>
Expand All @@ -12,41 +13,34 @@ type ControlProps<T extends FieldValues> = {
rules?: ControlRules<T>;
};

const getSelectValues: (
opt: ReadonlyArray<{ label: string; value: number }>,
) => number[] = (opt) => {
const getSelectValues: <TValue = string>(
opt: ReadonlyArray<Option<TValue>>,
) => TValue[] = (opt) => {
Pechenux marked this conversation as resolved.
Show resolved Hide resolved
return opt.map((element) => element.value);
};

const getSelectOptions: (
options: ReadonlyArray<{ label: string; value: number }>,
value: ReadonlyArray<number>,
) => ReadonlyArray<{ label: string; value: number }> = (options, values) => {
return options.filter(({ value }) => values.includes(value));
const getSelectOptions: <TValue = string>(
Pechenux marked this conversation as resolved.
Show resolved Hide resolved
options: ReadonlyArray<Option<TValue>> | undefined,
value: TValue[],
) => ReadonlyArray<Option<TValue>> | undefined = (options, values) => {
return options?.filter(({ value }) => values.includes(value));
};

const ControlledMultiSelect = <T extends FieldValues>({
const ControlledMultiSelect = <T extends FieldValues, TValue = string>({
controlName,
control,
rules,
...rest
}: SelectProps & ControlProps<T>) => (
}: MultiSelectProps<TValue> & ControlProps<T>) => (
<Controller
name={controlName}
control={control}
rules={rules}
render={({ field }) => (
<MultiSelect
<MultiSelect<TValue>
{...field}
onChange={(newValue) =>
field.onChange(
getSelectValues(newValue as { label: string; value: number }[]),
)
}
value={getSelectOptions(
rest.options as ReadonlyArray<{ label: string; value: number }>,
field.value,
)}
onChange={(newValue) => field.onChange(getSelectValues(newValue))}
value={getSelectOptions(rest.options, field.value)}
{...rest}
/>
)}
Expand Down
20 changes: 15 additions & 5 deletions web-app/client/src/components/Inputs/MultiSelect/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import cn from 'classnames';
import { forwardRef, ForwardRefRenderFunction, ReactNode } from 'react';
import {
ForwardedRef,
forwardRef,
ForwardRefRenderFunction,
ReactNode,
} from 'react';
import ReactSelect, { Props as ReactSelectProps } from 'react-select';
import { InputPropsBase } from '@components/Inputs';
import Tooltip from '@components/Tooltip';
import { Option } from 'types/inputs';
import customComponents, { colorStyles } from './customComponents';
import styles from './MultiSelect.module.scss';

export type MultiSelectProps = InputPropsBase &
ReactSelectProps & {
export type MultiSelectProps<TValue = string> = InputPropsBase &
ReactSelectProps<Option<TValue>, true> & {
tooltip?: ReactNode;
};

// Can't recreate explicit type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const MultiSelect: ForwardRefRenderFunction<any, MultiSelectProps> = (
type RefElement = any;

const MultiSelect: ForwardRefRenderFunction<RefElement, MultiSelectProps> = (
{ label, error, tooltip, className, id, components, ...props },
ref,
) => {
Expand Down Expand Up @@ -49,4 +57,6 @@ const MultiSelect: ForwardRefRenderFunction<any, MultiSelectProps> = (
);
};

export default forwardRef(MultiSelect);
export default forwardRef(MultiSelect) as <TValue = string>(
props: MultiSelectProps<TValue> & { ref?: ForwardedRef<RefElement> },
) => ReturnType<typeof MultiSelect>;
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@ import ChevronDownIcon from '@assets/icons/arrow-down.svg?component';
import EmptyButton from '@assets/icons/close.svg?component';
import { InputPropsBase } from '@components/Inputs';
import badgeStyles from '@components/Inputs/MultiSelect/OptionBadge/OptionBadge.module.scss';
import { Option as OptionType } from 'types/inputs';
import { OptionWithBadges } from 'types/multiSelect';
import { OptionBadge } from './OptionBadge';
import styles from './MultiSelect.module.scss';

export const colorStyles: StylesConfig = {
export const colorStyles: StylesConfig<OptionType, true> = {
control: () => ({}),
valueContainer: (styles) => ({ ...styles, padding: 0 }),
indicatorSeparator: (styles) => ({ ...styles, margin: 0 }),
};

const Control: ComponentType<ControlProps & InputPropsBase> = (props) => (
const Control: ComponentType<
ControlProps<OptionType, true> & InputPropsBase
> = (props) => (
<components.Control
className={cn(
styles.control,
Expand All @@ -43,15 +46,17 @@ const Control: ComponentType<ControlProps & InputPropsBase> = (props) => (
);

const MultiValueContainer: ComponentType<
MultiValueGenericProps & InputPropsBase
MultiValueGenericProps<OptionType, true> & InputPropsBase
> = (props) => <components.MultiValueContainer {...props} />;

const MultiValue: ComponentType<MultiValueProps & InputPropsBase> = (props) => (
const MultiValue: ComponentType<
MultiValueProps<OptionType, true> & InputPropsBase
> = (props) => (
<components.MultiValue className={styles.multiValue} {...props} />
);

const MultiValueLabel: ComponentType<
MultiValueGenericProps & InputPropsBase
MultiValueGenericProps<OptionType, true> & InputPropsBase
> = (props) => (
<div
{...props.innerProps}
Expand All @@ -63,7 +68,7 @@ const MultiValueLabel: ComponentType<
);

const MultiValueRemove: ComponentType<
MultiValueRemoveProps & InputPropsBase
MultiValueRemoveProps<OptionType, true> & InputPropsBase
> = (props) => (
<div
{...props.innerProps}
Expand All @@ -73,50 +78,49 @@ const MultiValueRemove: ComponentType<
</div>
);

const Placeholder: ComponentType<PlaceholderProps & InputPropsBase> = (
props,
) => <components.Placeholder className={styles.placeholder} {...props} />;
const Placeholder: ComponentType<
PlaceholderProps<OptionType, true> & InputPropsBase
> = (props) => (
<components.Placeholder className={styles.placeholder} {...props} />
);

const Input: ComponentType<InputProps & InputPropsBase> = (props) => (
const Input: ComponentType<InputProps<OptionType, true> & InputPropsBase> = (
props,
) => (
<components.Input
className={cn(styles.input, props.value && styles.hasValue)}
{...props}
/>
);

const ClearIndicator: ComponentType<ClearIndicatorProps & InputPropsBase> = ({
innerProps,
}) => (
const ClearIndicator: ComponentType<
ClearIndicatorProps<OptionType, true> & InputPropsBase
> = ({ innerProps }) => (
<div className={styles.clearIndicator} {...innerProps}>
<EmptyButton />
</div>
);

const DropdownIndicator: ComponentType<
DropdownIndicatorProps & InputPropsBase
DropdownIndicatorProps<OptionType, true> & InputPropsBase
> = ({ innerProps }) => (
<div className={styles.dropdownIndicator} {...innerProps}>
<ChevronDownIcon />
</div>
);

const IndicatorsContainer: ComponentType<
IndicatorsContainerProps & InputPropsBase
IndicatorsContainerProps<OptionType, true> & InputPropsBase
> = (props) => (
<components.IndicatorsContainer
className={styles.indicatorsContainer}
{...props}
/>
);

export const Option: ComponentType<OptionProps & InputPropsBase> = ({
innerProps,
innerRef,
children,
isFocused,
isSelected,
data,
}) => {
export const Option: ComponentType<
OptionProps<OptionType, true> & InputPropsBase
> = ({ innerProps, innerRef, children, isFocused, isSelected, data }) => {
const optionData = data as OptionWithBadges;

return (
Expand Down Expand Up @@ -148,19 +152,17 @@ export const Option: ComponentType<OptionProps & InputPropsBase> = ({
);
};

const NoOptionsMessage: ComponentType<NoticeProps & InputPropsBase> = ({
innerProps,
children,
}) => (
const NoOptionsMessage: ComponentType<
NoticeProps<OptionType, true> & InputPropsBase
> = ({ innerProps, children }) => (
<div className={cn(styles.option, styles.noOptionsMessage)} {...innerProps}>
{children}
</div>
);

const LoadingMessage: ComponentType<NoticeProps & InputPropsBase> = ({
innerProps,
children,
}) => (
const LoadingMessage: ComponentType<
NoticeProps<OptionType, true> & InputPropsBase
> = ({ innerProps, children }) => (
<div className={cn(styles.option, styles.noOptionsMessage)} {...innerProps}>
{children}
</div>
Expand Down
14 changes: 5 additions & 9 deletions web-app/client/src/components/Inputs/Select/ControlledSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,21 @@ type ControlProps<T extends FieldValues> = {
control: Control<T>;
rules?: ControlRules<T>;
};
const ControlledSelect = <T extends FieldValues>({
const ControlledSelect = <T extends FieldValues, TValue = string>({
controlName,
control,
rules,
...rest
}: SelectProps & ControlProps<T>) => (
}: SelectProps<TValue> & ControlProps<T>) => (
<Controller
name={controlName}
control={control}
rules={rules}
render={({ field }) => (
<Select
<Select<TValue>
{...field}
// Can't recreate explicit type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onChange={(option: any) => field.onChange(option?.value)}
// Can't recreate explicit type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value={rest.options?.find((e: any) => e?.value === field.value)}
onChange={(option) => field.onChange(option?.value)}
value={rest.options?.find((e) => e?.value === field.value)}
{...rest}
/>
)}
Expand Down
20 changes: 15 additions & 5 deletions web-app/client/src/components/Inputs/Select/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import cn from 'classnames';
import { forwardRef, ForwardRefRenderFunction, ReactNode } from 'react';
import {
ForwardedRef,
forwardRef,
ForwardRefRenderFunction,
ReactNode,
} from 'react';
import ReactSelect, { Props as ReactSelectProps } from 'react-select';
import { InputPropsBase } from '@components/Inputs';
import Tooltip from '@components/Tooltip';
import { Option } from 'types/inputs';
import customComponents from './customComponents';
import styles from './Select.module.scss';

export type Props = InputPropsBase &
ReactSelectProps & {
export type Props<TValue = string> = InputPropsBase &
ReactSelectProps<Option<TValue>, false> & {
tooltip?: ReactNode;
};

// Can't recreate explicit type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Select: ForwardRefRenderFunction<any, Props> = (
type RefElement = any;

const Select: ForwardRefRenderFunction<RefElement, Props> = (
{ label, error, tooltip, className, id, components, ...props },
ref,
) => {
Expand Down Expand Up @@ -45,4 +53,6 @@ const Select: ForwardRefRenderFunction<any, Props> = (
);
};

export default forwardRef(Select);
export default forwardRef(Select) as <TValue = string>(
props: Props<TValue> & { ref?: ForwardedRef<RefElement> },
) => ReturnType<typeof Select>;
Loading
Loading