taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 2f405d63bd6523e0ccdbbd4e97daa635fe4ddb85
parent 3a92650352eae0386c9b832612a9876721ee67cf
Author: Sebastian <sebasjm@gmail.com>
Date:   Tue, 18 Mar 2025 13:18:37 -0300

calculate state and erros on a single iteration

Diffstat:
Mpackages/web-util/src/forms/forms-types.ts | 31+++++++++++++++++++++----------
Mpackages/web-util/src/forms/gana/VQF_902_1_customer.ts | 63++++++++++++++++++++++-----------------------------------------
Mpackages/web-util/src/hooks/useForm.ts | 154++++++++++++++++++++++++++++++-------------------------------------------------
3 files changed, 101 insertions(+), 147 deletions(-)

diff --git a/packages/web-util/src/forms/forms-types.ts b/packages/web-util/src/forms/forms-types.ts @@ -194,7 +194,26 @@ type UIFormFieldToggle = { threeState?: boolean; } & UIFormFieldBaseConfig; -export type UIFieldElementDescription = { +export type ComputableFieldConfig = { + /* if the field should be initially hidden */ + hidden?: boolean; + + /* show a mark as required */ + required?: boolean; + + /* readonly and dim */ + disabled?: boolean; + + /* + update field config based on form state + */ + updateProps?: ( + value: any, + root?: any, + ) => Partial<ComputableFieldConfig> | undefined; +}; + +export type UIFieldElementDescription = ComputableFieldConfig & { /* label if the field, visible for the user */ label: string; @@ -204,9 +223,6 @@ export type UIFieldElementDescription = { /* short text to be shown close to the field, usually below and dimmer*/ help?: string; - /* if the field should be initially hidden */ - hidden?: boolean; - /* ui element to show before */ addonBeforeId?: string; @@ -218,17 +234,12 @@ export type UIFormFieldBaseConfig = UIFieldElementDescription & { /* example to be shown inside the field */ placeholder?: string; - /* show a mark as required */ - required?: boolean; - - /* readonly and dim */ - disabled?: boolean; - /* conversion id to convert the string into the value type the id should be known to the ui impl */ converterId?: string; + // FIXME: deprectate this and move to `computableFieldConfig` /* return if the field should be hidden. receives the value after conversion and the root of the form. diff --git a/packages/web-util/src/forms/gana/VQF_902_1_customer.ts b/packages/web-util/src/forms/gana/VQF_902_1_customer.ts @@ -43,7 +43,7 @@ member act as director of a domiciliary company, this domiciliary company is the title: i18n.str`Information on customer`, description: i18n.str`The customer is a natural person`, hide(root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; }, fields: [ { @@ -52,7 +52,7 @@ member act as director of a domiciliary company, this domiciliary company is the type: "text", required: true, hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; }, }, { @@ -61,7 +61,7 @@ member act as director of a domiciliary company, this domiciliary company is the type: "textArea", required: true, hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; }, }, { @@ -70,7 +70,7 @@ member act as director of a domiciliary company, this domiciliary company is the type: "text", required: false, hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; }, }, { @@ -79,7 +79,7 @@ member act as director of a domiciliary company, this domiciliary company is the type: "text", required: false, hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; }, }, { @@ -90,7 +90,7 @@ member act as director of a domiciliary company, this domiciliary company is the pattern: "dd/MM/yyyy", required: true, hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; }, }, { @@ -100,7 +100,7 @@ member act as director of a domiciliary company, this domiciliary company is the choices: countryNationalityList(i18n), required: true, hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; }, }, { @@ -110,7 +110,7 @@ member act as director of a domiciliary company, this domiciliary company is the accept: "application/pdf", required: true, hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; }, }, ], @@ -119,7 +119,7 @@ member act as director of a domiciliary company, this domiciliary company is the title: i18n.str`Information on customer (sole proprietor)`, description: i18n.str`The customer is a sole proprietor`, hide(root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; }, fields: [ { @@ -128,7 +128,7 @@ member act as director of a domiciliary company, this domiciliary company is the type: "text", required: false, hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; }, }, { @@ -138,7 +138,7 @@ member act as director of a domiciliary company, this domiciliary company is the type: "text", required: false, hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; }, }, { @@ -149,7 +149,7 @@ member act as director of a domiciliary company, this domiciliary company is the required: false, accept: "application/pdf", hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "NATURAL_PERSON"; }, }, ], @@ -158,35 +158,16 @@ member act as director of a domiciliary company, this domiciliary company is the title: i18n.str`Information on customer`, description: i18n.str`The customer is a legal entity`, hide(root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; }, fields: [ - // { - // id: TalerFormAttributes.VQF_902_1.CUSTOMER_NATURAL_COMPANY_NAME.id, - // label: i18n.str`Company name`, - // type: "text", - // required: false, - // hide(value, root) { - // return !!root && root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; - // }, - // }, - // { - // id: TalerFormAttributes.VQF_902_1.CUSTOMER_NATURAL_REGISTERED_OFFICE - // .id, - // label: i18n.str`Registered office`, - // type: "text", - // required: false, - // hide(value, root) { - // return !!root && root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; - // }, - // }, { id: TalerFormAttributes.VQF_902_1.CUSTOMER_ENTITY_COMPANY_NAME.id, label: i18n.str`Company name`, type: "text", required: true, hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; }, }, { @@ -195,7 +176,7 @@ member act as director of a domiciliary company, this domiciliary company is the type: "textArea", required: true, hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; }, }, { @@ -205,7 +186,7 @@ member act as director of a domiciliary company, this domiciliary company is the type: "text", required: false, hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; }, }, { @@ -214,7 +195,7 @@ member act as director of a domiciliary company, this domiciliary company is the type: "text", required: false, hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; }, }, { @@ -223,7 +204,7 @@ member act as director of a domiciliary company, this domiciliary company is the type: "text", required: false, hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; }, }, { @@ -233,7 +214,7 @@ member act as director of a domiciliary company, this domiciliary company is the accept: "application/pdf", required: true, hide(value, root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; + return !root || root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; }, }, ], @@ -241,14 +222,14 @@ member act as director of a domiciliary company, this domiciliary company is the { title: i18n.str`Information on the natural persons who establish the business relationship for legal entities and partnerships`, description: i18n.str`For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified.`, - hide(root) { - return !!root && root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; - }, fields: [ { id: TalerFormAttributes.VQF_902_1.FOUNDER_LIST.id, label: i18n.str`Founders`, type: "array", + hide(value, root) { + return !root || root["CUSTOMER_INFO_TYPE"] !== "LEGAL_ENTITY"; + }, labelFieldId: TalerFormAttributes.VQF_902_1_founder.FOUNDER_FULL_NAME.id, fields: [ diff --git a/packages/web-util/src/hooks/useForm.ts b/packages/web-util/src/hooks/useForm.ts @@ -29,6 +29,7 @@ import { UIFormElementConfig, useTranslationContext, } from "../index.browser.js"; +import e from "express"; export type FormHandler<T> = { [k in keyof T]?: T[k] extends string @@ -103,15 +104,11 @@ export function useForm<T>( const [formValue, formUpdateHandler] = useState<RecursivePartial<FormValues<T>>>(initialValue); - const errors = undefinedIfEmpty<FormErrors<T> | undefined>( - validateRequiredFields(formValue, design, i18n), - ); - - const { handler, result } = constructFormHandler( + const { handler, result, errors } = constructFormHandler( design, formValue, formUpdateHandler, - errors, + i18n ); const status = { @@ -181,90 +178,53 @@ export function undefinedIfEmpty<T extends object | undefined>( : undefined; } -function validateRequiredFields<FormType>( - formData: object, - formDesign: FormDesign, - i18n: InternationalizationAPI, -): FormErrors<FormType> | undefined { - let result: FormErrors<FormType> | undefined = undefined; - - function checkIfRequiredFieldHasValue( - formElement: UIFormElementConfig, - hiddenSection: boolean | undefined, - ) { - if (!("id" in formElement)) { - return; - } - const path = formElement.id.split("."); - const v = getValueFromPath(formData as any, path); - - const hidden = - hiddenSection === true || - formElement.hidden === true || - (formElement.hide && formElement.hide(v, formData) === true); +function checkFormFieldIsValid(formElement: UIFormElementConfig, currentValue: string | undefined, i18n: InternationalizationAPI): ErrorAndLabel | undefined { + if (!("id" in formElement)) { + return undefined; + } - if (hidden) { - return; - } - if (formElement.required && v === undefined) { - const e: ErrorAndLabel = { + if (formElement.required && currentValue === undefined) { + return { + label: formElement.label as TranslatedString, + message: i18n.str`required`, + }; + } else if (formElement.validator) { + try { + const message = formElement.validator(currentValue as any); + if (message !== undefined) { + return { + label: formElement.label as TranslatedString, + message, + }; + } + } catch (e) { + console.error(e); + const message = i18n.str`Validation function failed. Contact developers ${String( + e, + )}` + console.log(message); + return { label: formElement.label as TranslatedString, - message: i18n.str`required`, + message, }; - result = setValueIntoPath(result, path, e); - } - if (formElement.validator) { - try { - const message = formElement.validator(v as any); - if (message !== undefined) { - const e: ErrorAndLabel = { - label: formElement.label as TranslatedString, - message, - }; - result = setValueIntoPath(result, path, e); - } - } catch (e) { - console.log( - `Validation function failed. Contact developers ${String(e)}`, - ); - console.error(e); - result = setValueIntoPath( - result, - path, - `Validation function failed. Contact developers ${String(e)}`, - ); - } } } - - switch (formDesign.type) { - case "double-column": { - formDesign.sections.forEach((sec) => { - const hidden = sec.hide && sec.hide(result); - sec.fields.forEach((f) => checkIfRequiredFieldHasValue(f, hidden)); - }); - break; - } - case "single-column": { - formDesign.fields.forEach((f) => checkIfRequiredFieldHasValue(f, undefined)); - break; - } - default: { - assertUnreachable(formDesign); - } - } - - return result; + return undefined } function constructFormHandler<T>( design: FormDesign, formValue: RecursivePartial<FormValues<T>>, onValueChange: (d: RecursivePartial<FormValues<T>>) => void, - errors: FormErrors<T> | undefined, -): { handler: FormHandler<T>; result: FormStatus<T> } { + i18n: InternationalizationAPI, +): { + handler: FormHandler<T>; + result: FormStatus<T>; + errors: FormErrors<T> | undefined; +} { let handler: FormHandler<T> = {}; let result = {} as FormStatus<T>; + let errors: FormErrors<T> | undefined = undefined; function createFieldHandler( formElement: UIFormElementConfig, @@ -275,21 +235,28 @@ function constructFormHandler<T>( } const path = formElement.id.split("."); - function updater(newValue: unknown) { - const updated = setValueIntoPath(formValue, path, newValue); - onValueChange(updated); - } - const currentValue = getValueFromPath<string>( formValue as any, path, undefined, ); - const currentError = getValueFromPath<ErrorAndLabel>( - errors as any, - path, - undefined, - ); + + // compute prop based un state + const hidden = + hiddenSection === true || + formElement.hidden === true || + (formElement.hide && formElement.hide(currentValue, result) === true); + + const currentError: ErrorAndLabel | undefined = !hidden ? checkFormFieldIsValid(formElement, currentValue, i18n) : undefined; + + if (currentError !== undefined) { + errors = setValueIntoPath(errors, path, currentError); + } + + function updater(newValue: unknown) { + const updated = setValueIntoPath(formValue, path, newValue); + onValueChange(updated); + } const field: UIFieldHandler = { error: currentError?.message, @@ -297,15 +264,10 @@ function constructFormHandler<T>( onChange: updater, parentRef: result, }; - + + // FIXME: handler should not be set but we also need to refactor + // ui components. handler = setValueIntoPath(handler, path, field); - - // compute prop based un state - const hidden = - hiddenSection === true || - formElement.hidden === true || - (formElement.hide && formElement.hide(field.value, result) === true); - if (!hidden) { result = setValueIntoPath(result, path, field.value); } @@ -328,5 +290,5 @@ function constructFormHandler<T>( } } - return { handler, result }; + return { handler, result, errors }; }