diff --git a/ivette/src/dome/src/renderer/layout/form.tsx b/ivette/src/dome/src/renderer/layout/form.tsx index 165b1e54e96b2e21b694402cda8008c0be92bbd0..3d814e2ce1c8a62ee333b8721f164620b59a41d6 100644 --- a/ivette/src/dome/src/renderer/layout/form.tsx +++ b/ivette/src/dome/src/renderer/layout/form.tsx @@ -9,11 +9,12 @@ import { debounce } from 'lodash'; import React from 'react'; -import * as Utils from 'dome/utils'; +import { SVG } from 'dome/controls/icons'; +import * as Utils from 'dome/misc/utils'; export type Error = undefined | string; export type Setter<A> = (value: A) => void; -export type Checker<A> = (value: A) => true | Error; +export type Checker<A> = (value: A) => boolean | Error; export type State<A> = [A, Setter<A>] export type Callback<A> = (value: A, valid: boolean) => void; @@ -24,15 +25,23 @@ export type Callback<A> = (value: A, valid: boolean) => void; export function inRange( a: number, b: number, - msg?: string, ): Checker<number> { - return (v: number) => (a <= v && v <= b) || msg || 'Invalid Range'; + return (v: number) => (a <= v && v <= b); } -export function validate<A>(value: A, checker?: Checker<A>): Error { +export function validate<A>( + value: A, + checker: undefined | Checker<A>, + message: undefined | string, +): Error { if (checker) { - const r = checker(value); - return r === true ? undefined : r; + try { + const r = checker(value); + if (r === undefined || r === true) return undefined; + return message || r || 'Invalid Field'; + } catch (err) { + return err.toString(); + } } return undefined; } @@ -44,7 +53,7 @@ export function useCallback<A>( error: Error, onChange?: Callback<A>, ) { - React.useMemo( + React.useEffect( () => { if (onChange) onChange(value, isValid(error)); }, [value, error, onChange], ); @@ -130,6 +139,7 @@ export function Filter(props: FilterProps & Children) { export interface FieldProps<A> { state: State<A>; checker?: Checker<A>; + message?: string; onChange?: Callback<A>; latency?: number; } @@ -137,21 +147,21 @@ export interface FieldProps<A> { type FilterState<A> = [A, Setter<A>, Error]; export function useField<A>(props: FieldProps<A>): FilterState<A> { - const { checker, latency = 0, onChange } = props; + const { checker, message, latency = 0, onChange } = props; const [value, setValue] = props.state; const [current, setCurrent] = React.useState<A>(value); const [error, setError] = React.useState<Error>(undefined); const update = React.useMemo(() => { if (!latency) return (newValue: A) => { - const newError = validate(newValue, checker); + const newError = validate(newValue, checker, message); setCurrent(newValue); setValue(newValue); setError(newError); if (onChange) onChange(newValue, isValid(newError)); }; const propagate = debounce((newValue) => { - const newError = validate(newValue); + const newError = validate(newValue, checker, message); setValue(newValue); setError(newError); if (onChange) onChange(newValue, isValid(newError)); @@ -160,7 +170,7 @@ export function useField<A>(props: FieldProps<A>): FilterState<A> { setCurrent(newValue); propagate(newValue); } - }, [checker, latency, onChange, setValue, setError]); + }, [checker, message, latency, onChange, setValue, setError]); return [current, update, error]; }; @@ -188,4 +198,35 @@ export const Form = (props: FormProps) => { ); } +export interface WarningProps { + /** Short warn message in case of error. */ + warn?: string; + /** Error description (in tooltip if warn, on hover otherwized). */ + error?: Error; + /** Label offset. */ + offset?: number; +} + +/** Warning badge */ +export function Warning(props: WarningProps) { + const { error, warn, offset = 0 } = props; + if (!error) return null; + const hovered = warn ? warn : error; + const tooltip = warn ? error : undefined; + const style = warn ? { width: 'max-content' } : undefined; + + return ( + <div + className="dome-xIcon dome-xForm-error" + style={{ top: offset - 2 }} > + <SVG id="WARNING" size={11} title={tooltip} /> + <span + className="dome-xForm-warning" + style={style}> + {hovered} + </span> + </div> + ); +}; + // --------------------------------------------------------------------------