diff --git a/src/renderer/components/input/__tests__/input_validators.test.ts b/src/renderer/components/input/__tests__/input_validators.test.ts index cb6a0b3db9..63dda431d0 100644 --- a/src/renderer/components/input/__tests__/input_validators.test.ts +++ b/src/renderer/components/input/__tests__/input_validators.test.ts @@ -3,6 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ +import type { InputValidator } from "../input_validators"; import { isEmail, isUrl, systemName, unionInputValidators, unionInputValidatorsAsync } from "../input_validators"; type TextValidationCase = [string, boolean]; @@ -146,4 +147,12 @@ describe("input validation tests", () => { expect(systemName.validate(input)).toBe(output); }); }); + + it("should allow InputValidator to be used without any type params", () => { + const v: InputValidator = { + validate: (input: string) => input.length > 10, + }; + + expect(v.validate("hello")).toBe(false); + }); }); diff --git a/src/renderer/components/input/input.tsx b/src/renderer/components/input/input.tsx index f69d297432..3305c48e7b 100644 --- a/src/renderer/components/input/input.tsx +++ b/src/renderer/components/input/input.tsx @@ -178,7 +178,11 @@ export class Input extends React.Component { break; } - if (isAsyncValidator(validator)) { + const result = validator.validate(value, this.props); + + if (typeof result === "boolean" && !result) { + errors.push(this.getValidatorError(value, validator)); + } else if (result instanceof Promise) { if (!validationId) { this.validationId = validationId = uniqueId("validation_id_"); } @@ -189,8 +193,6 @@ export class Input extends React.Component { return this.getValidatorError(value, validator) || (error instanceof Error ? error.message : String(error)); } })()); - } else if (!validator.validate(value, this.props)) { - errors.push(this.getValidatorError(value, validator)); } } diff --git a/src/renderer/components/input/input_validators.ts b/src/renderer/components/input/input_validators.ts index 59e12a977a..065b60db3f 100644 --- a/src/renderer/components/input/input_validators.ts +++ b/src/renderer/components/input/input_validators.ts @@ -18,38 +18,58 @@ export type InputValidation = (value: string, props?: I export type SyncValidationMessage = React.ReactNode | ((value: string, props?: InputProps) => React.ReactNode); -export type InputValidator = { +/** + * @deprecated This type is not as type safe as it is possible to specify an async input validator without specifying a `debounce` time. + * + * Use {@link asyncInputValidator} or {@link inputValidator} instead to create validators + */ +export interface LegacyInputValidator { /** * Filters itself based on the input props */ condition?: (props: InputProps) => any; -} & ( - IsAsync extends true - ? { - /** - * The validation message maybe either specified from the `message` field (higher priority) - * or if that is not provided then the message will retrived from the rejected with value - */ - validate: InputValidation; - message?: SyncValidationMessage; - debounce: number; - } - : { - validate: InputValidation; - message: SyncValidationMessage; - debounce?: undefined; - } - ); + validate: InputValidation; + message?: SyncValidationMessage; + debounce?: number; +} -export function isAsyncValidator(validator: InputValidator): validator is InputValidator { +export interface AsyncInputValidator { + /** + * Filters itself based on the input props + */ + condition?: (props: InputProps) => any; + validate: InputValidation; + message?: SyncValidationMessage; + debounce: number; +} + +export interface SyncInputValidator { + /** + * Filters itself based on the input props + */ + condition?: (props: InputProps) => any; + validate: InputValidation; + message: SyncValidationMessage; + debounce?: undefined; +} + +export type InputValidator = SyncInputValidator | AsyncInputValidator | (IsAsync extends boolean ? LegacyInputValidator : never); + +export function isAsyncValidator(validator: InputValidator): validator is AsyncInputValidator { return typeof validator.debounce === "number"; } -export function asyncInputValidator(validator: InputValidator): InputValidator { +/** + * A helper function to create an {@link AsyncInputValidator} + */ +export function asyncInputValidator(validator: AsyncInputValidator): AsyncInputValidator { return validator; } -export function inputValidator(validator: InputValidator): InputValidator { +/** + * A helper function to create an {@link SyncInputValidator} + */ +export function inputValidator(validator: SyncInputValidator): SyncInputValidator { return validator; } @@ -58,9 +78,9 @@ export function inputValidator(validator: InputValidator): InputValidator * one of the input validators matches the input */ export function unionInputValidators( - baseValidator: Pick, "condition" | "message">, - ...validators: InputValidator[] -): InputValidator { + baseValidator: Pick, + ...validators: SyncInputValidator[] +): SyncInputValidator { return inputValidator({ ...baseValidator, validate: (value, props) => validators.some(validator => validator.validate(value, props)), @@ -72,13 +92,13 @@ export function unionInputValidators( * valid if one of the input validators matches the input */ export function unionInputValidatorsAsync( - baseValidator: SetRequired, "condition" | "message">, "message">, - ...validators: InputValidator[] -): InputValidator { + baseValidator: SetRequired, "message">, + ...validators: InputValidator[] +): AsyncInputValidator { const longestDebounce = Math.max( ...validators .filter(isAsyncValidator) - .map(validator => validator.debounce), + .map(validator => validator.debounce ?? 0), 0, );