commit bff90a241375a33c4450d10ee78de75a6a4dda82 parent ec29b9c3cc57e4b91bcd442bf77baaf3a18d8c16 Author: Florian Dold <florian@dold.me> Date: Sat, 22 Mar 2025 15:31:00 +0100 forms: cleanup, simplify types Diffstat:
22 files changed, 122 insertions(+), 97 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -383,7 +383,7 @@ function JumpByIdForm({ </a> </div> <div class="mt-2 cursor-default"> - <InputToggle<any, string> + <InputToggle threeState name="inv" label={i18n.str`Only investigated`} diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx @@ -16,12 +16,10 @@ import { Button, FormDesign, - FormErrors, InputLine, InternationalizationAPI, LocalNotificationBanner, UIHandlerId, - undefinedIfEmpty, useForm, useLocalNotificationHandler, useTranslationContext, @@ -100,7 +98,7 @@ export function UnlockAccount(): VNode { <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] "> <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12"> <div class="mb-4"> - <InputLine<FormType, "password"> + <InputLine label={i18n.str`Password`} name="password" type="password" diff --git a/packages/aml-backoffice-ui/src/pages/decision/Information.tsx b/packages/aml-backoffice-ui/src/pages/decision/Information.tsx @@ -135,7 +135,7 @@ function FillCustomerData({ <i18n.Translate>change form</i18n.Translate> </a> </div> - <InputAbsoluteTime<any, any> + <InputAbsoluteTime label={i18n.str`Expiration`} help={i18n.str`Expiration date of the information filled in this form.`} name="expiration" diff --git a/packages/web-util/src/forms/FormProvider.tsx b/packages/web-util/src/forms/FormProvider.tsx @@ -4,11 +4,11 @@ import { ComponentChildren, VNode } from "preact"; /** * Properties for form elements set at design type. */ -export interface UIFormProps<T extends object, K extends keyof T> { +export interface UIFormProps<ValType> { /** * Form file name. */ - name: K; + name: string; /** * instruction to be shown in the field @@ -56,7 +56,7 @@ export interface UIFormProps<T extends object, K extends keyof T> { /** * converter to string and back */ - converter?: StringConverter<T[K]>; + converter?: StringConverter<ValType>; handler?: UIFieldHandler; } diff --git a/packages/web-util/src/forms/field-types.ts b/packages/web-util/src/forms/field-types.ts @@ -1,25 +1,26 @@ import { VNode } from "preact"; import { Caption } from "./Caption.js"; import { DownloadLink } from "./DownloadLink.js"; +import { ExternalLink } from "./ExternalLink.js"; import { InputAbsoluteTime } from "./fields/InputAbsoluteTime.js"; import { InputAmount } from "./fields/InputAmount.js"; import { InputArray } from "./fields/InputArray.js"; import { InputChoiceHorizontal } from "./fields/InputChoiceHorizontal.js"; import { InputChoiceStacked } from "./fields/InputChoiceStacked.js"; +import { InputDuration } from "./fields/InputDuration.js"; +import { InputDurationText } from "./fields/InputDurationText.js"; import { InputFile } from "./fields/InputFile.js"; import { InputInteger } from "./fields/InputInteger.js"; -import { InputSelectMultiple } from "./fields/InputSelectMultiple.js"; -import { InputDuration } from "./fields/InputDuration.js"; +import { InputIsoDate } from "./fields/InputIsoDate.js"; import { InputSecret } from "./fields/InputSecret.js"; +import { InputSelectMultiple } from "./fields/InputSelectMultiple.js"; import { InputSelectOne } from "./fields/InputSelectOne.js"; import { InputText } from "./fields/InputText.js"; import { InputTextArea } from "./fields/InputTextArea.js"; import { InputToggle } from "./fields/InputToggle.js"; import { Group } from "./Group.js"; import { HtmlIframe } from "./HtmlIframe.js"; -import { InputDurationText } from "./fields/InputDurationText.js"; -import { ExternalLink } from "./ExternalLink.js"; -import { InputIsoDate } from "./fields/InputIsoDate.js"; + /** * Constrain the type with the ui props */ @@ -29,22 +30,22 @@ type FieldType<T extends object = any, K extends keyof T = any> = { "download-link": Parameters<typeof DownloadLink>[0]; "external-link": Parameters<typeof ExternalLink>[0]; htmlIframe: Parameters<typeof HtmlIframe>[0]; - array: Parameters<typeof InputArray<T, K>>[0]; - file: Parameters<typeof InputFile<T, K>>[0]; - selectOne: Parameters<typeof InputSelectOne<T, K>>[0]; - selectMultiple: Parameters<typeof InputSelectMultiple<T, K>>[0]; - text: Parameters<typeof InputText<T, K>>[0]; - textArea: Parameters<typeof InputTextArea<T, K>>[0]; - choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0]; - choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0]; - absoluteTimeText: Parameters<typeof InputAbsoluteTime<T, K>>[0]; - isoDateText: Parameters<typeof InputIsoDate<T, K>>[0]; - integer: Parameters<typeof InputInteger<T, K>>[0]; - secret: Parameters<typeof InputSecret<T, K>>[0]; - toggle: Parameters<typeof InputToggle<T, K>>[0]; - amount: Parameters<typeof InputAmount<T, K>>[0]; - duration: Parameters<typeof InputDuration<T, K>>[0]; - durationText: Parameters<typeof InputDurationText<T, K>>[0]; + array: Parameters<typeof InputArray>[0]; + file: Parameters<typeof InputFile>[0]; + selectOne: Parameters<typeof InputSelectOne<T[K]>>[0]; + selectMultiple: Parameters<typeof InputSelectMultiple>[0]; + text: Parameters<typeof InputText>[0]; + textArea: Parameters<typeof InputTextArea>[0]; + choiceStacked: Parameters<typeof InputChoiceStacked<T[K]>>[0]; + choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T[K]>>[0]; + absoluteTimeText: Parameters<typeof InputAbsoluteTime>[0]; + isoDateText: Parameters<typeof InputIsoDate>[0]; + integer: Parameters<typeof InputInteger>[0]; + secret: Parameters<typeof InputSecret>[0]; + toggle: Parameters<typeof InputToggle>[0]; + amount: Parameters<typeof InputAmount>[0]; + duration: Parameters<typeof InputDuration>[0]; + durationText: Parameters<typeof InputDurationText>[0]; }; /** diff --git a/packages/web-util/src/forms/fields/InputAbsoluteTime.tsx b/packages/web-util/src/forms/fields/InputAbsoluteTime.tsx @@ -5,11 +5,11 @@ import { useState } from "preact/hooks"; import { Calendar } from "../Calendar.js"; import { Dialog } from "../Dialog.js"; import { UIFormProps } from "../FormProvider.js"; -import { InputLine } from "./InputLine.js"; import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; +import { InputLine } from "./InputLine.js"; -export function InputAbsoluteTime<T extends object, K extends keyof T>( - properties: { pattern?: string } & UIFormProps<T, K>, +export function InputAbsoluteTime( + properties: { pattern?: string } & UIFormProps<AbsoluteTime>, ): VNode { const pattern = properties.pattern ?? "dd/MM/yyyy"; const [open, setOpen] = useState(false); @@ -18,7 +18,7 @@ export function InputAbsoluteTime<T extends object, K extends keyof T>( properties.handler ?? noHandlerPropsAndNoContextForField(properties.name); return ( <Fragment> - <InputLine<T, K> + <InputLine type="text" focus={properties.focus} after={{ diff --git a/packages/web-util/src/forms/fields/InputAmount.tsx b/packages/web-util/src/forms/fields/InputAmount.tsx @@ -3,11 +3,11 @@ import { VNode, h } from "preact"; import { UIFormProps } from "../FormProvider.js"; import { InputLine } from "./InputLine.js"; -export function InputAmount<T extends object, K extends keyof T>( - props: { currency: string } & UIFormProps<T, K>, +export function InputAmount( + props: { currency: string } & UIFormProps<AmountJson>, ): VNode { return ( - <InputLine<T, K> + <InputLine {...props} type="text" before={{ diff --git a/packages/web-util/src/forms/fields/InputArray.tsx b/packages/web-util/src/forms/fields/InputArray.tsx @@ -100,11 +100,11 @@ function ArrayForm({ ); } -export function InputArray<T extends object, K extends keyof T>( +export function InputArray( props: { fields: UIFormElementConfig[]; labelField: string; - } & UIFormProps<T, K>, + } & UIFormProps<string[]>, ): VNode { const { fields, labelField, label, required, tooltip, hidden, help } = props; const { i18n } = useTranslationContext(); diff --git a/packages/web-util/src/forms/fields/InputChoiceHorizontal.tsx b/packages/web-util/src/forms/fields/InputChoiceHorizontal.tsx @@ -1,18 +1,18 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { Fragment, VNode, h } from "preact"; import { UIFormProps } from "../FormProvider.js"; -import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; +import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; export interface ChoiceH<V> { label: TranslatedString; value: V; } -export function InputChoiceHorizontal<T extends object, K extends keyof T>( +export function InputChoiceHorizontal<ChoiceVal>( props: { choices: ChoiceH<string>[]; - } & UIFormProps<T, K>, + } & UIFormProps<ChoiceVal>, ): VNode { const { hidden, choices, label, tooltip, help, required, converter } = props; const { value, onChange } = diff --git a/packages/web-util/src/forms/fields/InputChoiceStacked.tsx b/packages/web-util/src/forms/fields/InputChoiceStacked.tsx @@ -1,19 +1,25 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { Fragment, VNode, h } from "preact"; import { UIFormProps } from "../FormProvider.js"; -import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; +import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; +/** + * Choice of a translated string, with attached description + * of the choice. + * + * The value is usually a string or numeric constant. + */ export interface ChoiceS<V> { label: TranslatedString; description?: TranslatedString; value: V; } -export function InputChoiceStacked<T extends object, K extends keyof T>( +export function InputChoiceStacked<Choices>( props: { - choices: ChoiceS<T[K]>[]; - } & UIFormProps<T, K>, + choices: ChoiceS<Choices>[]; + } & UIFormProps<Choices>, ): VNode { const { choices, name, label, tooltip, help, hidden, required, converter } = props; diff --git a/packages/web-util/src/forms/fields/InputDuration.tsx b/packages/web-util/src/forms/fields/InputDuration.tsx @@ -1,14 +1,12 @@ +import { Duration } from "@gnu-taler/taler-util"; import { Fragment, VNode, h } from "preact"; +import { useEffect, useRef } from "preact/hooks"; import { useTranslationContext } from "../../index.browser.js"; import { UIFormProps } from "../FormProvider.js"; import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; import { InputWrapper } from "./InputLine.js"; -import { Duration } from "@gnu-taler/taler-util"; -import { useEffect, useRef, useState } from "preact/hooks"; -export function InputDuration<T extends object, K extends keyof T>( - props: UIFormProps<T, K>, -): VNode { +export function InputDuration(props: UIFormProps<Duration>): VNode { const { name, placeholder, before, after, converter, disabled } = props; const { i18n } = useTranslationContext(); const { value, onChange, error } = @@ -130,8 +128,16 @@ export function InputDuration<T extends object, K extends keyof T>( " text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-indigo-600"; } return ( - <InputWrapper<T, K> + <InputWrapper {...props} + converter={{ + fromStringUI(v) { + return v ?? ""; + }, + toStringUI(v) { + return v ?? ""; + }, + }} help={props.help} disabled={disabled ?? false} error={showError ? error : undefined} @@ -308,6 +314,7 @@ export function InputDuration<T extends object, K extends keyof T>( function defaultToString(v: unknown) { return v === undefined ? "" : typeof v !== "object" ? String(v) : ""; } + function defaultFromString(v: string) { return v; } diff --git a/packages/web-util/src/forms/fields/InputDurationText.tsx b/packages/web-util/src/forms/fields/InputDurationText.tsx @@ -1,7 +1,7 @@ +import { Duration } from "@gnu-taler/taler-util"; import { VNode, h } from "preact"; -import { InputLine } from "./InputLine.js"; import { UIFormProps } from "../FormProvider.js"; -import { Duration } from "@gnu-taler/taler-util"; +import { InputLine } from "./InputLine.js"; const PATTERN = /^(?<value>[0-9]+)(?<unit>[smhDMY])$/; const UNIT_GROUP = "unit"; @@ -52,11 +52,8 @@ function parseDurationValue(str: string): DurationValue | undefined { return { value, unit }; } -export function InputDurationText<T extends object, K extends keyof T>( - props: UIFormProps<T, K>, -): VNode { +export function InputDurationText(props: UIFormProps<string>): VNode { return ( - // <div>s</div> <InputLine type="text" {...props} diff --git a/packages/web-util/src/forms/fields/InputFile.tsx b/packages/web-util/src/forms/fields/InputFile.tsx @@ -3,8 +3,9 @@ import { UIFormProps } from "../FormProvider.js"; import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; -export function InputFile<T extends object, K extends keyof T>( - props: { maxBites: number; accept?: string } & UIFormProps<T, K>, +export function InputFile( + // FIXME: Specify type properly, not "any" + props: { maxBites: number; accept?: string } & UIFormProps<any>, ): VNode { const { label, tooltip, required, help: propsHelp, maxBites, accept } = props; const { value, onChange } = diff --git a/packages/web-util/src/forms/fields/InputInteger.tsx b/packages/web-util/src/forms/fields/InputInteger.tsx @@ -2,8 +2,8 @@ import { VNode, h } from "preact"; import { InputLine } from "./InputLine.js"; import { UIFormProps } from "../FormProvider.js"; -export function InputInteger<T extends object, K extends keyof T>( - props: UIFormProps<T, K>, +export function InputInteger( + props: UIFormProps<number>, ): VNode { return ( <InputLine diff --git a/packages/web-util/src/forms/fields/InputIsoDate.tsx b/packages/web-util/src/forms/fields/InputIsoDate.tsx @@ -17,8 +17,14 @@ export interface InputIsoDateProps { pattern?: string; } -export function InputIsoDate<T extends object, K extends keyof T>( - properties: InputIsoDateProps & UIFormProps<T, K>, +/** + * Input field for an ISO date (yyyy-MM-dd). + * + * The user can enter the date in a format specified by a + * pattern. + */ +export function InputIsoDate( + properties: InputIsoDateProps & UIFormProps<string>, ): VNode { const pattern = properties.pattern ?? "dd/MM/yyyy"; const [open, setOpen] = useState(false); @@ -30,7 +36,7 @@ export function InputIsoDate<T extends object, K extends keyof T>( // const strTime = format(time, pattern); return ( <Fragment> - <InputLine<T, K> + <InputLine type="text" {...properties} after={{ @@ -57,26 +63,26 @@ export function InputIsoDate<T extends object, K extends keyof T>( ), }} converter={{ - //@ts-ignore - toStringUI(v) { + toStringUI(v: string | undefined) { if (!v || typeof v !== "string") { - return undefined; + return ""; } try { const d = parse(v, "yyyy-MM-dd", Date.now()); return format(d, pattern); } catch (e) { - return undefined; + return ""; } }, - //@ts-ignore - fromStringUI: (v): string | undefined => { - if (!v) return undefined; + fromStringUI: (v: string | undefined): string => { + if (!v) { + return ""; + } try { const t_ms = parse(v, pattern, Date.now()).getTime(); return format(t_ms, pattern); } catch (e) { - return undefined; + return ""; } }, }} diff --git a/packages/web-util/src/forms/fields/InputLine.tsx b/packages/web-util/src/forms/fields/InputLine.tsx @@ -1,6 +1,6 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { ComponentChildren, Fragment, VNode, h } from "preact"; -import { useEffect, useRef, useState } from "preact/hooks"; +import { useEffect, useRef } from "preact/hooks"; import { composeRef, saveRef } from "../../components/utils.js"; import { Addon, UIFormProps } from "../FormProvider.js"; import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; @@ -112,7 +112,10 @@ export function RenderAddon({ } } -export function InputWrapper<T extends object, K extends keyof T>({ +/** + * FIXME: Document what this is! + */ +export function InputWrapper({ children, label, tooltip, @@ -127,7 +130,7 @@ export function InputWrapper<T extends object, K extends keyof T>({ error?: string; disabled: boolean; children: ComponentChildren; -} & UIFormProps<T, K>): VNode { +} & UIFormProps<string>): VNode { return ( <div class="sm:col-span-6 "> <LabelWithTooltipMaybeRequired @@ -166,8 +169,8 @@ function defaultFromString(v: string) { type InputType = "text" | "text-area" | "password" | "email" | "number"; -export function InputLine<T extends object, K extends keyof T>( - props: { type: InputType } & UIFormProps<T, K>, +export function InputLine( + props: { type: InputType } & UIFormProps<string>, ): VNode { const { name, @@ -244,7 +247,7 @@ export function InputLine<T extends object, K extends keyof T>( if (type === "text-area") { return ( - <InputWrapper<T, K> + <InputWrapper {...props} help={props.help} disabled={disabled ?? false} @@ -271,7 +274,7 @@ export function InputLine<T extends object, K extends keyof T>( } return ( - <InputWrapper<T, K> + <InputWrapper {...props} help={props.help} disabled={disabled ?? false} diff --git a/packages/web-util/src/forms/fields/InputSecret.tsx b/packages/web-util/src/forms/fields/InputSecret.tsx @@ -2,8 +2,8 @@ import { VNode, h } from "preact"; import { InputLine } from "./InputLine.js"; import { UIFormProps } from "../FormProvider.js"; -export function InputSecret<T extends object, K extends keyof T>( - props: UIFormProps<T, K>, +export function InputSecret( + props: UIFormProps<string>, ): VNode { return <InputLine type="password" {...props} />; } diff --git a/packages/web-util/src/forms/fields/InputSelectMultiple.tsx b/packages/web-util/src/forms/fields/InputSelectMultiple.tsx @@ -1,17 +1,20 @@ import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { useTranslationContext } from "../../index.browser.js"; import { UIFormProps } from "../FormProvider.js"; import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; import { ChoiceS } from "./InputChoiceStacked.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; -import { useTranslationContext } from "../../index.browser.js"; -export function InputSelectMultiple<T extends object, K extends keyof T>( +/** + * @type ChoiceVal result type of the choice (for example: "choiceA" | "choiceB") + */ +export function InputSelectMultiple<ChoiceVal>( props: { - choices: ChoiceS<T[K]>[]; + choices: ChoiceS<ChoiceVal>[]; unique?: boolean; max?: number; - } & UIFormProps<T, K>, + } & UIFormProps<ChoiceVal>, ): VNode { const { converter, diff --git a/packages/web-util/src/forms/fields/InputSelectOne.tsx b/packages/web-util/src/forms/fields/InputSelectOne.tsx @@ -5,10 +5,10 @@ import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; import { ChoiceS } from "./InputChoiceStacked.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; -export function InputSelectOne<T extends object, K extends keyof T>( +export function InputSelectOne<Choices>( props: { - choices: ChoiceS<T[K]>[]; - } & UIFormProps<T, K>, + choices: ChoiceS<Choices>[]; + } & UIFormProps<Choices>, ): VNode { const { label, choices, placeholder, tooltip, required, help, hidden } = props; diff --git a/packages/web-util/src/forms/fields/InputText.tsx b/packages/web-util/src/forms/fields/InputText.tsx @@ -2,8 +2,8 @@ import { VNode, h } from "preact"; import { UIFormProps } from "../FormProvider.js"; import { InputLine } from "./InputLine.js"; -export function InputText<T extends object, K extends keyof T>( - props: UIFormProps<T, K>, +export function InputText( + props: UIFormProps<string>, ): VNode { return <InputLine type="text" {...props} />; } diff --git a/packages/web-util/src/forms/fields/InputTextArea.tsx b/packages/web-util/src/forms/fields/InputTextArea.tsx @@ -1,9 +1,7 @@ import { VNode, h } from "preact"; -import { InputLine } from "./InputLine.js"; import { UIFormProps } from "../FormProvider.js"; +import { InputLine } from "./InputLine.js"; -export function InputTextArea<T extends object, K extends keyof T>( - props: UIFormProps<T, K>, -): VNode { +export function InputTextArea(props: UIFormProps<string>): VNode { return <InputLine type="text-area" {...props} />; } diff --git a/packages/web-util/src/forms/fields/InputToggle.tsx b/packages/web-util/src/forms/fields/InputToggle.tsx @@ -1,11 +1,16 @@ import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; import { UIFormProps } from "../FormProvider.js"; import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; -import { useState } from "preact/hooks"; -export function InputToggle<T extends object, K extends keyof T>( - props: { threeState: boolean } & UIFormProps<T, K>, +/** + * Two-state (on/off) or tri-state (on/off/unselected) toggle. + * + * FIXME: Types would be clearer if two/tri state were different types. + */ +export function InputToggle( + props: { threeState: boolean } & UIFormProps<boolean>, ): VNode { const { label, tooltip, help, required, threeState } = props; const { value, onChange, error } =