mirror of
https://github.com/Sonarr/Sonarr
synced 2025-12-06 16:32:24 +01:00
359 lines
11 KiB
TypeScript
359 lines
11 KiB
TypeScript
import React, { ElementType, ReactNode, useMemo, useState } from 'react';
|
|
import Link from 'Components/Link/Link';
|
|
import { inputTypes } from 'Helpers/Props';
|
|
import { InputType } from 'Helpers/Props/inputTypes';
|
|
import { ValidationError, ValidationWarning } from 'typings/pending';
|
|
import translate from 'Utilities/String/translate';
|
|
import AutoCompleteInput, { AutoCompleteInputProps } from './AutoCompleteInput';
|
|
import CaptchaInput, { CaptchaInputProps } from './CaptchaInput';
|
|
import CheckInput, { CheckInputProps } from './CheckInput';
|
|
import FloatInput, { FloatInputProps } from './FloatInput';
|
|
import { FormInputButtonProps } from './FormInputButton';
|
|
import { FormInputGroupProvider } from './FormInputGroupContext';
|
|
import FormInputHelpText from './FormInputHelpText';
|
|
import KeyValueListInput, { KeyValueListInputProps } from './KeyValueListInput';
|
|
import NumberInput, { NumberInputProps } from './NumberInput';
|
|
import OAuthInput, { OAuthInputProps } from './OAuthInput';
|
|
import PasswordInput from './PasswordInput';
|
|
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';
|
|
|
|
const componentMap: Record<InputType, ElementType> = {
|
|
autoComplete: AutoCompleteInput,
|
|
captcha: CaptchaInput,
|
|
check: CheckInput,
|
|
date: TextInput,
|
|
device: DeviceInput,
|
|
downloadClientSelect: DownloadClientSelectInput,
|
|
dynamicSelect: ProviderDataSelectInput,
|
|
file: TextInput,
|
|
float: FloatInput,
|
|
indexerFlagsSelect: IndexerFlagsSelectInput,
|
|
indexerSelect: IndexerSelectInput,
|
|
keyValueList: KeyValueListInput,
|
|
languageSelect: LanguageSelectInput,
|
|
monitorEpisodesSelect: MonitorEpisodesSelectInput,
|
|
monitorNewItemsSelect: MonitorNewItemsSelectInput,
|
|
number: NumberInput,
|
|
oauth: OAuthInput,
|
|
password: PasswordInput,
|
|
path: PathInput,
|
|
qualityProfileSelect: QualityProfileSelectInput,
|
|
rootFolderSelect: RootFolderSelectInput,
|
|
select: EnhancedSelectInput,
|
|
seriesTag: SeriesTagInput,
|
|
seriesTypeSelect: SeriesTypeSelectInput,
|
|
tag: SeriesTagInput,
|
|
tagSelect: TagSelectInput,
|
|
text: TextInput,
|
|
textArea: TextArea,
|
|
textTag: TextTagInput,
|
|
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'
|
|
? FloatInputProps
|
|
: 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<V>
|
|
: C extends 'seriesTypeSelect'
|
|
? SeriesTypeSelectInputProps
|
|
: C extends 'tag'
|
|
? SeriesTagInputProps<V>
|
|
: 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;
|
|
value: string;
|
|
hint?: string;
|
|
}
|
|
|
|
// TODO: Remove once all parent components are updated to TSX and we can refactor to a consistent type
|
|
export interface ValidationMessage {
|
|
message: string;
|
|
}
|
|
|
|
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;
|
|
buttons?: ReactNode | ReactNode[];
|
|
helpText?: string;
|
|
helpTexts?: string[];
|
|
helpTextWarning?: string;
|
|
helpLink?: string;
|
|
pending?: boolean;
|
|
placeholder?: string;
|
|
unit?: string;
|
|
errors?: (ValidationMessage | ValidationError)[];
|
|
warnings?: (ValidationMessage | ValidationWarning)[];
|
|
};
|
|
|
|
function FormInputGroup<T, C extends InputType>(
|
|
props: FormInputGroupProps<T, C>
|
|
) {
|
|
const {
|
|
className = styles.inputGroup,
|
|
containerClassName = styles.inputGroupContainer,
|
|
inputClassName,
|
|
type,
|
|
unit,
|
|
buttons = [],
|
|
helpText,
|
|
helpTexts = [],
|
|
helpTextWarning,
|
|
helpLink,
|
|
pending,
|
|
errors: serverErrors = [],
|
|
warnings: serverWarnings = [],
|
|
...otherProps
|
|
} = props;
|
|
|
|
const [clientErrors, setClientErrors] = useState<
|
|
(ValidationMessage | ValidationError)[]
|
|
>([]);
|
|
|
|
const [clientWarnings, setClientWarnings] = useState<
|
|
(ValidationMessage | ValidationWarning)[]
|
|
>([]);
|
|
|
|
const errors = useMemo(() => {
|
|
return [...clientErrors, ...serverErrors];
|
|
}, [clientErrors, serverErrors]);
|
|
|
|
const warnings = useMemo(() => {
|
|
return [...clientWarnings, ...serverWarnings];
|
|
}, [clientWarnings, serverWarnings]);
|
|
|
|
const InputComponent = componentMap[type];
|
|
const checkInput = type === inputTypes.CHECK;
|
|
const hasError = !!errors.length;
|
|
const hasWarning = !hasError && !!warnings.length;
|
|
const buttonsArray = React.Children.toArray(buttons);
|
|
const lastButtonIndex = buttonsArray.length - 1;
|
|
const hasButton = !!buttonsArray.length;
|
|
|
|
return (
|
|
<FormInputGroupProvider
|
|
setClientErrors={setClientErrors}
|
|
setClientWarnings={setClientWarnings}
|
|
>
|
|
<div className={containerClassName}>
|
|
<div className={className}>
|
|
<div className={styles.inputContainer}>
|
|
{/* @ts-expect-error - types are validated already */}
|
|
<InputComponent
|
|
className={inputClassName}
|
|
helpText={helpText}
|
|
helpTextWarning={helpTextWarning}
|
|
hasError={hasError}
|
|
hasWarning={hasWarning}
|
|
hasButton={hasButton}
|
|
{...otherProps}
|
|
/>
|
|
|
|
{unit && (
|
|
<div
|
|
className={
|
|
type === inputTypes.NUMBER
|
|
? styles.inputUnitNumber
|
|
: styles.inputUnit
|
|
}
|
|
>
|
|
{unit}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{buttonsArray.map((button, index) => {
|
|
if (!React.isValidElement<FormInputButtonProps>(button)) {
|
|
return button;
|
|
}
|
|
|
|
return React.cloneElement(button, {
|
|
isLastButton: index === lastButtonIndex,
|
|
});
|
|
})}
|
|
|
|
{/* <div className={styles.pendingChangesContainer}>
|
|
{
|
|
pending &&
|
|
<Icon
|
|
name={icons.UNSAVED_SETTING}
|
|
className={styles.pendingChangesIcon}
|
|
title="Change has not been saved yet"
|
|
/>
|
|
}
|
|
</div> */}
|
|
</div>
|
|
|
|
{!checkInput && helpText ? <FormInputHelpText text={helpText} /> : null}
|
|
|
|
{!checkInput && helpTexts ? (
|
|
<div>
|
|
{helpTexts.map((text, index) => {
|
|
return (
|
|
<FormInputHelpText
|
|
key={index}
|
|
text={text}
|
|
isCheckInput={checkInput}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
) : null}
|
|
|
|
{(!checkInput || helpText) && helpTextWarning ? (
|
|
<FormInputHelpText text={helpTextWarning} isWarning={true} />
|
|
) : null}
|
|
|
|
{helpLink ? <Link to={helpLink}>{translate('MoreInfo')}</Link> : null}
|
|
|
|
{errors.map((error, index) => {
|
|
return 'errorMessage' in error ? (
|
|
<FormInputHelpText
|
|
key={index}
|
|
text={error.errorMessage}
|
|
link={error.infoLink}
|
|
tooltip={error.detailedDescription}
|
|
isError={true}
|
|
isCheckInput={checkInput}
|
|
/>
|
|
) : (
|
|
<FormInputHelpText
|
|
key={index}
|
|
text={error.message}
|
|
isError={true}
|
|
isCheckInput={checkInput}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{warnings.map((warning, index) => {
|
|
return 'errorMessage' in warning ? (
|
|
<FormInputHelpText
|
|
key={index}
|
|
text={warning.errorMessage}
|
|
link={warning.infoLink}
|
|
tooltip={warning.detailedDescription}
|
|
isWarning={true}
|
|
isCheckInput={checkInput}
|
|
/>
|
|
) : (
|
|
<FormInputHelpText
|
|
key={index}
|
|
text={warning.message}
|
|
isWarning={true}
|
|
isCheckInput={checkInput}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
</FormInputGroupProvider>
|
|
);
|
|
}
|
|
|
|
export default FormInputGroup;
|