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>
<FormInputGroup
type={inputTypes.MOVIE_MONITORED_SELECT}
type={inputTypes.MONITOR_MOVIES_SELECT}
name="monitor"
onChange={onInputChange}
{...monitor}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -41,10 +41,11 @@
.checkbox:focus + .input {
outline: 0;
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);
background-color: var(--dangerColor);
@ -53,7 +54,7 @@
}
}
.primaryIsChecked {
.primary {
border-color: var(--primaryColor);
background-color: var(--primaryColor);
@ -62,7 +63,7 @@
}
}
.successIsChecked {
.success {
border-color: var(--successColor);
background-color: var(--successColor);
@ -71,7 +72,7 @@
}
}
.warningIsChecked {
.warning {
border-color: var(--warningColor);
background-color: var(--warningColor);

View file

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

View file

@ -11,7 +11,7 @@ interface ChangeEvent<T = Element> extends SyntheticEvent<T, MouseEvent> {
target: EventTarget & T;
}
interface CheckInputProps {
export interface CheckInputProps {
className?: string;
containerClassName?: string;
name: string;
@ -45,7 +45,6 @@ function CheckInput(props: CheckInputProps) {
const isChecked = value === checkedValue;
const isUnchecked = value === uncheckedValue;
const isIndeterminate = !isChecked && !isUnchecked;
const isCheckClass: keyof typeof styles = `${kind}IsChecked`;
const toggleChecked = useCallback(
(checked: boolean, shiftKey: boolean) => {
@ -112,7 +111,7 @@ function CheckInput(props: CheckInputProps) {
<div
className={classNames(
className,
isChecked ? styles[isCheckClass] : styles.isNotChecked,
isChecked ? styles[kind] : styles.isNotChecked,
isIndeterminate && styles.isIndeterminate,
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 { inputTypes } from 'Helpers/Props';
import { InputType } from 'Helpers/Props/inputTypes';
import { Kind } from 'Helpers/Props/kinds';
import { ValidationError, ValidationWarning } from 'typings/pending';
import translate from 'Utilities/String/translate';
import AutoCompleteInput from './AutoCompleteInput';
import CaptchaInput from './CaptchaInput';
import CheckInput from './CheckInput';
import AutoCompleteInput, { AutoCompleteInputProps } from './AutoCompleteInput';
import CaptchaInput, { CaptchaInputProps } from './CaptchaInput';
import CheckInput, { CheckInputProps } from './CheckInput';
import { FormInputButtonProps } from './FormInputButton';
import FormInputHelpText from './FormInputHelpText';
import KeyValueListInput from './KeyValueListInput';
import NumberInput from './NumberInput';
import OAuthInput from './OAuthInput';
import KeyValueListInput, { KeyValueListInputProps } from './KeyValueListInput';
import NumberInput, { NumberInputProps } from './NumberInput';
import OAuthInput, { OAuthInputProps } from './OAuthInput';
import PasswordInput from './PasswordInput';
import PathInput from './PathInput';
import AvailabilitySelectInput from './Select/AvailabilitySelectInput';
import DownloadClientSelectInput from './Select/DownloadClientSelectInput';
import EnhancedSelectInput from './Select/EnhancedSelectInput';
import IndexerFlagsSelectInput from './Select/IndexerFlagsSelectInput';
import IndexerSelectInput from './Select/IndexerSelectInput';
import LanguageSelectInput from './Select/LanguageSelectInput';
import MonitorMoviesSelectInput from './Select/MonitorMoviesSelectInput';
import ProviderDataSelectInput from './Select/ProviderOptionSelectInput';
import QualityProfileSelectInput from './Select/QualityProfileSelectInput';
import RootFolderSelectInput from './Select/RootFolderSelectInput';
import UMaskInput from './Select/UMaskInput';
import DeviceInput from './Tag/DeviceInput';
import MovieTagInput from './Tag/MovieTagInput';
import TagSelectInput from './Tag/TagSelectInput';
import TextTagInput from './Tag/TextTagInput';
import TextArea from './TextArea';
import TextInput from './TextInput';
import PathInput, { PathInputProps } from './PathInput';
import AvailabilitySelectInput, {
AvailabilitySelectInputProps,
} from './Select/AvailabilitySelectInput';
import DownloadClientSelectInput, {
DownloadClientSelectInputProps,
} from './Select/DownloadClientSelectInput';
import EnhancedSelectInput, {
EnhancedSelectInputProps,
} from './Select/EnhancedSelectInput';
import IndexerFlagsSelectInput, {
IndexerFlagsSelectInputProps,
} from './Select/IndexerFlagsSelectInput';
import IndexerSelectInput, {
IndexerSelectInputProps,
} from './Select/IndexerSelectInput';
import LanguageSelectInput, {
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';
function getComponent(type: InputType) {
switch (type) {
case inputTypes.AUTO_COMPLETE:
return AutoCompleteInput;
const componentMap: Record<InputType, ElementType> = {
autoComplete: AutoCompleteInput,
availabilitySelect: AvailabilitySelectInput,
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:
return AvailabilitySelectInput;
// type Components = typeof componentMap;
case inputTypes.CAPTCHA:
return CaptchaInput;
type PickProps<V, C extends InputType> = C extends 'text'
? 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:
return CheckInput;
case inputTypes.DEVICE:
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;
}
export interface FormInputGroupValues<T> {
key: T;
value: string;
hint?: string;
}
// 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;
}
interface FormInputGroupProps<T> {
export type FormInputGroupProps<V, C extends InputType> = Omit<
PickProps<V, C>,
'className'
> & {
type: C;
className?: string;
containerClassName?: string;
inputClassName?: string;
autoFocus?: boolean;
autocomplete?: string;
name: string;
value?: unknown;
values?: unknown[];
isDisabled?: boolean;
type?: InputType;
kind?: Kind;
min?: number;
max?: number;
unit?: string;
buttons?: ReactNode | ReactNode[];
helpText?: string;
helpTexts?: string[];
helpTextWarning?: string;
helpLink?: string;
placeholder?: string;
autoFocus?: boolean;
includeNoChange?: boolean;
includeNoChangeDisabled?: boolean;
valueOptions?: object;
selectedValueOptions?: object;
indexerFlags?: number;
pending?: boolean;
canEdit?: boolean;
includeAny?: boolean;
delimiters?: string[];
readOnly?: boolean;
placeholder?: string;
unit?: string;
errors?: (ValidationMessage | ValidationError)[];
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 {
className = styles.inputGroup,
containerClassName = styles.inputGroupContainer,
inputClassName,
type = 'text',
type,
unit,
buttons = [],
helpText,
@ -173,7 +204,7 @@ function FormInputGroup<T>(props: FormInputGroupProps<T>) {
...otherProps
} = props;
const InputComponent = getComponent(type);
const InputComponent = componentMap[type];
const checkInput = type === inputTypes.CHECK;
const hasError = !!errors.length;
const hasWarning = !hasError && !!warnings.length;
@ -185,7 +216,7 @@ function FormInputGroup<T>(props: FormInputGroupProps<T>) {
<div className={containerClassName}>
<div className={className}>
<div className={styles.inputContainer}>
{/* @ts-expect-error - need to pass through all the expected options */}
{/* @ts-expect-error - types are validated already */}
<InputComponent
className={inputClassName}
helpText={helpText}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -19,7 +19,7 @@ interface DeviceTag {
name: string;
}
interface DeviceInputProps extends TagInputProps<DeviceTag> {
export interface DeviceInputProps extends TagInputProps<DeviceTag> {
className?: string;
name: 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 { createSelector } from 'reselect';
import { addTag } from 'Store/Actions/tagActions';
@ -12,10 +12,10 @@ interface MovieTag extends TagBase {
name: string;
}
interface MovieTagInputProps {
export interface MovieTagInputProps {
name: string;
value: number | number[];
onChange: (change: InputChanged<number | number[]>) => void;
value: number[];
onChange: (change: InputChanged<number[]>) => void;
}
const VALID_TAG_REGEX = new RegExp('[^-_a-z0-9]', 'i');
@ -65,42 +65,22 @@ export default function MovieTagInput({
onChange,
}: MovieTagInputProps) {
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(
createMovieTagsSelector(arrayValue)
createMovieTagsSelector(value)
);
const handleTagCreated = useCallback(
(tag: MovieTag) => {
if (isArray) {
onChange({ name, value: [...value, tag.id] });
} else {
onChange({
name,
value: tag.id,
});
}
onChange({ name, value: [...value, tag.id] });
},
[name, value, isArray, onChange]
[name, value, onChange]
);
const handleTagAdd = useCallback(
(newTag: MovieTag) => {
if (newTag.id) {
if (isArray) {
onChange({ name, value: [...value, newTag.id] });
} else {
onChange({ name, value: newTag.id });
}
onChange({ name, value: [...value, newTag.id] });
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(
({ index }: { index: number }) => {
if (isArray) {
const newValue = value.slice();
newValue.splice(index, 1);
const newValue = value.slice();
newValue.splice(index, 1);
onChange({ name, value: newValue });
} else {
onChange({ name, value: 0 });
}
onChange({ name, value: newValue });
},
[name, value, isArray, onChange]
[name, value, onChange]
);
return (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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