Improve typings in FormInputGroup

pull/7640/head
Mark McDowall 2 months ago
parent b218461678
commit 6838f068bc
No known key found for this signature in database

@ -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;

@ -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;

@ -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);

@ -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;

@ -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
)}

@ -1,51 +1,73 @@
import React, { ComponentType, FocusEvent, ReactNode } from 'react';
import React, { ElementType, ReactNode } from 'react';
import Link from 'Components/Link/Link';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { inputTypes } from 'Helpers/Props';
import { InputType } from 'Helpers/Props/inputTypes';
import { Kind } from 'Helpers/Props/kinds';
import { InputChanged } from 'typings/inputs';
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 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 MonitorEpisodesSelectInput from './Select/MonitorEpisodesSelectInput';
import MonitorNewItemsSelectInput from './Select/MonitorNewItemsSelectInput';
import ProviderDataSelectInput from './Select/ProviderOptionSelectInput';
import QualityProfileSelectInput from './Select/QualityProfileSelectInput';
import RootFolderSelectInput from './Select/RootFolderSelectInput';
import SeriesTypeSelectInput from './Select/SeriesTypeSelectInput';
import UMaskInput from './Select/UMaskInput';
import DeviceInput from './Tag/DeviceInput';
import SeriesTagInput from './Tag/SeriesTagInput';
import TagSelectInput from './Tag/TagSelectInput';
import TextTagInput from './Tag/TextTagInput';
import TextArea from './TextArea';
import TextInput from './TextInput';
import PathInput, { PathInputProps } from './PathInput';
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 MonitorEpisodesSelectInput, {
MonitorEpisodesSelectInputProps,
} from './Select/MonitorEpisodesSelectInput';
import MonitorNewItemsSelectInput, {
MonitorNewItemsSelectInputProps,
} from './Select/MonitorNewItemsSelectInput';
import ProviderDataSelectInput, {
ProviderOptionSelectInputProps,
} from './Select/ProviderOptionSelectInput';
import QualityProfileSelectInput, {
QualityProfileSelectInputProps,
} from './Select/QualityProfileSelectInput';
import RootFolderSelectInput, {
RootFolderSelectInputProps,
} from './Select/RootFolderSelectInput';
import SeriesTypeSelectInput, {
SeriesTypeSelectInputProps,
} from './Select/SeriesTypeSelectInput';
import UMaskInput, { UMaskInputProps } from './Select/UMaskInput';
import DeviceInput, { DeviceInputProps } from './Tag/DeviceInput';
import SeriesTagInput, { SeriesTagInputProps } from './Tag/SeriesTagInput';
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';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const componentMap: Record<InputType, ComponentType<any>> = {
const componentMap: Record<InputType, ElementType> = {
autoComplete: 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,
monitorEpisodesSelect: MonitorEpisodesSelectInput,
@ -55,21 +77,84 @@ const componentMap: Record<InputType, ComponentType<any>> = {
password: PasswordInput,
path: PathInput,
qualityProfileSelect: QualityProfileSelectInput,
indexerSelect: IndexerSelectInput,
indexerFlagsSelect: IndexerFlagsSelectInput,
downloadClientSelect: DownloadClientSelectInput,
rootFolderSelect: RootFolderSelectInput,
select: EnhancedSelectInput,
dynamicSelect: ProviderDataSelectInput,
tag: SeriesTagInput,
seriesTag: SeriesTagInput,
seriesTypeSelect: SeriesTypeSelectInput,
tag: SeriesTagInput,
tagSelect: TagSelectInput,
text: TextInput,
textArea: TextArea,
textTag: TextTagInput,
tagSelect: TagSelectInput,
umask: UMaskInput,
};
} as const;
// type Components = typeof componentMap;
type PickProps<V, C extends InputType> = C extends 'text'
? TextInputProps
: C extends 'autoComplete'
? AutoCompleteInputProps
: 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 'monitorEpisodesSelect'
? MonitorEpisodesSelectInputProps
: C extends 'monitorNewItemsSelect'
? MonitorNewItemsSelectInputProps
: 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 'seriesTag'
? SeriesTagInputProps
: C extends 'seriesTypeSelect'
? SeriesTypeSelectInputProps
: C extends 'tag'
? SeriesTagInputProps
: C extends 'tagSelect'
? TagSelectInputProps
: C extends 'text'
? TextInputProps
: C extends 'textArea'
? TextAreaProps
: C extends 'textTag'
? TextTagInputProps
: C extends 'umask'
? UMaskInputProps
: never;
export interface FormInputGroupValues<T> {
key: T;
@ -82,53 +167,37 @@ 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?: FormInputGroupValues<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;
includeFiles?: boolean;
includeMissingValue?: boolean;
includeNoChange?: boolean;
includeNoChangeDisabled?: boolean;
valueOptions?: object;
selectedValueOptions?: object;
selectOptionsProviderAction?: string;
indexerFlags?: number;
pending?: boolean;
protocol?: DownloadProtocol;
canEdit?: boolean;
includeAny?: boolean;
delimiters?: string[];
readOnly?: boolean;
placeholder?: string;
unit?: string;
errors?: (ValidationMessage | ValidationError)[];
warnings?: (ValidationMessage | ValidationWarning)[];
onChange: (change: InputChanged<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,
@ -153,6 +222,7 @@ function FormInputGroup<T>(props: FormInputGroupProps<T>) {
<div className={containerClassName}>
<div className={className}>
<div className={styles.inputContainer}>
{/* @ts-expect-error - tpyes are validated already */}
<InputComponent
className={inputClassName}
helpText={helpText}

@ -24,7 +24,8 @@ function parseValue(
return newValue;
}
interface NumberInputProps extends Omit<TextInputProps, 'value' | 'onChange'> {
export interface NumberInputProps
extends Omit<TextInputProps, 'value' | 'onChange'> {
value?: number | null;
min?: number;
max?: number;

@ -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;

@ -24,7 +24,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;

@ -120,6 +120,7 @@ function ProviderFieldFormGroup<T>({
helpTextWarning={helpTextWarning}
helpLink={helpLink}
placeholder={placeholder}
// @ts-expect-error - this isn;'t available on all types
selectOptionsProviderAction={selectOptionsProviderAction}
value={value}
values={selectValues}

@ -51,8 +51,11 @@ function createDownloadClientsSelector(
);
}
interface DownloadClientSelectInputProps
extends EnhancedSelectInputProps<EnhancedSelectInputValue<number>, number> {
export interface DownloadClientSelectInputProps
extends Omit<
EnhancedSelectInputProps<EnhancedSelectInputValue<number>, number>,
'values'
> {
name: string;
value: number;
includeAny?: boolean;

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

@ -38,7 +38,7 @@ function createIndexersSelector(includeAny: boolean) {
);
}
interface IndexerSelectInputConnectorProps {
export interface IndexerSelectInputProps {
name: string;
value: number;
includeAny?: boolean;
@ -50,7 +50,7 @@ function IndexerSelectInput({
value,
includeAny = false,
onChange,
}: IndexerSelectInputConnectorProps) {
}: IndexerSelectInputProps) {
const dispatch = useDispatch();
const { isFetching, isPopulated, values } = useSelector(
createIndexersSelector(includeAny)

@ -12,20 +12,20 @@ interface LanguageSelectInputOnChangeProps {
value: number | string | Language;
}
interface LanguageSelectInputProps {
export interface LanguageSelectInputProps {
name: string;
value: number | string | Language;
includeNoChange: boolean;
includeNoChange?: boolean;
includeNoChangeDisabled?: boolean;
includeMixed: boolean;
includeMixed?: boolean;
onChange: (payload: LanguageSelectInputOnChangeProps) => void;
}
export default function LanguageSelectInput({
value,
includeNoChange,
includeNoChange = false,
includeNoChangeDisabled,
includeMixed,
includeMixed = false,
onChange,
...otherProps
}: LanguageSelectInputProps) {

@ -6,13 +6,13 @@ import EnhancedSelectInput, {
EnhancedSelectInputValue,
} from './EnhancedSelectInput';
interface MonitorEpisodesSelectInputProps
export interface MonitorEpisodesSelectInputProps
extends Omit<
EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string>,
'values'
> {
includeNoChange: boolean;
includeMixed: boolean;
includeNoChange?: boolean;
includeMixed?: boolean;
}
function MonitorEpisodesSelectInput(props: MonitorEpisodesSelectInputProps) {

@ -5,19 +5,20 @@ import EnhancedSelectInput, {
EnhancedSelectInputValue,
} from './EnhancedSelectInput';
interface MonitorNewItemsSelectInputProps
export interface MonitorNewItemsSelectInputProps
extends Omit<
EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string>,
'values'
> {
includeNoChange?: boolean;
includeNoChangeDisabled?: boolean;
includeMixed?: boolean;
onChange: (...args: unknown[]) => unknown;
}
function MonitorNewItemsSelectInput(props: MonitorNewItemsSelectInputProps) {
const {
includeNoChange = false,
includeNoChangeDisabled = true,
includeMixed = false,
...otherProps
} = props;
@ -30,7 +31,7 @@ function MonitorNewItemsSelectInput(props: MonitorNewItemsSelectInputProps) {
values.unshift({
key: 'noChange',
value: 'No Change',
isDisabled: true,
isDisabled: includeNoChangeDisabled,
});
}

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

@ -56,7 +56,7 @@ function createQualityProfilesSelector(
);
}
interface QualityProfileSelectInputConnectorProps
export interface QualityProfileSelectInputProps
extends Omit<
EnhancedSelectInputProps<
EnhancedSelectInputValue<number | string>,
@ -78,7 +78,7 @@ function QualityProfileSelectInput({
includeMixed = false,
onChange,
...otherProps
}: QualityProfileSelectInputConnectorProps) {
}: QualityProfileSelectInputProps) {
const values = useSelector(
createQualityProfilesSelector(
includeNoChange,

@ -24,16 +24,16 @@ export interface RootFolderSelectInputValue
isMissing?: boolean;
}
interface RootFolderSelectInputProps
export interface RootFolderSelectInputProps
extends Omit<
EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string>,
'value' | 'values'
> {
name: string;
value?: string;
isSaving: boolean;
saveError?: object;
includeNoChange: boolean;
includeMissingValue?: boolean;
includeNoChange?: boolean;
includeNoChangeDisabled?: boolean;
}
function createRootFolderOptionsSelector(
@ -107,13 +107,20 @@ function createRootFolderOptionsSelector(
function RootFolderSelectInput({
name,
value,
includeMissingValue = true,
includeNoChange = false,
includeNoChangeDisabled = true,
onChange,
...otherProps
}: RootFolderSelectInputProps) {
const dispatch = useDispatch();
const { values, isSaving, saveError } = useSelector(
createRootFolderOptionsSelector(value, true, includeNoChange, false)
createRootFolderOptionsSelector(
value,
includeMissingValue,
includeNoChange,
includeNoChangeDisabled
)
);
const [isAddNewRootFolderModalOpen, setIsAddNewRootFolderModalOpen] =
useState(false);

@ -8,11 +8,14 @@ import EnhancedSelectInput, {
import SeriesTypeSelectInputOption from './SeriesTypeSelectInputOption';
import SeriesTypeSelectInputSelectedValue from './SeriesTypeSelectInputSelectedValue';
interface SeriesTypeSelectInputProps
extends EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string> {
includeNoChange: boolean;
export interface SeriesTypeSelectInputProps
extends Omit<
EnhancedSelectInputProps<EnhancedSelectInputValue<string>, string>,
'values'
> {
includeNoChange?: boolean;
includeNoChangeDisabled?: boolean;
includeMixed: boolean;
includeMixed?: boolean;
}
export interface ISeriesTypeOption {

@ -1,5 +1,4 @@
/* eslint-disable no-bitwise */
import PropTypes from 'prop-types';
import React, { SyntheticEvent } from 'react';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
@ -67,7 +66,7 @@ function formatPermissions(permissions: number) {
return result;
}
interface UMaskInputProps {
export interface UMaskInputProps {
name: string;
value: string;
hasError?: boolean;
@ -129,14 +128,4 @@ function UMaskInput({ name, value, onChange }: UMaskInputProps) {
);
}
UMaskInput.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
};
export default UMaskInput;

@ -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[];

@ -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 SeriesTag extends TagBase {
name: string;
}
interface SeriesTagInputProps {
export interface SeriesTagInputProps {
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 SeriesTagInput({
onChange,
}: SeriesTagInputProps) {
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(
createSeriesTagsSelector(arrayValue)
createSeriesTagsSelector(value)
);
const handleTagCreated = useCallback(
(tag: SeriesTag) => {
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: SeriesTag) => {
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 SeriesTagInput({
);
}
},
[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 (

@ -1,5 +1,4 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, {
KeyboardEvent,
Ref,
@ -16,9 +15,7 @@ import {
SuggestionsFetchRequestedParams,
} from 'react-autosuggest';
import useDebouncedCallback from 'Helpers/Hooks/useDebouncedCallback';
import { kinds } from 'Helpers/Props';
import { Kind } from 'Helpers/Props/kinds';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import { InputChanged } from 'typings/inputs';
import AutoSuggestInput from '../AutoSuggestInput';
import TagInputInput from './TagInputInput';
@ -337,23 +334,4 @@ function TagInput<T extends TagBase>({
);
}
TagInput.propTypes = {
className: PropTypes.string,
inputContainerClassName: PropTypes.string,
tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
allowNew: PropTypes.bool,
kind: PropTypes.oneOf(kinds.all),
placeholder: PropTypes.string,
delimiters: PropTypes.arrayOf(PropTypes.string),
minQueryLength: PropTypes.number,
canEdit: PropTypes.bool,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
tagComponent: PropTypes.elementType,
onTagAdd: PropTypes.func.isRequired,
onTagDelete: PropTypes.func.isRequired,
onTagReplace: PropTypes.func,
};
export default TagInput;

@ -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[];

@ -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;

@ -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;

@ -10,7 +10,7 @@ import React, {
import { FileInputChanged, InputChanged } from 'typings/inputs';
import styles from './TextInput.css';
interface CommonTextInputProps {
export interface CommonTextInputProps {
className?: string;
readOnly?: boolean;
autoFocus?: boolean;
@ -23,7 +23,7 @@ interface CommonTextInputProps {
step?: number;
min?: number;
max?: number;
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;
@ -102,7 +102,7 @@ function TextInput({
);
const handleFocus = useCallback(
(event: FocusEvent) => {
(event: FocusEvent<HTMLInputElement, Element>) => {
onFocus?.(event);
selectionChanged();

@ -61,10 +61,10 @@ function TableOptionsModal({
dropIndex > dragIndex;
const handlePageSizeChange = useCallback(
({ value }: InputChanged<number>) => {
({ value }: InputChanged<number | null>) => {
let error: string | null = null;
if (value < 5) {
if (value === null || value < 5) {
error = translate('TablePageSizeMinimum', {
minimumValue: '5',
});
@ -76,7 +76,7 @@ function TableOptionsModal({
onTableOptionChange({ pageSize: value });
}
setPageSize(value);
setPageSize(value ?? 0);
setPageSizeError(error);
},
[maxPageSize, onTableOptionChange]

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

@ -241,6 +241,7 @@ function EditSeriesModalContent({
<Icon name={icons.ROOT_FOLDER} />
</FormInputButton>,
]}
includeFiles={false}
onChange={handleInputChange}
/>
</FormGroup>

@ -137,7 +137,7 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
? translate('DeleteSeriesFoldersHelpText')
: translate('DeleteSeriesFolderHelpText')
}
kind={kinds.DANGER}
kind="danger"
onChange={onDeleteFilesChange}
/>
</FormGroup>

@ -9,6 +9,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props';
import MoveSeriesModal from 'Series/MoveSeries/MoveSeriesModal';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './EditSeriesModalContent.css';
@ -142,25 +143,25 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
);
const onInputChange = useCallback(
({ name, value }: { name: string; value: string }) => {
({ name, value }: InputChanged) => {
switch (name) {
case 'monitored':
setMonitored(value);
setMonitored(value as string);
break;
case 'monitorNewItems':
setMonitorNewItems(value);
setMonitorNewItems(value as string);
break;
case 'qualityProfileId':
setQualityProfileId(value);
setQualityProfileId(value as string);
break;
case 'seriesType':
setSeriesType(value);
setSeriesType(value as string);
break;
case 'seasonFolder':
setSeasonFolder(value);
setSeasonFolder(value as string);
break;
case 'rootFolderPath':
setRootFolderPath(value);
setRootFolderPath(value as string);
break;
default:
console.warn('EditSeriesModalContent Unknown Input');

@ -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;

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

@ -66,8 +66,6 @@ function UpdateSettings({
}
helpLink="https://wiki.servarr.com/sonarr/settings#updates"
{...branch}
// @ts-expect-error - FormInputGroup doesn't accept a values prop
// of string[] which is needed for AutoCompleteInput
values={branchValues}
readOnly={usingExternalUpdateMechanism}
onChange={onInputChange}

@ -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';
@ -102,9 +103,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]
);

@ -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';
@ -90,24 +91,21 @@ function ManageImportListsEditModalContent(
onModalClose,
]);
const onInputChange = useCallback(
({ name, value }: { name: string; value: string }) => {
switch (name) {
case 'enableAutomaticAdd':
setEnableAutomaticAdd(value);
break;
case 'qualityProfileId':
setQualityProfileId(value);
break;
case 'rootFolderPath':
setRootFolderPath(value);
break;
default:
console.warn(`EditImportListModalContent Unknown Input: '${name}'`);
}
},
[]
);
const onInputChange = useCallback(({ name, value }: InputChanged) => {
switch (name) {
case 'enableAutomaticAdd':
setEnableAutomaticAdd(value as string);
break;
case 'qualityProfileId':
setQualityProfileId(value as number);
break;
case 'rootFolderPath':
setRootFolderPath(value as string);
break;
default:
console.warn(`EditImportListModalContent Unknown Input: '${name}'`);
}
}, []);
const selectedCount = importListIds.length;

@ -107,6 +107,7 @@ function ImportListOptions({
{...listSyncLevel}
/>
</FormGroup>
{listSyncLevel.value === 'keepAndTag' ? (
<FormGroup
advancedSettings={showAdvancedSettings}
@ -115,7 +116,7 @@ function ImportListOptions({
<FormLabel>{translate('ListSyncTag')}</FormLabel>
<FormInputGroup
{...listSyncTag}
type={inputTypes.TAG}
type={inputTypes.SERIES_TAG}
name="listSyncTag"
value={listSyncTag.value === 0 ? [] : [listSyncTag.value]}
helpText={translate('ListSyncTagHelpText')}

@ -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;

@ -464,6 +464,7 @@ function MediaManagement() {
type={inputTypes.PATH}
name="recycleBin"
helpText={translate('RecyclingBinHelpText')}
includeFiles={false}
onChange={handleInputChange}
{...settings.recycleBin}
/>
@ -537,7 +538,6 @@ function MediaManagement() {
name="chownGroup"
helpText={translate('ChownGroupHelpText')}
helpTextWarning={translate('ChownGroupHelpTextWarning')}
values={fileDateOptions}
onChange={handleInputChange}
{...settings.chownGroup}
/>

@ -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';
@ -88,9 +89,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);

@ -21,11 +21,15 @@ import {
setDelayProfileValue,
} from 'Store/Actions/settingsActions';
import selectSettings from 'Store/Selectors/selectSettings';
import DelayProfile from 'typings/DelayProfile';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './EditDelayProfileModalContent.css';
const newDelayProfile: Record<string, boolean | number | number[] | string> = {
const newDelayProfile: DelayProfile & { [key: string]: unknown } = {
id: 0,
name: '',
order: 0,
enableUsenet: true,
enableTorrent: true,
preferredProtocol: 'usenet',
@ -72,7 +76,11 @@ function createDelayProfileSelector(id: number | undefined) {
delayProfiles;
const profile = id ? items.find((i) => i.id === id) : newDelayProfile;
const settings = selectSettings(profile!, pendingChanges, saveError);
const settings = selectSettings<DelayProfile>(
profile!,
pendingChanges,
saveError
);
return {
isFetching,

@ -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';
@ -101,9 +102,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]
);
@ -124,7 +125,6 @@ function EditReleaseProfileModalContent({
name="name"
{...name}
placeholder={translate('OptionalName')}
canEdit={true}
onChange={handleInputChange}
/>
</FormGroup>

Loading…
Cancel
Save