diff --git a/web/html/src/components/input/InputBase.tsx b/web/html/src/components/input/InputBase.tsx index 488ada21648..8bcbf48512a 100644 --- a/web/html/src/components/input/InputBase.tsx +++ b/web/html/src/components/input/InputBase.tsx @@ -64,7 +64,7 @@ export type InputBaseProps = { disabled?: boolean; /** - * Validate the input, either sync or async, resolve with `undefined` for valid or an error message for invalid + * Validate the input, either sync or async, return `undefined` when valid, a string or string array for an error message when invalid */ validate?: Validator | Validator[]; @@ -74,11 +74,12 @@ export type InputBaseProps = { */ debounceValidate?: number; - // TODO: Refactor this out /** - * @deprecated * - * Hint to display on a validation error + * Prefer returning an error message from your validator instead + * + * Fallback validation error + * */ invalidHint?: React.ReactNode; @@ -333,7 +334,13 @@ export class InputBase extends React.Component this.pushHint(hints, error)); if (this.state.isTouched) { - this.state.validationErrors.forEach((error) => this.pushHint(hints, error)); + if (this.state.validationErrors.size) { + if (this.props.invalidHint) { + this.pushHint(hints, this.props.invalidHint); + } else { + this.state.validationErrors.forEach((error) => this.pushHint(hints, error)); + } + } if (this.props.required && !this.props.disabled) { this.pushHint(hints, this.requiredHint()); diff --git a/web/html/src/components/input/validation/validation.ts b/web/html/src/components/input/validation/validation.ts index e293ba49e23..b955ebacdfe 100644 --- a/web/html/src/components/input/validation/validation.ts +++ b/web/html/src/components/input/validation/validation.ts @@ -73,7 +73,7 @@ const min = const parsed = parseFloat(value); if (isNaN(parsed) || parsed < minValue) { - return message ?? t(`Must be larger than ${minValue}`); + return message ?? t(`Must be no smaller than ${minValue}`); } }; @@ -87,7 +87,35 @@ const max = const parsed = parseFloat(value); if (isNaN(parsed) || parsed > maxValue) { - return message ?? t(`Must be smaller than ${maxValue}`); + return message ?? t(`Must be no larger than ${maxValue}`); + } + }; + +/** Value is greater than `gtValue` */ +const gt = + (gtValue: number, message?: string): Validator => + (value: string) => { + if (value === "") { + return; + } + + const parsed = parseFloat(value); + if (isNaN(parsed) || parsed <= gtValue) { + return message ?? t(`Must be greater than ${gtValue}`); + } + }; + +/** Value is smaller than `ltValue` */ +const lt = + (ltValue: number, message?: string): Validator => + (value: string) => { + if (value === "") { + return; + } + + const parsed = parseFloat(value); + if (isNaN(parsed) || parsed >= ltValue) { + return message ?? t(`Must be greater than ${ltValue}`); } }; @@ -111,6 +139,8 @@ export const Validation = { maxLength, min, max, + gt, + lt, isInt, intRange, floatRange, diff --git a/web/html/src/components/validation.ts b/web/html/src/components/validation.ts deleted file mode 100644 index 18aa1dcd571..00000000000 --- a/web/html/src/components/validation.ts +++ /dev/null @@ -1,20 +0,0 @@ -// TODO: Deprecate this whole file - -// TODO: Deprecate this dependency -// https://github.com/chriso/validator.js -import validator from "validator"; - -const f = - (fn) => - (...args) => - (str) => - fn(str, ...args); -const validations: Record = {}; - -Object.keys(validator).forEach((v) => { - if (typeof validator[v] === "function") { - validations[v] = f(validator[v]); - } -}); - -export default validations; diff --git a/web/html/src/manager/proxy/container-config.tsx b/web/html/src/manager/proxy/container-config.tsx index ff47a17fc6c..992ad08da58 100644 --- a/web/html/src/manager/proxy/container-config.tsx +++ b/web/html/src/manager/proxy/container-config.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { AsyncButton } from "components/buttons"; import { SubmitButton } from "components/buttons"; +import { Validation } from "components/input"; import { Form } from "components/input/form/Form"; import { FormMultiInput } from "components/input/form-multi-input/FormMultiInput"; import { unflattenModel } from "components/input/form-utils"; @@ -10,7 +11,6 @@ import { Radio } from "components/input/radio/Radio"; import { Text } from "components/input/text/Text"; import { Panel } from "components/panels/Panel"; import { TopPanel } from "components/panels/TopPanel"; -import Validation from "components/validation"; import Network from "utils/network"; @@ -220,7 +220,7 @@ export function ProxyConfig() { name="proxyPort" label={t("Proxy SSH port")} hint={t("Port range: 1 - 65535")} - validate={[Validation.isInt({ gt: 0, lt: 65536 })]} + validate={[Validation.intRange(0, 65536)]} defaultValue="8022" labelClass="col-md-3" divClass="col-md-6" @@ -231,7 +231,7 @@ export function ProxyConfig() { label={t("Max Squid cache size")} hint={t("The maximum value of the Squid cache in Megabytes")} required - validate={[Validation.isInt({ gt: 0 })]} + validate={[Validation.isInt(), Validation.gt(0)]} placeholder={t("e.g., 2048")} labelClass="col-md-3" divClass="col-md-6" diff --git a/web/html/src/manager/virtualization/guests/GuestProperties.tsx b/web/html/src/manager/virtualization/guests/GuestProperties.tsx index c7ec4a69dc9..e63c8307ed8 100644 --- a/web/html/src/manager/virtualization/guests/GuestProperties.tsx +++ b/web/html/src/manager/virtualization/guests/GuestProperties.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { ActionChain } from "components/action-schedule"; import { Select } from "components/input"; +import { Validation } from "components/input"; import { Check } from "components/input/check/Check"; import { Text } from "components/input/text/Text"; import { MessageType } from "components/messages/messages"; @@ -9,7 +10,6 @@ import { Messages } from "components/messages/messages"; import { Utils as MessagesUtils } from "components/messages/messages"; import { Panel } from "components/panels/Panel"; import { Loading } from "components/utils/loading/Loading"; -import Validation from "components/validation"; import { VirtualizationPoolCapsApi } from "../pools/virtualization-pools-capabilities-api"; import { VirtualizationListRefreshApi } from "../virtualization-list-refresh-api"; @@ -166,7 +166,7 @@ export function GuestProperties(props: Props) { invalidHint={t("A positive integer is required")} labelClass="col-md-3" divClass="col-md-6" - validate={[Validation.isInt({ gt: 0 })]} + validate={[Validation.isInt(), Validation.gt(0)]} /> {initialModel.arch === undefined && ( )} {adapter_fields.includes("parent_address_uid") && ( @@ -413,7 +411,7 @@ export function PoolProperties(props: Props) { labelClass="col-md-3" divClass="col-md-6" invalidHint={t("PCI address formatted like 0000:00:00.0")} - validate={[Validation.isInt]} + validate={[Validation.isInt()]} /> )} {adapter_fields.includes("parent") && ( @@ -538,7 +536,7 @@ export function PoolProperties(props: Props) { label={t("Owner UID")} labelClass="col-md-3" divClass="col-md-6" - validate={[Validation.isInt({ min: 0 })]} + validate={[Validation.isInt(), Validation.min(0)]} invalidHint={t("UID is a numeric value")} /> {/* TODO Add Link to the UI Reference */}