diff options
author | Sebastian <sebasjm@gmail.com> | 2024-05-03 08:43:53 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-05-03 08:44:07 -0300 |
commit | 20353eda268efa962959bead466b59823bfb9b29 (patch) | |
tree | 868d016693f09b40e2c55893d3aed72eca505ecb | |
parent | fa4c7039f4ebeb6ad3cf19237ad7b138519ac142 (diff) | |
download | wallet-core-20353eda268efa962959bead466b59823bfb9b29.tar.gz wallet-core-20353eda268efa962959bead466b59823bfb9b29.tar.bz2 wallet-core-20353eda268efa962959bead466b59823bfb9b29.zip |
form hook now takes the shape of the form (do not rely on initial value)
-rw-r--r-- | packages/aml-backoffice-ui/src/context/ui-forms.ts | 36 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/forms/simplest.ts | 1 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/hooks/form.ts | 89 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 4 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx | 239 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/Cases.tsx | 14 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CreateAccount.tsx | 2 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx | 7 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx | 2 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/utils/converter.ts | 25 | ||||
-rw-r--r-- | packages/web-util/src/forms/Caption.tsx | 17 | ||||
-rw-r--r-- | packages/web-util/src/forms/Group.tsx | 38 | ||||
-rw-r--r-- | packages/web-util/src/forms/InputAmount.tsx | 6 | ||||
-rw-r--r-- | packages/web-util/src/forms/InputChoiceHorizontal.tsx | 8 | ||||
-rw-r--r-- | packages/web-util/src/forms/InputLine.tsx | 83 |
15 files changed, 360 insertions, 211 deletions
diff --git a/packages/aml-backoffice-ui/src/context/ui-forms.ts b/packages/aml-backoffice-ui/src/context/ui-forms.ts index 2e0b8a76d..9cf6125c9 100644 --- a/packages/aml-backoffice-ui/src/context/ui-forms.ts +++ b/packages/aml-backoffice-ui/src/context/ui-forms.ts @@ -39,7 +39,7 @@ import { useContext } from "preact/hooks"; export type Type = UiForms; const defaultForms: UiForms = { - forms: [] + forms: [], }; const Context = createContext<Type>(defaultForms); @@ -142,7 +142,7 @@ type UIFormFieldConfigCaption = { type UIFormFieldConfigGroup = { type: "group"; - properties: UIFormFieldBaseConfig & { + properties: UIFieldBaseDescription & { fields: UIFormFieldConfig[]; }; }; @@ -213,7 +213,7 @@ type UIFormFieldConfigToggle = { properties: UIFormFieldBaseConfig; }; -type UIFieldBaseDescription = { +export type UIFieldBaseDescription = { /* label if the field, visible for the user */ label: string; /* long text to be shown on user demand */ @@ -222,6 +222,9 @@ type UIFieldBaseDescription = { /* short text to be shown close to the field */ help?: string; + /* name of the field, useful for a11y */ + name: string; + /* if the field should be initialy hidden */ hidden?: boolean; /* ui element to show before */ @@ -230,7 +233,7 @@ type UIFieldBaseDescription = { addonAfterId?: string; }; -type UIFormFieldBaseConfig = UIFieldBaseDescription & { +export type UIFormFieldBaseConfig = UIFieldBaseDescription & { /* example to be shown inside the field */ placeholder?: string; @@ -240,9 +243,6 @@ type UIFormFieldBaseConfig = UIFieldBaseDescription & { /* readonly and dim */ disabled?: boolean; - /* name of the field, useful for a11y */ - name: string; - /* conversion id to conver the string into the value type the id should be known to the ui impl */ @@ -258,23 +258,27 @@ export type UIHandlerId = string & { [__handlerId]: true }; // FIXME: validate well formed ui field id const codecForUiFieldId = codecForString as () => Codec<UIHandlerId>; -const codecForUIFormFieldBaseConfigTemplate = < - T extends UIFormFieldBaseConfig, +const codecForUIFormFieldBaseDescriptionTemplate = < + T extends UIFieldBaseDescription, >() => buildCodecForObject<T>() - .property("id", codecForUiFieldId()) .property("addonAfterId", codecOptional(codecForString())) .property("addonBeforeId", codecOptional(codecForString())) - .property("converterId", codecOptional(codecForString())) - .property("disabled", codecOptional(codecForBoolean())) .property("hidden", codecOptional(codecForBoolean())) - .property("required", codecOptional(codecForBoolean())) .property("help", codecOptional(codecForString())) .property("label", codecForString()) .property("name", codecForString()) - .property("placeholder", codecOptional(codecForString())) .property("tooltip", codecOptional(codecForString())); +const codecForUIFormFieldBaseConfigTemplate = < + T extends UIFormFieldBaseConfig, +>() => + codecForUIFormFieldBaseDescriptionTemplate<T>() + .property("id", codecForUiFieldId()) + .property("converterId", codecOptional(codecForString())) + .property("disabled", codecOptional(codecForBoolean())) + .property("required", codecOptional(codecForBoolean())) + .property("placeholder", codecOptional(codecForString())); const codecForUIFormFieldBaseConfig = (): Codec<UIFormFieldBaseConfig> => codecForUIFormFieldBaseConfigTemplate().build("UIFieldToggleProperties"); @@ -370,7 +374,9 @@ const codecForUiFormFieldFile = (): Codec<UIFormFieldConfigFile> => const codecForUIFormFieldWithFieldsConfig = (): Codec< UIFormFieldConfigGroup["properties"] > => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigGroup["properties"]>() + codecForUIFormFieldBaseDescriptionTemplate< + UIFormFieldConfigGroup["properties"] + >() .property("fields", codecForList(codecForUiFormField())) .build("UIFormFieldConfigGroup.properties"); diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts index c7ba95462..b52f2bf74 100644 --- a/packages/aml-backoffice-ui/src/forms/simplest.ts +++ b/packages/aml-backoffice-ui/src/forms/simplest.ts @@ -86,6 +86,7 @@ export function resolutionSection( id: ".threshold" as UIHandlerId, currency: "NETZBON", name: "threshold", + converterId: "Taler.Amount", label: i18n.str`New threshold`, }, }, diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts index edeae6085..752444bd2 100644 --- a/packages/aml-backoffice-ui/src/hooks/form.ts +++ b/packages/aml-backoffice-ui/src/hooks/form.ts @@ -15,12 +15,14 @@ */ import { + AbsoluteTime, AmountJson, TalerExchangeApi, TranslatedString, } from "@gnu-taler/taler-util"; import { UIFieldHandler } from "@gnu-taler/web-util/browser"; import { useState } from "preact/hooks"; +import { UIFormFieldConfig, UIHandlerId } from "../context/ui-forms.js"; // export type UIField = { // value: string | undefined; @@ -57,6 +59,8 @@ export type FormErrors<T> = { ? TranslatedString : T[k] extends AmountJson ? TranslatedString + : T[k] extends AbsoluteTime + ? TranslatedString : T[k] extends TalerExchangeApi.AmlState ? TranslatedString : FormErrors<T[k]>; @@ -75,31 +79,22 @@ export type FormStatus<T> = }; function constructFormHandler<T>( + shape: Array<UIHandlerId>, form: RecursivePartial<FormValues<T>>, updateForm: (d: RecursivePartial<FormValues<T>>) => void, errors: FormErrors<T> | undefined, ): FormHandler<T> { - const keys = Object.keys(form) as Array<keyof T>; - const handler = keys.reduce((prev, fieldName) => { - const currentValue: unknown = form[fieldName]; - const currentError: unknown = - errors !== undefined ? errors[fieldName] : undefined; + const handler = shape.reduce((handleForm, fieldId) => { + + const path = fieldId.split(".") + function updater(newValue: unknown) { - updateForm({ ...form, [fieldName]: newValue }); - } - /** - * There is no clear way to know if this object is a custom field - * or a group of fields - */ - if (typeof currentValue === "object") { - // @ts-expect-error FIXME better typing - const group = constructFormHandler(currentValue, updater, currentError); - // @ts-expect-error FIXME better typing - prev[fieldName] = group; - return prev; + updateForm(setValueDeeper(form, path, newValue)); } + const currentValue: unknown = getValueDeeper(form, path) + const currentError: unknown = errors === undefined ? undefined : getValueDeeper(errors, path) const field: UIFieldHandler = { // @ts-expect-error FIXME better typing error: currentError, @@ -108,14 +103,37 @@ function constructFormHandler<T>( onChange: updater, state: {}, }; - // @ts-expect-error FIXME better typing - prev[fieldName] = field; - return prev; + + return setValueDeeper(handleForm, path, field) + + /** + * There is no clear way to know if this object is a custom field + * or a group of fields + */ + // if (typeof currentValue === "object") { + // // @ts-expect-error FIXME better typing + // const group = constructFormHandler(currentValue, updater, currentError); + // // @ts-expect-error FIXME better typing + // prev[fieldName] = group; + // return prev; + // } + + // const field: UIFieldHandler = { + // // @ts-expect-error FIXME better typing + // error: currentError, + // // @ts-expect-error FIXME better typing + // value: currentValue, + // onChange: updater, + // state: {}, + // }; + // handleForm[fieldName] = field; + // return handleForm; }, {} as FormHandler<T>); return handler; } + /** * FIXME: Consider sending this to web-utils * @@ -125,6 +143,7 @@ function constructFormHandler<T>( * @returns */ export function useFormState<T>( + shape: Array<UIHandlerId>, defaultValue: RecursivePartial<FormValues<T>>, check: (f: RecursivePartial<FormValues<T>>) => FormStatus<T>, ): [FormHandler<T>, FormStatus<T>] { @@ -132,7 +151,35 @@ export function useFormState<T>( useState<RecursivePartial<FormValues<T>>>(defaultValue); const status = check(form); - const handler = constructFormHandler(form, updateForm, status.errors); + const handler = constructFormHandler(shape, form, updateForm, status.errors); return [handler, status]; } + + +function getValueDeeper( + object: Record<string, any>, + names: string[], +): UIFieldHandler { + if (names.length === 0) return object as UIFieldHandler; + const [head, ...rest] = names; + if (!head) { + return getValueDeeper(object, rest); + } + if (object === undefined) { + throw Error("handler not found"); + } + return getValueDeeper(object[head], rest); +} + +function setValueDeeper(object: any, names: string[], value: any): any { + if (names.length === 0) return value; + const [head, ...rest] = names; + if (!head) { + return setValueDeeper(object, rest, value); + } + if (object === undefined) { + return { [head]: setValueDeeper({}, rest, value) }; + } + return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) }; +} diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx index 62c54d222..11b6d053e 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -45,6 +45,7 @@ import { privatePages } from "../Routing.js"; import { FormMetadata, useUiFormsContext } from "../context/ui-forms.js"; import { useCaseDetails } from "../hooks/useCaseDetails.js"; import { ShowConsolidated } from "./ShowConsolidated.js"; +import { preloadedForms } from "../forms/index.js"; export type AmlEvent = | AmlFormEvent @@ -164,6 +165,7 @@ export function CaseDetails({ account }: { account: string }) { const details = useCaseDetails(account); const {forms} = useUiFormsContext() + const allForms = [...forms, ...preloadedForms(i18n)] if (!details) { return <Loading />; } @@ -183,7 +185,7 @@ export function CaseDetails({ account }: { account: string }) { } const { aml_history, kyc_attributes } = details.body; - const events = getEventsFromAmlHistory(aml_history, kyc_attributes, i18n, forms); + const events = getEventsFromAmlHistory(aml_history, kyc_attributes, i18n, allForms); if (showForm !== undefined) { return ( diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx index f04d404d0..bbfa65ca7 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx @@ -20,13 +20,15 @@ import { HttpStatusCode, TalerExchangeApi, TalerProtocolTimestamp, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; import { + Addon, Button, InternationalizationAPI, LocalNotificationBanner, RenderAllFieldsByUiConfig, + StringConverter, UIFieldHandler, UIFormField, useExchangeApiContext, @@ -37,14 +39,19 @@ import { Fragment, VNode, h } from "preact"; import { privatePages } from "../Routing.js"; import { FormMetadata, + UIFieldBaseDescription, + UIFormFieldBaseConfig, UIFormFieldConfig, + UIHandlerId, useUiFormsContext, } from "../context/ui-forms.js"; import { preloadedForms } from "../forms/index.js"; -import { FormHandler, useFormState } from "../hooks/form.js"; +import { FormErrors, FormHandler, useFormState } from "../hooks/form.js"; import { useOfficer } from "../hooks/officer.js"; import { Justification } from "./CaseDetails.js"; import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; +import { undefinedIfEmpty } from "./CreateAccount.js"; +import { getConverterById } from "../utils/converter.js"; function searchForm( i18n: InternationalizationAPI, @@ -67,6 +74,7 @@ type FormType = { when: AbsoluteTime; state: TalerExchangeApi.AmlState; threshold: AmountJson; + comment: string; }; export function CaseUpdate({ @@ -89,6 +97,7 @@ export function CaseUpdate({ when: AbsoluteTime.now(), state: TalerExchangeApi.AmlState.pending, threshold: Amounts.zeroOfCurrency(config.currency), + comment: "", }; if (officer.state !== "ready") { @@ -99,27 +108,41 @@ export function CaseUpdate({ return <div>form with id {formId} not found</div>; } - let defaultValue = initial + const shape: Array<UIHandlerId> = []; theForm.config.design.forEach((section) => { section.fields.forEach((field) => { if ("id" in field.properties) { - const path = field.properties.id.split("."); - defaultValue = setValueDeeper(defaultValue, path, undefined); + shape.push(field.properties.id); + // const path = field.properties.id.split("."); + // defaultValue = setValueDeeper(defaultValue, path, undefined); } }); }); - - const [form, state] = useFormState<FormType>(defaultValue, (st) => { + const [form, state] = useFormState<FormType>(shape, initial, (st) => { + const errors = undefinedIfEmpty<FormErrors<FormType>>({ + state: !st.state ? i18n.str`required` : undefined, + threshold: !st.threshold ? i18n.str`required` : undefined, + when: !st.when ? i18n.str`required` : undefined, + comment: !st.comment ? i18n.str`required` : undefined, + }); + if (errors === undefined) { + return { + status: "ok", + result: st as any, + errors: undefined, + }; + } return { - status: "ok", + status: "fail", result: st as any, - errors: undefined, + errors, }; }); - - console.log("FORM", form) - const validatedForm = state.status === "fail" ? undefined : state.result; + + console.log("NOW FORM", form); + + const validatedForm = state.status !== "ok" ? undefined : state.result; const submitHandler = validatedForm === undefined @@ -166,75 +189,6 @@ export function CaseUpdate({ }, ); - function convertUiField( - fieldConfig: UIFormFieldConfig[], - form: FormHandler<unknown>, - ): UIFormField[] { - return fieldConfig.map((config) => { - switch (config.type) { - case "absoluteTime": { - return undefined!; - } - case "amount": { - return { - type: "amount", - properties: { - ...(config.properties as any), - handler: getValueDeeper(form, config.properties.id.split(".")), - }, - } as UIFormField; - } - case "array": { - return undefined!; - } - case "caption": { - return undefined!; - } - case "choiceHorizontal": { - return { - type: "choiceHorizontal", - properties: { - handler: getValueDeeper(form, config.properties.id.split(".")), - choices: config.properties.choices, - }, - } as UIFormField; - } - case "choiceStacked": - case "file": - case "group": - case "integer": - case "selectMultiple": - case "selectOne": { - return undefined!; - } - case "text": { - return { - type: "text", - properties: { - ...(config.properties as any), - handler: getValueDeeper(form, config.properties.id.split(".")), - }, - } as UIFormField; - } - case "textArea": { - return { - type: "text", - properties: { - ...(config.properties as any), - handler: getValueDeeper(form, config.properties.id.split(".")), - }, - } as UIFormField; - } - case "toggle": { - return undefined!; - } - default: { - assertUnreachable(config); - } - } - }); - } - return ( <Fragment> <LocalNotificationBanner notification={notification} /> @@ -261,7 +215,7 @@ export function CaseUpdate({ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> <RenderAllFieldsByUiConfig key={i} - fields={convertUiField(section.fields, form)} + fields={convertUiField(i18n, section.fields, form)} /> </div> </div> @@ -281,7 +235,8 @@ export function CaseUpdate({ <Button type="submit" handler={submitHandler} - class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + disabled={!submitHandler} + class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Confirm</i18n.Translate> </Button> @@ -350,3 +305,121 @@ function setValueDeeper(object: any, names: string[], value: any): any { } return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) }; } + +function getAddonById(_id: string | undefined): Addon { + return undefined!; +} + + +function converInputFieldsProps( + form: FormHandler<unknown>, + p: UIFormFieldBaseConfig, +) { + return { + converter: getConverterById(p.converterId), + handler: getValueDeeper(form, p.id.split(".")), + }; +} + +function converBaseFieldsProps( + i18n_: InternationalizationAPI, + p: UIFieldBaseDescription, +) { + return { + after: getAddonById(p.addonAfterId), + before: getAddonById(p.addonBeforeId), + hidden: p.hidden, + name: p.name, + help: i18n_.str`${p.help}`, + label: i18n_.str`${p.label}`, + tooltip: i18n_.str`${p.tooltip}`, + }; +} + +function convertUiField( + i18n_: InternationalizationAPI, + fieldConfig: UIFormFieldConfig[], + form: FormHandler<unknown>, +): UIFormField[] { + return fieldConfig.map((config) => { + // NON input fields + switch (config.type) { + case "caption": { + const resp: UIFormField = { + type: config.type, + properties: converBaseFieldsProps(i18n_, config.properties), + }; + return resp; + } + case "group": { + const resp: UIFormField = { + type: config.type, + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + fields: convertUiField(i18n_, config.properties.fields, form), + }, + }; + return resp; + } + } + // Input Fields + switch (config.type) { + case "absoluteTime": { + return undefined!; + } + case "amount": { + return { + type: "amount", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties), + }, + } as UIFormField; + } + case "array": { + return undefined!; + } + case "choiceHorizontal": { + return { + type: "choiceHorizontal", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties), + choices: config.properties.choices, + }, + } as UIFormField; + } + case "choiceStacked": + case "file": + case "integer": + case "selectMultiple": + case "selectOne": { + return undefined!; + } + case "text": { + return { + type: "text", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties), + }, + } as UIFormField; + } + case "textArea": { + return { + type: "text", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties), + }, + } as UIFormField; + } + case "toggle": { + return undefined!; + } + default: { + assertUnreachable(config); + } + } + }); +} diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx index 2e92c111e..3860bcd98 100644 --- a/packages/aml-backoffice-ui/src/pages/Cases.tsx +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -24,7 +24,7 @@ import { ErrorLoading, InputChoiceHorizontal, Loading, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; @@ -34,6 +34,8 @@ import { privatePages } from "../Routing.js"; import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js"; import { undefinedIfEmpty } from "./CreateAccount.js"; import { Officer } from "./Officer.js"; +import { UIHandlerId } from "../context/ui-forms.js"; +import { amlStateConverter } from "../utils/converter.js"; type FormType = { state: TalerExchangeApi.AmlState; @@ -55,6 +57,7 @@ export function CasesUI({ const { i18n } = useTranslationContext(); const [form, status] = useFormState<FormType>( + [".state"] as Array<UIHandlerId>, { state: filter, }, @@ -106,18 +109,19 @@ export function CasesUI({ name="state" label={i18n.str`Filter`} handler={form.state} + converter={amlStateConverter} choices={[ { label: i18n.str`Pending`, - value: TalerExchangeApi.AmlState.pending, + value: "pending", }, { label: i18n.str`Frozen`, - value: TalerExchangeApi.AmlState.frozen, + value: "frozen", }, { label: i18n.str`Normal`, - value: TalerExchangeApi.AmlState.normal, + value: "normal", }, ]} /> @@ -269,7 +273,7 @@ export function Cases() { onNext={list.isLastPage ? undefined : list.loadNext} filter={stateFilter} onChangeFilter={(d) => { - setStateFilter(d) + setStateFilter(d); }} /> ); diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx index abcaaa2a6..98160fb3a 100644 --- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx +++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx @@ -31,6 +31,7 @@ import { } from "../hooks/form.js"; import { useOfficer } from "../hooks/officer.js"; import { usePreferences } from "../hooks/preferences.js"; +import { UIHandlerId } from "../context/ui-forms.js"; type FormType = { password: string; @@ -104,6 +105,7 @@ export function CreateAccount(): VNode { const [notification, withErrorHandler] = useLocalNotificationHandler(); const [form, status] = useFormState<FormType>( + [".password", ".repeat"] as Array<UIHandlerId>, { password: undefined, repeat: undefined, diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx index 0169572bf..0978d8bcc 100644 --- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx +++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx @@ -72,18 +72,19 @@ export function ShowConsolidated({ properties: { label: i18n.str`State`, name: "aml.state", + choices: [ { label: i18n.str`Frozen`, - value: TalerExchangeApi.AmlState.frozen, + value: "frozen", }, { label: i18n.str`Pending`, - value: TalerExchangeApi.AmlState.pending, + value: "pending", }, { label: i18n.str`Normal`, - value: TalerExchangeApi.AmlState.normal, + value: "normal", }, ], }, diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx index 9552f2b0c..b81e66468 100644 --- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx +++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx @@ -24,6 +24,7 @@ import { VNode, h } from "preact"; import { FormErrors, useFormState } from "../hooks/form.js"; import { useOfficer } from "../hooks/officer.js"; import { undefinedIfEmpty } from "./CreateAccount.js"; +import { UIHandlerId } from "../context/ui-forms.js"; type FormType = { password: string; @@ -36,6 +37,7 @@ export function UnlockAccount(): VNode { const [notification, withErrorHandler] = useLocalNotificationHandler(); const [form, status] = useFormState<FormType>( + [".password"] as Array<UIHandlerId>, { password: undefined, }, diff --git a/packages/aml-backoffice-ui/src/utils/converter.ts b/packages/aml-backoffice-ui/src/utils/converter.ts index cca764a81..25a824697 100644 --- a/packages/aml-backoffice-ui/src/utils/converter.ts +++ b/packages/aml-backoffice-ui/src/utils/converter.ts @@ -14,7 +14,8 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { TalerExchangeApi } from "@gnu-taler/taler-util"; +import { AmountJson, Amounts, TalerExchangeApi } from "@gnu-taler/taler-util"; +import { StringConverter } from "@gnu-taler/web-util/browser"; export const amlStateConverter = { toStringUI: stringifyAmlState, @@ -45,3 +46,25 @@ function parseAmlState(s: string | undefined): TalerExchangeApi.AmlState { throw Error(`unknown AML state: ${s}`); } } + +const amountConverter: StringConverter<AmountJson> = { + fromStringUI(v: string | undefined): AmountJson { + // FIXME: requires currency + return Amounts.parse(`NETZBON:${v}`) ?? Amounts.zeroOfCurrency("NETZBON"); + }, + toStringUI(v: unknown): string { + return v === undefined ? "" : Amounts.stringifyValue(v as AmountJson); + }, +}; + +export function getConverterById(id: string | undefined): StringConverter<unknown> { + if (id === "Taler.Amount") { + // @ts-expect-error check this + return amountConverter; + } + if (id === "TalerExchangeApi.AmlState") { + // @ts-expect-error check this + return amlStateConverter; + } + return undefined!; +} diff --git a/packages/web-util/src/forms/Caption.tsx b/packages/web-util/src/forms/Caption.tsx index 8facddec3..be4725ffa 100644 --- a/packages/web-util/src/forms/Caption.tsx +++ b/packages/web-util/src/forms/Caption.tsx @@ -1,27 +1,22 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { VNode, h } from "preact"; -import { - LabelWithTooltipMaybeRequired -} from "./InputLine.js"; +import { LabelWithTooltipMaybeRequired, RenderAddon } from "./InputLine.js"; +import { Addon } from "./FormProvider.js"; interface Props { label: TranslatedString; tooltip?: TranslatedString; help?: TranslatedString; - before?: VNode; - after?: VNode; + before?: Addon; + after?: Addon; } export function Caption({ before, after, label, tooltip, help }: Props): VNode { return ( <div class="sm:col-span-6 flex"> - {before !== undefined && ( - <span class="pointer-events-none flex items-center pr-2">{before}</span> - )} + {before !== undefined && <RenderAddon addon={before} />} <LabelWithTooltipMaybeRequired label={label} tooltip={tooltip} /> - {after !== undefined && ( - <span class="pointer-events-none flex items-center pl-2">{after}</span> - )} + {after !== undefined && <RenderAddon addon={after} />} {help && ( <p class="mt-2 text-sm text-gray-500" id="email-description"> {help} diff --git a/packages/web-util/src/forms/Group.tsx b/packages/web-util/src/forms/Group.tsx index 0645f6d97..d5626be1d 100644 --- a/packages/web-util/src/forms/Group.tsx +++ b/packages/web-util/src/forms/Group.tsx @@ -1,41 +1,39 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { VNode, h } from "preact"; -import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; +import { LabelWithTooltipMaybeRequired, RenderAddon } from "./InputLine.js"; import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js"; +import { Addon } from "./FormProvider.js"; interface Props { - before?: TranslatedString; - after?: TranslatedString; - tooltipBefore?: TranslatedString; - tooltipAfter?: TranslatedString; + label: TranslatedString; + tooltip?: TranslatedString; + help?: TranslatedString; + before?: Addon; + after?: Addon; fields: UIFormField[]; } export function Group({ before, after, - tooltipAfter, - tooltipBefore, + label, + tooltip, + help, fields, }: Props): VNode { return ( <div class="sm:col-span-6 p-4 rounded-lg border-r-2 border-2 bg-gray-50"> - <div class="pb-4"> - {before && ( - <LabelWithTooltipMaybeRequired - label={before} - tooltip={tooltipBefore} - /> - )} - </div> + {before !== undefined && <RenderAddon addon={before} />} + <LabelWithTooltipMaybeRequired label={label} tooltip={tooltip} /> + {after !== undefined && <RenderAddon addon={after} />} + {help && ( + <p class="mt-2 text-sm text-gray-500" id="email-description"> + {help} + </p> + )} <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-2 sm:grid-cols-6"> <RenderAllFieldsByUiConfig fields={fields} /> </div> - <div class="pt-4"> - {after && ( - <LabelWithTooltipMaybeRequired label={after} tooltip={tooltipAfter} /> - )} - </div> </div> ); } diff --git a/packages/web-util/src/forms/InputAmount.tsx b/packages/web-util/src/forms/InputAmount.tsx index 31e83350e..e8683468e 100644 --- a/packages/web-util/src/forms/InputAmount.tsx +++ b/packages/web-util/src/forms/InputAmount.tsx @@ -23,15 +23,15 @@ export function InputAmount<T extends object, K extends keyof T>( type: "text", text: currency as TranslatedString, }} - converter={{ - //@ts-ignore + //@ts-ignore + converter={ props.converter ?? { + fromStringUI: (v): AmountJson => { return ( Amounts.parse(`${currency}:${v}`) ?? Amounts.zeroOfCurrency(currency) ); }, - //@ts-ignore toStringUI: (v: AmountJson) => { return v === undefined ? "" : Amounts.stringifyValue(v); }, diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.tsx b/packages/web-util/src/forms/InputChoiceHorizontal.tsx index 82a7c3115..d8361718d 100644 --- a/packages/web-util/src/forms/InputChoiceHorizontal.tsx +++ b/packages/web-util/src/forms/InputChoiceHorizontal.tsx @@ -12,10 +12,10 @@ export interface ChoiceH<V> { export function InputChoiceHorizontal<T extends object, K extends keyof T>( props: { - choices: ChoiceH<T[K]>[]; + choices: ChoiceH<string>[]; } & UIFormProps<T, K>, ): VNode { - const { choices, label, tooltip, help, required } = props; + const { choices, label, tooltip, help, required, converter } = props; //FIXME: remove deprecated const fieldCtx = useField<T, K>(props.name); const { value, onChange, state } = @@ -38,7 +38,7 @@ export function InputChoiceHorizontal<T extends object, K extends keyof T>( const isLast = idx === choices.length - 1; let clazz = "relative inline-flex items-center px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 focus:z-10"; - if (choice.value === value) { + if (converter?.fromStringUI(choice.value as any) === value) { clazz += " text-white bg-indigo-600 hover:bg-indigo-500 ring-2 ring-indigo-600 hover:ring-indigo-500"; } else { @@ -61,7 +61,7 @@ export function InputChoiceHorizontal<T extends object, K extends keyof T>( class={clazz} onClick={(e) => { onChange( - (value === choice.value ? undefined : choice.value) as any, + (value === choice.value ? undefined : converter?.fromStringUI(choice.value as any)) as any, ); }} > diff --git a/packages/web-util/src/forms/InputLine.tsx b/packages/web-util/src/forms/InputLine.tsx index ee9150492..eb3238ef9 100644 --- a/packages/web-util/src/forms/InputLine.tsx +++ b/packages/web-util/src/forms/InputLine.tsx @@ -1,6 +1,6 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { ComponentChildren, Fragment, VNode, h } from "preact"; -import { UIFormProps } from "./FormProvider.js"; +import { Addon, UIFormProps } from "./FormProvider.js"; import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; import { useField } from "./useField.js"; @@ -68,6 +68,37 @@ export function LabelWithTooltipMaybeRequired({ return WithTooltip; } +export function RenderAddon({ disabled, addon }: { disabled?: boolean, addon: Addon }): VNode { + switch (addon.type) { + case "text": { + return ( + <span class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm"> + {addon.text} + </span> + ); + } + case "icon": { + return ( + <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> + {addon.icon} + </div> + ); + } + case "button": { + return ( + <button + type="button" + disabled={disabled} + onClick={addon.onClick} + class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-l-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50" + > + {addon.children} + </button> + ); + } + } +} + function InputWrapper<T extends object, K extends keyof T>({ children, label, @@ -91,47 +122,11 @@ function InputWrapper<T extends object, K extends keyof T>({ tooltip={tooltip} /> <div class="relative mt-2 flex rounded-md shadow-sm"> - {before && - (before.type === "text" ? ( - <span class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm"> - {before.text} - </span> - ) : before.type === "icon" ? ( - <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> - {before.icon} - </div> - ) : before.type === "button" ? ( - <button - type="button" - disabled={disabled} - onClick={before.onClick} - class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-l-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50" - > - {before.children} - </button> - ) : undefined)} + {before && <RenderAddon disabled={disabled} addon={before} />} {children} - {after && - (after.type === "text" ? ( - <span class="inline-flex items-center rounded-r-md border border-l-0 border-gray-300 px-3 text-gray-500 sm:text-sm"> - {after.text} - </span> - ) : after.type === "icon" ? ( - <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"> - {after.icon} - </div> - ) : after.type === "button" ? ( - <button - type="button" - disabled={disabled} - onClick={after.onClick} - class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50" - > - {after.children} - </button> - ) : undefined)} + {after && <RenderAddon disabled={disabled} addon={after} />} </div> {error && ( <p class="mt-2 text-sm text-red-600" id="email-error"> @@ -259,13 +254,13 @@ export function InputLine<T extends object, K extends keyof T>( name={String(name)} type={type} onChange={(e) => { - onChange(e.currentTarget.value as any); + onChange(fromString(e.currentTarget.value)); }} placeholder={placeholder ? placeholder : undefined} - value={value as string} - onBlur={() => { - onChange(fromString(value as any)); - }} + value={toString(value) ?? ""} + // onBlur={() => { + // onChange(fromString(value as any)); + // }} // defaultValue={toString(value)} disabled={state.disabled} aria-invalid={showError} |