Improve typings in FormInputGroup

(cherry picked from commit 6838f068bcd04b770cd9c53873f160be97ea745f)
This commit is contained in:
Mark McDowall 2025-01-03 09:47:17 -08:00 committed by Bogdan
parent 8fb2f64e98
commit 2c81f3be0f
43 changed files with 321 additions and 301 deletions

View file

@ -110,7 +110,7 @@ class AddNewMovieModalContent extends Component {
</FormLabel> </FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.MOVIE_MONITORED_SELECT} type={inputTypes.MONITOR_MOVIES_SELECT}
name="monitor" name="monitor"
onChange={onInputChange} onChange={onInputChange}
{...monitor} {...monitor}

View file

@ -117,7 +117,7 @@ class ImportMovieFooter extends Component {
</div> </div>
<FormInputGroup <FormInputGroup
type={inputTypes.MOVIE_MONITORED_SELECT} type={inputTypes.MONITOR_MOVIES_SELECT}
name="monitor" name="monitor"
value={monitor} value={monitor}
isDisabled={!selectedCount} isDisabled={!selectedCount}

View file

@ -44,7 +44,7 @@ function ImportMovieRow(props) {
<VirtualTableRowCell className={styles.monitor}> <VirtualTableRowCell className={styles.monitor}>
<FormInputGroup <FormInputGroup
type={inputTypes.MOVIE_MONITORED_SELECT} type={inputTypes.MONITOR_MOVIES_SELECT}
name="monitor" name="monitor"
value={monitor} value={monitor}
onChange={onInputChange} onChange={onInputChange}

View file

@ -107,7 +107,7 @@ class AddNewCollectionMovieModalContent extends Component {
</FormLabel> </FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.MOVIE_MONITORED_SELECT} type={inputTypes.MONITOR_MOVIES_SELECT}
name="monitor" name="monitor"
onChange={onInputChange} onChange={onInputChange}
{...monitor} {...monitor}

View file

@ -7,8 +7,9 @@ import {
import { InputChanged } from 'typings/inputs'; import { InputChanged } from 'typings/inputs';
import AutoSuggestInput from './AutoSuggestInput'; import AutoSuggestInput from './AutoSuggestInput';
interface AutoCompleteInputProps { export interface AutoCompleteInputProps {
name: string; name: string;
readOnly?: boolean;
value?: string; value?: string;
values: string[]; values: string[];
onChange: (change: InputChanged<string>) => unknown; onChange: (change: InputChanged<string>) => unknown;

View file

@ -16,7 +16,7 @@ import FormInputButton from './FormInputButton';
import TextInput from './TextInput'; import TextInput from './TextInput';
import styles from './CaptchaInput.css'; import styles from './CaptchaInput.css';
interface CaptchaInputProps { export interface CaptchaInputProps {
className?: string; className?: string;
name: string; name: string;
value?: string; value?: string;

View file

@ -41,10 +41,11 @@
.checkbox:focus + .input { .checkbox:focus + .input {
outline: 0; outline: 0;
border-color: var(--inputFocusBorderColor); border-color: var(--inputFocusBorderColor);
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor), 0 0 8px var(--inputFocusBoxShadowColor); box-shadow: inset 0 1px 1px var(--inputBoxShadowColor),
0 0 8px var(--inputFocusBoxShadowColor);
} }
.dangerIsChecked { .danger {
border-color: var(--dangerColor); border-color: var(--dangerColor);
background-color: var(--dangerColor); background-color: var(--dangerColor);
@ -53,7 +54,7 @@
} }
} }
.primaryIsChecked { .primary {
border-color: var(--primaryColor); border-color: var(--primaryColor);
background-color: var(--primaryColor); background-color: var(--primaryColor);
@ -62,7 +63,7 @@
} }
} }
.successIsChecked { .success {
border-color: var(--successColor); border-color: var(--successColor);
background-color: var(--successColor); background-color: var(--successColor);
@ -71,7 +72,7 @@
} }
} }
.warningIsChecked { .warning {
border-color: var(--warningColor); border-color: var(--warningColor);
background-color: var(--warningColor); background-color: var(--warningColor);

View file

@ -3,16 +3,16 @@
interface CssExports { interface CssExports {
'checkbox': string; 'checkbox': string;
'container': string; 'container': string;
'dangerIsChecked': string; 'danger': string;
'helpText': string; 'helpText': string;
'input': string; 'input': string;
'isDisabled': string; 'isDisabled': string;
'isIndeterminate': string; 'isIndeterminate': string;
'isNotChecked': string; 'isNotChecked': string;
'label': string; 'label': string;
'primaryIsChecked': string; 'primary': string;
'successIsChecked': string; 'success': string;
'warningIsChecked': string; 'warning': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;

View file

@ -11,7 +11,7 @@ interface ChangeEvent<T = Element> extends SyntheticEvent<T, MouseEvent> {
target: EventTarget & T; target: EventTarget & T;
} }
interface CheckInputProps { export interface CheckInputProps {
className?: string; className?: string;
containerClassName?: string; containerClassName?: string;
name: string; name: string;
@ -45,7 +45,6 @@ function CheckInput(props: CheckInputProps) {
const isChecked = value === checkedValue; const isChecked = value === checkedValue;
const isUnchecked = value === uncheckedValue; const isUnchecked = value === uncheckedValue;
const isIndeterminate = !isChecked && !isUnchecked; const isIndeterminate = !isChecked && !isUnchecked;
const isCheckClass: keyof typeof styles = `${kind}IsChecked`;
const toggleChecked = useCallback( const toggleChecked = useCallback(
(checked: boolean, shiftKey: boolean) => { (checked: boolean, shiftKey: boolean) => {
@ -112,7 +111,7 @@ function CheckInput(props: CheckInputProps) {
<div <div
className={classNames( className={classNames(
className, className,
isChecked ? styles[isCheckClass] : styles.isNotChecked, isChecked ? styles[kind] : styles.isNotChecked,
isIndeterminate && styles.isIndeterminate, isIndeterminate && styles.isIndeterminate,
isDisabled && styles.isDisabled isDisabled && styles.isDisabled
)} )}

View file

@ -1,166 +1,197 @@
import React, { FocusEvent, ReactNode } from 'react'; import React, { ElementType, ReactNode } from 'react';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import { inputTypes } from 'Helpers/Props'; import { inputTypes } from 'Helpers/Props';
import { InputType } from 'Helpers/Props/inputTypes'; import { InputType } from 'Helpers/Props/inputTypes';
import { Kind } from 'Helpers/Props/kinds';
import { ValidationError, ValidationWarning } from 'typings/pending'; import { ValidationError, ValidationWarning } from 'typings/pending';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import AutoCompleteInput from './AutoCompleteInput'; import AutoCompleteInput, { AutoCompleteInputProps } from './AutoCompleteInput';
import CaptchaInput from './CaptchaInput'; import CaptchaInput, { CaptchaInputProps } from './CaptchaInput';
import CheckInput from './CheckInput'; import CheckInput, { CheckInputProps } from './CheckInput';
import { FormInputButtonProps } from './FormInputButton'; import { FormInputButtonProps } from './FormInputButton';
import FormInputHelpText from './FormInputHelpText'; import FormInputHelpText from './FormInputHelpText';
import KeyValueListInput from './KeyValueListInput'; import KeyValueListInput, { KeyValueListInputProps } from './KeyValueListInput';
import NumberInput from './NumberInput'; import NumberInput, { NumberInputProps } from './NumberInput';
import OAuthInput from './OAuthInput'; import OAuthInput, { OAuthInputProps } from './OAuthInput';
import PasswordInput from './PasswordInput'; import PasswordInput from './PasswordInput';
import PathInput from './PathInput'; import PathInput, { PathInputProps } from './PathInput';
import AvailabilitySelectInput from './Select/AvailabilitySelectInput'; import AvailabilitySelectInput, {
import DownloadClientSelectInput from './Select/DownloadClientSelectInput'; AvailabilitySelectInputProps,
import EnhancedSelectInput from './Select/EnhancedSelectInput'; } from './Select/AvailabilitySelectInput';
import IndexerFlagsSelectInput from './Select/IndexerFlagsSelectInput'; import DownloadClientSelectInput, {
import IndexerSelectInput from './Select/IndexerSelectInput'; DownloadClientSelectInputProps,
import LanguageSelectInput from './Select/LanguageSelectInput'; } from './Select/DownloadClientSelectInput';
import MonitorMoviesSelectInput from './Select/MonitorMoviesSelectInput'; import EnhancedSelectInput, {
import ProviderDataSelectInput from './Select/ProviderOptionSelectInput'; EnhancedSelectInputProps,
import QualityProfileSelectInput from './Select/QualityProfileSelectInput'; } from './Select/EnhancedSelectInput';
import RootFolderSelectInput from './Select/RootFolderSelectInput'; import IndexerFlagsSelectInput, {
import UMaskInput from './Select/UMaskInput'; IndexerFlagsSelectInputProps,
import DeviceInput from './Tag/DeviceInput'; } from './Select/IndexerFlagsSelectInput';
import MovieTagInput from './Tag/MovieTagInput'; import IndexerSelectInput, {
import TagSelectInput from './Tag/TagSelectInput'; IndexerSelectInputProps,
import TextTagInput from './Tag/TextTagInput'; } from './Select/IndexerSelectInput';
import TextArea from './TextArea'; import LanguageSelectInput, {
import TextInput from './TextInput'; LanguageSelectInputProps,
} from './Select/LanguageSelectInput';
import MonitorMoviesSelectInput, {
MonitorMoviesSelectInputProps,
} from './Select/MonitorMoviesSelectInput';
import ProviderDataSelectInput, {
ProviderOptionSelectInputProps,
} from './Select/ProviderOptionSelectInput';
import QualityProfileSelectInput, {
QualityProfileSelectInputProps,
} from './Select/QualityProfileSelectInput';
import RootFolderSelectInput, {
RootFolderSelectInputProps,
} from './Select/RootFolderSelectInput';
import UMaskInput, { UMaskInputProps } from './Select/UMaskInput';
import DeviceInput, { DeviceInputProps } from './Tag/DeviceInput';
import MovieTagInput, { MovieTagInputProps } from './Tag/MovieTagInput';
import TagSelectInput, { TagSelectInputProps } from './Tag/TagSelectInput';
import TextTagInput, { TextTagInputProps } from './Tag/TextTagInput';
import TextArea, { TextAreaProps } from './TextArea';
import TextInput, { TextInputProps } from './TextInput';
import styles from './FormInputGroup.css'; import styles from './FormInputGroup.css';
function getComponent(type: InputType) { const componentMap: Record<InputType, ElementType> = {
switch (type) { autoComplete: AutoCompleteInput,
case inputTypes.AUTO_COMPLETE: availabilitySelect: AvailabilitySelectInput,
return AutoCompleteInput; captcha: CaptchaInput,
check: CheckInput,
date: TextInput,
device: DeviceInput,
downloadClientSelect: DownloadClientSelectInput,
dynamicSelect: ProviderDataSelectInput,
file: TextInput,
float: NumberInput,
indexerFlagsSelect: IndexerFlagsSelectInput,
indexerSelect: IndexerSelectInput,
keyValueList: KeyValueListInput,
languageSelect: LanguageSelectInput,
monitorMoviesSelect: MonitorMoviesSelectInput,
movieTag: MovieTagInput,
number: NumberInput,
oauth: OAuthInput,
password: PasswordInput,
path: PathInput,
qualityProfileSelect: QualityProfileSelectInput,
rootFolderSelect: RootFolderSelectInput,
select: EnhancedSelectInput,
tag: MovieTagInput,
tagSelect: TagSelectInput,
text: TextInput,
textArea: TextArea,
textTag: TextTagInput,
umask: UMaskInput,
} as const;
case inputTypes.AVAILABILITY_SELECT: // type Components = typeof componentMap;
return AvailabilitySelectInput;
case inputTypes.CAPTCHA: type PickProps<V, C extends InputType> = C extends 'text'
return CaptchaInput; ? TextInputProps
: C extends 'autoComplete'
? AutoCompleteInputProps
: C extends 'availabilitySelect'
? AvailabilitySelectInputProps
: C extends 'captcha'
? CaptchaInputProps
: C extends 'check'
? CheckInputProps
: C extends 'date'
? TextInputProps
: C extends 'device'
? DeviceInputProps
: C extends 'downloadClientSelect'
? DownloadClientSelectInputProps
: C extends 'dynamicSelect'
? ProviderOptionSelectInputProps
: C extends 'file'
? TextInputProps
: C extends 'float'
? TextInputProps
: C extends 'indexerFlagsSelect'
? IndexerFlagsSelectInputProps
: C extends 'indexerSelect'
? IndexerSelectInputProps
: C extends 'keyValueList'
? KeyValueListInputProps
: C extends 'languageSelect'
? LanguageSelectInputProps
: C extends 'monitorMoviesSelect'
? MonitorMoviesSelectInputProps
: C extends 'movieTag'
? MovieTagInputProps
: C extends 'number'
? NumberInputProps
: C extends 'oauth'
? OAuthInputProps
: C extends 'password'
? TextInputProps
: C extends 'path'
? PathInputProps
: C extends 'qualityProfileSelect'
? QualityProfileSelectInputProps
: C extends 'rootFolderSelect'
? RootFolderSelectInputProps
: C extends 'select'
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
EnhancedSelectInputProps<any, V>
: C extends 'tag'
? MovieTagInputProps
: C extends 'tagSelect'
? TagSelectInputProps
: C extends 'text'
? TextInputProps
: C extends 'textArea'
? TextAreaProps
: C extends 'textTag'
? TextTagInputProps
: C extends 'umask'
? UMaskInputProps
: never;
case inputTypes.CHECK: export interface FormInputGroupValues<T> {
return CheckInput; key: T;
value: string;
case inputTypes.DEVICE: hint?: string;
return DeviceInput;
case inputTypes.KEY_VALUE_LIST:
return KeyValueListInput;
case inputTypes.LANGUAGE_SELECT:
return LanguageSelectInput;
case inputTypes.MOVIE_MONITORED_SELECT:
return MonitorMoviesSelectInput;
case inputTypes.NUMBER:
return NumberInput;
case inputTypes.OAUTH:
return OAuthInput;
case inputTypes.PASSWORD:
return PasswordInput;
case inputTypes.PATH:
return PathInput;
case inputTypes.QUALITY_PROFILE_SELECT:
return QualityProfileSelectInput;
case inputTypes.INDEXER_SELECT:
return IndexerSelectInput;
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInput;
case inputTypes.DOWNLOAD_CLIENT_SELECT:
return DownloadClientSelectInput;
case inputTypes.ROOT_FOLDER_SELECT:
return RootFolderSelectInput;
case inputTypes.SELECT:
return EnhancedSelectInput;
case inputTypes.DYNAMIC_SELECT:
return ProviderDataSelectInput;
case inputTypes.TAG:
case inputTypes.MOVIE_TAG:
return MovieTagInput;
case inputTypes.TEXT_AREA:
return TextArea;
case inputTypes.TEXT_TAG:
return TextTagInput;
case inputTypes.TAG_SELECT:
return TagSelectInput;
case inputTypes.UMASK:
return UMaskInput;
default:
return TextInput;
}
} }
// TODO: Remove once all parent components are updated to TSX and we can refactor to a consistent type // TODO: Remove once all parent components are updated to TSX and we can refactor to a consistent type
interface ValidationMessage { export interface ValidationMessage {
message: string; message: string;
} }
interface FormInputGroupProps<T> { export type FormInputGroupProps<V, C extends InputType> = Omit<
PickProps<V, C>,
'className'
> & {
type: C;
className?: string; className?: string;
containerClassName?: string; containerClassName?: string;
inputClassName?: string; inputClassName?: string;
autoFocus?: boolean;
autocomplete?: string;
name: string; name: string;
value?: unknown;
values?: unknown[];
isDisabled?: boolean;
type?: InputType;
kind?: Kind;
min?: number;
max?: number;
unit?: string;
buttons?: ReactNode | ReactNode[]; buttons?: ReactNode | ReactNode[];
helpText?: string; helpText?: string;
helpTexts?: string[]; helpTexts?: string[];
helpTextWarning?: string; helpTextWarning?: string;
helpLink?: string; helpLink?: string;
placeholder?: string;
autoFocus?: boolean;
includeNoChange?: boolean;
includeNoChangeDisabled?: boolean;
valueOptions?: object;
selectedValueOptions?: object;
indexerFlags?: number;
pending?: boolean; pending?: boolean;
canEdit?: boolean; placeholder?: string;
includeAny?: boolean; unit?: string;
delimiters?: string[];
readOnly?: boolean;
errors?: (ValidationMessage | ValidationError)[]; errors?: (ValidationMessage | ValidationError)[];
warnings?: (ValidationMessage | ValidationWarning)[]; warnings?: (ValidationMessage | ValidationWarning)[];
onChange: (args: T) => void; };
onFocus?: (event: FocusEvent<HTMLInputElement>) => void;
}
function FormInputGroup<T>(props: FormInputGroupProps<T>) { function FormInputGroup<T, C extends InputType>(
props: FormInputGroupProps<T, C>
) {
const { const {
className = styles.inputGroup, className = styles.inputGroup,
containerClassName = styles.inputGroupContainer, containerClassName = styles.inputGroupContainer,
inputClassName, inputClassName,
type = 'text', type,
unit, unit,
buttons = [], buttons = [],
helpText, helpText,
@ -173,7 +204,7 @@ function FormInputGroup<T>(props: FormInputGroupProps<T>) {
...otherProps ...otherProps
} = props; } = props;
const InputComponent = getComponent(type); const InputComponent = componentMap[type];
const checkInput = type === inputTypes.CHECK; const checkInput = type === inputTypes.CHECK;
const hasError = !!errors.length; const hasError = !!errors.length;
const hasWarning = !hasError && !!warnings.length; const hasWarning = !hasError && !!warnings.length;
@ -185,7 +216,7 @@ function FormInputGroup<T>(props: FormInputGroupProps<T>) {
<div className={containerClassName}> <div className={containerClassName}>
<div className={className}> <div className={className}>
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
{/* @ts-expect-error - need to pass through all the expected options */} {/* @ts-expect-error - types are validated already */}
<InputComponent <InputComponent
className={inputClassName} className={inputClassName}
helpText={helpText} helpText={helpText}

View file

@ -24,12 +24,13 @@ function parseValue(
return newValue; return newValue;
} }
interface NumberInputProps export interface NumberInputProps
extends Omit<TextInputProps<number | null>, 'value'> { extends Omit<TextInputProps, 'value' | 'onChange'> {
value?: number | null; value?: number | null;
min?: number; min?: number;
max?: number; max?: number;
isFloat?: boolean; isFloat?: boolean;
onChange: (input: InputChanged<number | null>) => void;
} }
function NumberInput({ function NumberInput({

View file

@ -6,7 +6,7 @@ import { kinds } from 'Helpers/Props';
import { resetOAuth, startOAuth } from 'Store/Actions/oAuthActions'; import { resetOAuth, startOAuth } from 'Store/Actions/oAuthActions';
import { InputOnChange } from 'typings/inputs'; import { InputOnChange } from 'typings/inputs';
interface OAuthInputProps { export interface OAuthInputProps {
label?: string; label?: string;
name: string; name: string;
provider: string; provider: string;

View file

@ -7,7 +7,7 @@ function onCopy(e: SyntheticEvent) {
e.nativeEvent.stopImmediatePropagation(); e.nativeEvent.stopImmediatePropagation();
} }
function PasswordInput(props: TextInputProps<string>) { function PasswordInput(props: TextInputProps) {
return <TextInput {...props} type="password" onCopy={onCopy} />; return <TextInput {...props} type="password" onCopy={onCopy} />;
} }

View file

@ -23,7 +23,7 @@ import AutoSuggestInput from './AutoSuggestInput';
import FormInputButton from './FormInputButton'; import FormInputButton from './FormInputButton';
import styles from './PathInput.css'; import styles from './PathInput.css';
interface PathInputProps { export interface PathInputProps {
className?: string; className?: string;
name: string; name: string;
value?: string; value?: string;

View file

@ -5,7 +5,7 @@ import EnhancedSelectInput, {
EnhancedSelectInputValue, EnhancedSelectInputValue,
} from './EnhancedSelectInput'; } from './EnhancedSelectInput';
interface AvailabilitySelectInputProps export interface AvailabilitySelectInputProps
extends Omit< extends Omit<
EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string>, EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string>,
'values' 'values'

View file

@ -51,7 +51,7 @@ function createDownloadClientsSelector(
); );
} }
interface DownloadClientSelectInputProps export interface DownloadClientSelectInputProps
extends Omit< extends Omit<
EnhancedSelectInputProps<EnhancedSelectInputValue<number>, number>, EnhancedSelectInputProps<EnhancedSelectInputValue<number>, number>,
'values' 'values'

View file

@ -30,7 +30,7 @@ const selectIndexerFlagsValues = (selectedFlags: number) =>
} }
); );
interface IndexerFlagsSelectInputProps { export interface IndexerFlagsSelectInputProps {
name: string; name: string;
indexerFlags: number; indexerFlags: number;
onChange(payload: EnhancedSelectInputChanged<number>): void; onChange(payload: EnhancedSelectInputChanged<number>): void;

View file

@ -38,7 +38,7 @@ function createIndexersSelector(includeAny: boolean) {
); );
} }
interface IndexerSelectInputProps { export interface IndexerSelectInputProps {
name: string; name: string;
value: number; value: number;
includeAny?: boolean; includeAny?: boolean;

View file

@ -4,7 +4,7 @@ import EnhancedSelectInput, {
EnhancedSelectInputValue, EnhancedSelectInputValue,
} from './EnhancedSelectInput'; } from './EnhancedSelectInput';
interface LanguageSelectInputProps { export interface LanguageSelectInputProps {
name: string; name: string;
value: number; value: number;
values: EnhancedSelectInputValue<number>[]; values: EnhancedSelectInputValue<number>[];

View file

@ -6,7 +6,7 @@ import EnhancedSelectInput, {
EnhancedSelectInputValue, EnhancedSelectInputValue,
} from './EnhancedSelectInput'; } from './EnhancedSelectInput';
interface MonitorMoviesSelectInputProps export interface MonitorMoviesSelectInputProps
extends Omit< extends Omit<
EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string>, EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string>,
'values' 'values'

View file

@ -69,7 +69,7 @@ function createProviderOptionsSelector(
); );
} }
interface ProviderOptionSelectInputProps export interface ProviderOptionSelectInputProps
extends Omit< extends Omit<
EnhancedSelectInputProps<EnhancedSelectInputValue<unknown>, unknown>, EnhancedSelectInputProps<EnhancedSelectInputValue<unknown>, unknown>,
'values' 'values'

View file

@ -56,7 +56,7 @@ function createQualityProfilesSelector(
); );
} }
interface QualityProfileSelectInputProps export interface QualityProfileSelectInputProps
extends Omit< extends Omit<
EnhancedSelectInputProps< EnhancedSelectInputProps<
EnhancedSelectInputValue<number | string>, EnhancedSelectInputValue<number | string>,

View file

@ -24,7 +24,7 @@ export interface RootFolderSelectInputValue
isMissing?: boolean; isMissing?: boolean;
} }
interface RootFolderSelectInputProps export interface RootFolderSelectInputProps
extends Omit< extends Omit<
EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string>, EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string>,
'value' | 'values' 'value' | 'values'

View file

@ -66,7 +66,7 @@ function formatPermissions(permissions: number) {
return result; return result;
} }
interface UMaskInputProps { export interface UMaskInputProps {
name: string; name: string;
value: string; value: string;
hasError?: boolean; hasError?: boolean;

View file

@ -19,7 +19,7 @@ interface DeviceTag {
name: string; name: string;
} }
interface DeviceInputProps extends TagInputProps<DeviceTag> { export interface DeviceInputProps extends TagInputProps<DeviceTag> {
className?: string; className?: string;
name: string; name: string;
value: string[]; value: string[];

View file

@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { addTag } from 'Store/Actions/tagActions'; import { addTag } from 'Store/Actions/tagActions';
@ -12,10 +12,10 @@ interface MovieTag extends TagBase {
name: string; name: string;
} }
interface MovieTagInputProps { export interface MovieTagInputProps {
name: string; name: string;
value: number | number[]; value: number[];
onChange: (change: InputChanged<number | number[]>) => void; onChange: (change: InputChanged<number[]>) => void;
} }
const VALID_TAG_REGEX = new RegExp('[^-_a-z0-9]', 'i'); const VALID_TAG_REGEX = new RegExp('[^-_a-z0-9]', 'i');
@ -65,42 +65,22 @@ export default function MovieTagInput({
onChange, onChange,
}: MovieTagInputProps) { }: MovieTagInputProps) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const isArray = Array.isArray(value);
const arrayValue = useMemo(() => {
if (isArray) {
return value;
}
return value === 0 ? [] : [value];
}, [isArray, value]);
const { tags, tagList, allTags } = useSelector( const { tags, tagList, allTags } = useSelector(
createMovieTagsSelector(arrayValue) createMovieTagsSelector(value)
); );
const handleTagCreated = useCallback( const handleTagCreated = useCallback(
(tag: MovieTag) => { (tag: MovieTag) => {
if (isArray) { onChange({ name, value: [...value, tag.id] });
onChange({ name, value: [...value, tag.id] });
} else {
onChange({
name,
value: tag.id,
});
}
}, },
[name, value, isArray, onChange] [name, value, onChange]
); );
const handleTagAdd = useCallback( const handleTagAdd = useCallback(
(newTag: MovieTag) => { (newTag: MovieTag) => {
if (newTag.id) { if (newTag.id) {
if (isArray) { onChange({ name, value: [...value, newTag.id] });
onChange({ name, value: [...value, newTag.id] });
} else {
onChange({ name, value: newTag.id });
}
return; return;
} }
@ -116,21 +96,17 @@ export default function MovieTagInput({
); );
} }
}, },
[name, value, isArray, allTags, handleTagCreated, onChange, dispatch] [name, value, allTags, handleTagCreated, onChange, dispatch]
); );
const handleTagDelete = useCallback( const handleTagDelete = useCallback(
({ index }: { index: number }) => { ({ index }: { index: number }) => {
if (isArray) { const newValue = value.slice();
const newValue = value.slice(); newValue.splice(index, 1);
newValue.splice(index, 1);
onChange({ name, value: newValue }); onChange({ name, value: newValue });
} else {
onChange({ name, value: 0 });
}
}, },
[name, value, isArray, onChange] [name, value, onChange]
); );
return ( return (

View file

@ -13,7 +13,7 @@ interface TagSelectValue {
order: number; order: number;
} }
interface TagSelectInputProps extends TagInputProps<SelectTag> { export interface TagSelectInputProps extends TagInputProps<SelectTag> {
name: string; name: string;
value: number[]; value: number[];
values: TagSelectValue[]; values: TagSelectValue[];

View file

@ -8,7 +8,11 @@ interface TextTag extends TagBase {
name: string; name: string;
} }
interface TextTagInputProps extends TagInputProps<TextTag> { export interface TextTagInputProps
extends Omit<
TagInputProps<TextTag>,
'tags' | 'tagList' | 'onTagAdd' | 'onTagDelete'
> {
name: string; name: string;
value: string | string[]; value: string | string[];
onChange: (change: InputChanged<string[]>) => unknown; onChange: (change: InputChanged<string[]>) => unknown;

View file

@ -9,7 +9,7 @@ import React, {
import { InputChanged } from 'typings/inputs'; import { InputChanged } from 'typings/inputs';
import styles from './TextArea.css'; import styles from './TextArea.css';
interface TextAreaProps { export interface TextAreaProps {
className?: string; className?: string;
readOnly?: boolean; readOnly?: boolean;
autoFocus?: boolean; autoFocus?: boolean;

View file

@ -7,13 +7,11 @@ import React, {
useEffect, useEffect,
useRef, useRef,
} from 'react'; } from 'react';
import { InputType } from 'Helpers/Props/inputTypes';
import { FileInputChanged, InputChanged } from 'typings/inputs'; import { FileInputChanged, InputChanged } from 'typings/inputs';
import styles from './TextInput.css'; import styles from './TextInput.css';
export interface TextInputProps<T> { export interface CommonTextInputProps {
className?: string; className?: string;
type?: InputType;
readOnly?: boolean; readOnly?: boolean;
autoFocus?: boolean; autoFocus?: boolean;
placeholder?: string; placeholder?: string;
@ -25,14 +23,23 @@ export interface TextInputProps<T> {
step?: number; step?: number;
min?: number; min?: number;
max?: number; max?: number;
onChange: (change: InputChanged<T> | FileInputChanged) => void; onFocus?: (event: FocusEvent<HTMLInputElement, Element>) => void;
onFocus?: (event: FocusEvent) => void;
onBlur?: (event: SyntheticEvent) => void; onBlur?: (event: SyntheticEvent) => void;
onCopy?: (event: SyntheticEvent) => void; onCopy?: (event: SyntheticEvent) => void;
onSelectionChange?: (start: number | null, end: number | null) => void; onSelectionChange?: (start: number | null, end: number | null) => void;
} }
function TextInput<T>({ export interface TextInputProps extends CommonTextInputProps {
type?: 'date' | 'number' | 'password' | 'text';
onChange: (change: InputChanged<string>) => void;
}
export interface FileInputProps extends CommonTextInputProps {
type: 'file';
onChange: (change: FileInputChanged) => void;
}
function TextInput({
className = styles.input, className = styles.input,
type = 'text', type = 'text',
readOnly = false, readOnly = false,
@ -51,7 +58,7 @@ function TextInput<T>({
onCopy, onCopy,
onChange, onChange,
onSelectionChange, onSelectionChange,
}: TextInputProps<T>) { }: TextInputProps | FileInputProps): JSX.Element {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const selectionTimeout = useRef<ReturnType<typeof setTimeout>>(); const selectionTimeout = useRef<ReturnType<typeof setTimeout>>();
const selectionStart = useRef<number | null>(); const selectionStart = useRef<number | null>();
@ -95,7 +102,7 @@ function TextInput<T>({
); );
const handleFocus = useCallback( const handleFocus = useCallback(
(event: FocusEvent) => { (event: FocusEvent<HTMLInputElement, Element>) => {
onFocus?.(event); onFocus?.(event);
selectionChanged(); selectionChanged();

View file

@ -4,7 +4,7 @@ export const CAPTCHA = 'captcha';
export const CHECK = 'check'; export const CHECK = 'check';
export const DEVICE = 'device'; export const DEVICE = 'device';
export const KEY_VALUE_LIST = 'keyValueList'; export const KEY_VALUE_LIST = 'keyValueList';
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect'; export const MONITOR_MOVIES_SELECT = 'monitorMoviesSelect';
export const FLOAT = 'float'; export const FLOAT = 'float';
export const NUMBER = 'number'; export const NUMBER = 'number';
export const OAUTH = 'oauth'; export const OAUTH = 'oauth';
@ -33,7 +33,7 @@ export const all = [
CHECK, CHECK,
DEVICE, DEVICE,
KEY_VALUE_LIST, KEY_VALUE_LIST,
MOVIE_MONITORED_SELECT, MONITOR_MOVIES_SELECT,
FLOAT, FLOAT,
NUMBER, NUMBER,
OAUTH, OAUTH,
@ -61,9 +61,10 @@ export type InputType =
| 'availabilitySelect' | 'availabilitySelect'
| 'captcha' | 'captcha'
| 'check' | 'check'
| 'date'
| 'device' | 'device'
| 'keyValueList' | 'keyValueList'
| 'movieMonitoredSelect' | 'monitorMoviesSelect'
| 'file' | 'file'
| 'float' | 'float'
| 'number' | 'number'

View file

@ -86,8 +86,8 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
}, [items]); }, [items]);
const onQualityChange = useCallback( const onQualityChange = useCallback(
({ value }: { value: string }) => { ({ value }: { value: number }) => {
setQualityId(parseInt(value)); setQualityId(value);
}, },
[setQualityId] [setQualityId]
); );
@ -128,7 +128,7 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
{isFetching && <LoadingIndicator />} {isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? ( {!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('QualitiesLoadError')}</Alert> <Alert kind={kinds.DANGER}>{translate('QualitiesLoadError')}</Alert>

View file

@ -183,6 +183,7 @@ function EditMovieModalContent({
type={inputTypes.PATH} type={inputTypes.PATH}
name="path" name="path"
{...settings.path} {...settings.path}
includeFiles={false}
onChange={handleInputChange} onChange={handleInputChange}
/> />
</FormGroup> </FormGroup>

View file

@ -141,7 +141,7 @@ function DeleteMovieModalContent(props: DeleteMovieModalContentProps) {
? translate('DeleteMovieFoldersHelpText') ? translate('DeleteMovieFoldersHelpText')
: translate('DeleteMovieFolderHelpText') : translate('DeleteMovieFolderHelpText')
} }
kind={kinds.DANGER} kind="danger"
onChange={onDeleteFilesChange} onChange={onDeleteFilesChange}
/> />
</FormGroup> </FormGroup>

View file

@ -9,6 +9,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props'; import { inputTypes } from 'Helpers/Props';
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal'; import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './EditMoviesModalContent.css'; import styles from './EditMoviesModalContent.css';
@ -104,19 +105,19 @@ function EditMoviesModalContent(props: EditMoviesModalContentProps) {
); );
const onInputChange = useCallback( const onInputChange = useCallback(
({ name, value }: { name: string; value: string }) => { ({ name, value }: InputChanged) => {
switch (name) { switch (name) {
case 'monitored': case 'monitored':
setMonitored(value); setMonitored(value as string);
break; break;
case 'qualityProfileId': case 'qualityProfileId':
setQualityProfileId(value); setQualityProfileId(value as string);
break; break;
case 'minimumAvailability': case 'minimumAvailability':
setMinimumAvailability(value); setMinimumAvailability(value as string);
break; break;
case 'rootFolderPath': case 'rootFolderPath':
setRootFolderPath(value); setRootFolderPath(value as string);
break; break;
default: default:
console.warn('EditMoviesModalContent Unknown Input'); console.warn('EditMoviesModalContent Unknown Input');

View file

@ -8,6 +8,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props'; import { inputTypes } from 'Helpers/Props';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './ManageDownloadClientsEditModalContent.css'; import styles from './ManageDownloadClientsEditModalContent.css';
@ -57,7 +58,7 @@ function ManageDownloadClientsEditModalContent(
const [removeCompletedDownloads, setRemoveCompletedDownloads] = const [removeCompletedDownloads, setRemoveCompletedDownloads] =
useState(NO_CHANGE); useState(NO_CHANGE);
const [removeFailedDownloads, setRemoveFailedDownloads] = useState(NO_CHANGE); const [removeFailedDownloads, setRemoveFailedDownloads] = useState(NO_CHANGE);
const [priority, setPriority] = useState<null | string | number>(null); const [priority, setPriority] = useState<null | number>(null);
const save = useCallback(() => { const save = useCallback(() => {
let hasChanges = false; let hasChanges = false;
@ -97,29 +98,26 @@ function ManageDownloadClientsEditModalContent(
onModalClose, onModalClose,
]); ]);
const onInputChange = useCallback( const onInputChange = useCallback(({ name, value }: InputChanged) => {
({ name, value }: { name: string; value: string }) => { switch (name) {
switch (name) { case 'enable':
case 'enable': setEnable(value as string);
setEnable(value); break;
break; case 'priority':
case 'priority': setPriority(value as number);
setPriority(value); break;
break; case 'removeCompletedDownloads':
case 'removeCompletedDownloads': setRemoveCompletedDownloads(value as string);
setRemoveCompletedDownloads(value); break;
break; case 'removeFailedDownloads':
case 'removeFailedDownloads': setRemoveFailedDownloads(value as string);
setRemoveFailedDownloads(value); break;
break; default:
default: console.warn(
console.warn( `EditDownloadClientsModalContent Unknown Input: '${name}'`
`EditDownloadClientsModalContent Unknown Input: '${name}'` );
); }
} }, []);
},
[]
);
const selectedCount = downloadClientIds.length; const selectedCount = downloadClientIds.length;

View file

@ -36,6 +36,7 @@ function BackupSettings(props) {
type={inputTypes.PATH} type={inputTypes.PATH}
name="backupFolder" name="backupFolder"
helpText={translate('BackupFolderHelpText')} helpText={translate('BackupFolderHelpText')}
includeFiles={false}
onChange={onInputChange} onChange={onInputChange}
{...backupFolder} {...backupFolder}
/> />

View file

@ -22,6 +22,7 @@ import {
} from 'Store/Actions/settingsActions'; } from 'Store/Actions/settingsActions';
import selectSettings from 'Store/Selectors/selectSettings'; import selectSettings from 'Store/Selectors/selectSettings';
import ImportListExclusion from 'typings/ImportListExclusion'; import ImportListExclusion from 'typings/ImportListExclusion';
import { InputChanged } from 'typings/inputs';
import { PendingSection } from 'typings/pending'; import { PendingSection } from 'typings/pending';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './EditImportListExclusionModalContent.css'; import styles from './EditImportListExclusionModalContent.css';
@ -104,9 +105,9 @@ function EditImportListExclusionModalContent({
}, [dispatch, id]); }, [dispatch, id]);
const onInputChange = useCallback( const onInputChange = useCallback(
(payload: { name: string; value: string | number }) => { (change: InputChanged) => {
// @ts-expect-error 'setImportListExclusionValue' isn't typed yet // @ts-expect-error 'setImportListExclusionValue' isn't typed yet
dispatch(setImportListExclusionValue(payload)); dispatch(setImportListExclusionValue(change));
}, },
[dispatch] [dispatch]
); );

View file

@ -142,7 +142,7 @@ function EditImportListModalContent(props) {
<FormLabel>{translate('Monitor')}</FormLabel> <FormLabel>{translate('Monitor')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.MOVIE_MONITORED_SELECT} type={inputTypes.MONITOR_MOVIES_SELECT}
name="monitor" name="monitor"
helpText={translate('ListMonitorMovieHelpText')} helpText={translate('ListMonitorMovieHelpText')}
{...monitor} {...monitor}

View file

@ -8,6 +8,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props'; import { inputTypes } from 'Helpers/Props';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './ManageImportListsEditModalContent.css'; import styles from './ManageImportListsEditModalContent.css';
@ -106,30 +107,27 @@ function ManageImportListsEditModalContent(
onModalClose, onModalClose,
]); ]);
const onInputChange = useCallback( const onInputChange = useCallback(({ name, value }: InputChanged) => {
({ name, value }: { name: string; value: string }) => { switch (name) {
switch (name) { case 'enabled':
case 'enabled': setEnabled(value as string);
setEnabled(value); break;
break; case 'enableAuto':
case 'enableAuto': setEnableAuto(value as string);
setEnableAuto(value); break;
break; case 'qualityProfileId':
case 'qualityProfileId': setQualityProfileId(value as string);
setQualityProfileId(value); break;
break; case 'minimumAvailability':
case 'minimumAvailability': setMinimumAvailability(value as string);
setMinimumAvailability(value); break;
break; case 'rootFolderPath':
case 'rootFolderPath': setRootFolderPath(value as string);
setRootFolderPath(value); break;
break; default:
default: console.warn(`EditImportListModalContent Unknown Input: '${name}'`);
console.warn(`EditImportListModalContent Unknown Input: '${name}'`); }
} }, []);
},
[]
);
const selectedCount = importListIds.length; const selectedCount = importListIds.length;

View file

@ -8,6 +8,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props'; import { inputTypes } from 'Helpers/Props';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './ManageIndexersEditModalContent.css'; import styles from './ManageIndexersEditModalContent.css';
@ -57,7 +58,7 @@ function ManageIndexersEditModalContent(
const [enableAutomaticSearch, setEnableAutomaticSearch] = useState(NO_CHANGE); const [enableAutomaticSearch, setEnableAutomaticSearch] = useState(NO_CHANGE);
const [enableInteractiveSearch, setEnableInteractiveSearch] = const [enableInteractiveSearch, setEnableInteractiveSearch] =
useState(NO_CHANGE); useState(NO_CHANGE);
const [priority, setPriority] = useState<null | string | number>(null); const [priority, setPriority] = useState<null | number>(null);
const save = useCallback(() => { const save = useCallback(() => {
let hasChanges = false; let hasChanges = false;
@ -97,27 +98,24 @@ function ManageIndexersEditModalContent(
onModalClose, onModalClose,
]); ]);
const onInputChange = useCallback( const onInputChange = useCallback(({ name, value }: InputChanged) => {
({ name, value }: { name: string; value: string }) => { switch (name) {
switch (name) { case 'enableRss':
case 'enableRss': setEnableRss(value as string);
setEnableRss(value); break;
break; case 'enableAutomaticSearch':
case 'enableAutomaticSearch': setEnableAutomaticSearch(value as string);
setEnableAutomaticSearch(value); break;
break; case 'enableInteractiveSearch':
case 'enableInteractiveSearch': setEnableInteractiveSearch(value as string);
setEnableInteractiveSearch(value); break;
break; case 'priority':
case 'priority': setPriority(value as number);
setPriority(value); break;
break; default:
default: console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`); }
} }, []);
},
[]
);
const selectedCount = indexerIds.length; const selectedCount = indexerIds.length;

View file

@ -19,6 +19,7 @@ import {
setNamingSettingsValue, setNamingSettingsValue,
} from 'Store/Actions/settingsActions'; } from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import { InputChanged } from 'typings/inputs';
import NamingConfig from 'typings/Settings/NamingConfig'; import NamingConfig from 'typings/Settings/NamingConfig';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import NamingModal from './NamingModal'; import NamingModal from './NamingModal';
@ -77,9 +78,9 @@ function Naming() {
}, [dispatch]); }, [dispatch]);
const handleInputChange = useCallback( const handleInputChange = useCallback(
({ name, value }: { name: string; value: string }) => { (change: InputChanged) => {
// @ts-expect-error 'setNamingSettingsValue' isn't typed yet // @ts-expect-error 'setNamingSettingsValue' isn't typed yet
dispatch(setNamingSettingsValue({ name, value })); dispatch(setNamingSettingsValue(change));
if (namingExampleTimeout.current) { if (namingExampleTimeout.current) {
clearTimeout(namingExampleTimeout.current); clearTimeout(namingExampleTimeout.current);

View file

@ -19,6 +19,7 @@ import {
setReleaseProfileValue, setReleaseProfileValue,
} from 'Store/Actions/Settings/releaseProfiles'; } from 'Store/Actions/Settings/releaseProfiles';
import selectSettings from 'Store/Selectors/selectSettings'; import selectSettings from 'Store/Selectors/selectSettings';
import { InputChanged } from 'typings/inputs';
import ReleaseProfile from 'typings/Settings/ReleaseProfile'; import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './EditReleaseProfileModalContent.css'; import styles from './EditReleaseProfileModalContent.css';
@ -102,9 +103,9 @@ function EditReleaseProfileModalContent({
}, [dispatch, id]); }, [dispatch, id]);
const handleInputChange = useCallback( const handleInputChange = useCallback(
(payload: { name: string; value: string | number }) => { (change: InputChanged) => {
// @ts-expect-error 'setReleaseProfileValue' isn't typed yet // @ts-expect-error 'setReleaseProfileValue' isn't typed yet
dispatch(setReleaseProfileValue(payload)); dispatch(setReleaseProfileValue(change));
}, },
[dispatch] [dispatch]
); );
@ -125,7 +126,6 @@ function EditReleaseProfileModalContent({
name="name" name="name"
{...name} {...name}
placeholder={translate('OptionalName')} placeholder={translate('OptionalName')}
canEdit={true}
onChange={handleInputChange} onChange={handleInputChange}
/> />
</FormGroup> </FormGroup>