diff options
author | Sebastian <sebasjm@gmail.com> | 2024-05-06 12:47:45 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-05-06 12:47:45 -0300 |
commit | bf03157b6570af6804032e206a3c60a3c7e030f3 (patch) | |
tree | 4d2b9ec0f0919703783bfe9ad40e2c02b27c2501 | |
parent | 35fee72ef3d75b7a9681353ab7a1ca5bacff150e (diff) | |
download | wallet-core-bf03157b6570af6804032e206a3c60a3c7e030f3.tar.xz |
add required validation
-rw-r--r-- | packages/aml-backoffice-ui/src/forms.json | 336 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/hooks/form.ts | 291 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx | 51 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/Cases.tsx | 2 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CreateAccount.tsx | 3 | ||||
-rw-r--r-- | packages/web-util/src/forms/Group.tsx | 11 | ||||
-rw-r--r-- | packages/web-util/src/forms/converter.ts (renamed from packages/aml-backoffice-ui/src/utils/converter.ts) | 2 | ||||
-rw-r--r-- | packages/web-util/src/forms/forms.ts | 229 | ||||
-rw-r--r-- | packages/web-util/src/forms/index.ts | 1 | ||||
-rw-r--r-- | packages/web-util/src/forms/ui-form.ts | 2 |
10 files changed, 524 insertions, 404 deletions
diff --git a/packages/aml-backoffice-ui/src/forms.json b/packages/aml-backoffice-ui/src/forms.json index de0da601c..ed556307b 100644 --- a/packages/aml-backoffice-ui/src/forms.json +++ b/packages/aml-backoffice-ui/src/forms.json @@ -17,6 +17,7 @@ "name": "customerType", "id": ".customerType", "label": "Type of customer", + "help": "Select one and complete the next form", "required": true, "choices": [ { @@ -47,173 +48,186 @@ "label": "Full name", "required": true } - } + }, + { + "type": "text", + "properties": { + "name": "naturalCustomer.address", + "id": ".naturalCustomer.address", + "label": "Residential address", + "required": true + } + }, + { + "type": "integer", + "properties": { + "name": "naturalCustomer.telephone", + "id": ".naturalCustomer.telephone", + "label": "Telephone" + } + }, + { + "type": "text", + "properties": { + "name": "naturalCustomer.email", + "id": ".naturalCustomer.email", + "label": "E-mail" + } + }, + { + "type": "absoluteTime", + "properties": { + "pattern": "dd/MM/yyyy", + "name": "naturalCustomer.dateOfBirth", + "id": ".naturalCustomer.dateOfBirth", + "label": "Date of birth", + "required": true + } + }, + { + "type": "text", + "properties": { + "name": "naturalCustomer.nationality", + "id": ".naturalCustomer.nationality", + "label": "Nationality", + "required": true + } + }, + { + "type": "text", + "properties": { + "name": "naturalCustomer.document", + "id": ".naturalCustomer.document", + "label": "Identification document", + "required": true + } + }, + { + "type": "file", + "properties": { + "name": "naturalCustomer.documentAttachment", + "id": ".naturalCustomer.documentAttachment", + "label": "Document attachment", + "required": true, + "maxBites": 2097152, + "accept": ".pdf", + "help": "PDF file with max size of 2 mega bytes" + } + }, + { + "type": "text", + "properties": { + "name": "naturalCustomer.companyName", + "id": ".naturalCustomer.companyName", + "label": "Company name" + } + }, + { + "type": "text", + "properties": { + "name": "naturalCustomer.office", + "id": ".naturalCustomer.office", + "label": "Registered office" + } + }, + { + "type": "text", + "properties": { + "name": "naturalCustomer.companyDocument", + "id": ".naturalCustomer.companyDocument", + "label": "Company identification document" + } + }, + { + "type": "file", + "properties": { + "name": "naturalCustomer.companyDocumentAttachment", + "id": ".naturalCustomer.companyDocumentAttachment", + "label": "Document attachment", + "required": true, + "maxBites": 2097152, + "accept": ".png", + "help": "PNG file with max size of 2 mega bytes" + } + } ] } }, + { - "type": "text", - "properties": { - "name": "naturalCustomer.address", - "id": ".naturalCustomer.address", - "label": "Residential address", - "required": true - } - }, - { - "type": "integer", - "properties": { - "name": "naturalCustomer.telephone", - "id": ".naturalCustomer.telephone", - "label": "Telephone" - } - }, - { - "type": "text", - "properties": { - "name": "naturalCustomer.email", - "id": ".naturalCustomer.email", - "label": "E-mail" - } - }, - { - "type": "absoluteTime", - "properties": { - "pattern": "dd/MM/yyyy", - "name": "naturalCustomer.dateOfBirth", - "id": ".naturalCustomer.dateOfBirth", - "label": "Date of birth", - "required": true - } - }, - { - "type": "text", - "properties": { - "name": "naturalCustomer.nationality", - "id": ".naturalCustomer.nationality", - "label": "Nationality", - "required": true - } - }, - { - "type": "text", - "properties": { - "name": "naturalCustomer.document", - "id": ".naturalCustomer.document", - "label": "Identification document", - "required": true - } - }, - { - "type": "file", - "properties": { - "name": "naturalCustomer.documentAttachment", - "id": ".naturalCustomer.documentAttachment", - "label": "Document attachment", - "required": true, - "maxBites": 2097152, - "accept": ".pdf", - "help": "PDF file with max size of 2 mega bytes" - } - }, - { - "type": "text", - "properties": { - "name": "naturalCustomer.companyName", - "id": ".naturalCustomer.companyName", - "label": "Company name" - } - }, - { - "type": "text", - "properties": { - "name": "naturalCustomer.office", - "id": ".naturalCustomer.office", - "label": "Registered office" - } - }, - { - "type": "text", - "properties": { - "name": "naturalCustomer.companyDocument", - "id": ".naturalCustomer.companyDocument", - "label": "Company identification document" - } - }, - { - "type": "file", - "properties": { - "name": "naturalCustomer.companyDocumentAttachment", - "id": ".naturalCustomer.companyDocumentAttachment", - "label": "Document attachment", - "required": true, - "maxBites": 2097152, - "accept": ".png", - "help": "PNG file with max size of 2 mega bytes" - } - }, - { - "type": "text", - "properties": { - "name": "legalCustomer.companyName", - "id": ".legalCustomer.companyName", - "label": "Company name", - "required": true - } - }, - { - "type": "text", - "properties": { - "name": "legalCustomer.domicile", - "id": ".legalCustomer.domicile", - "label": "Domicile", - "required": true - } - }, - { - "type": "text", - "properties": { - "name": "legalCustomer.contactPerson", - "id": ".legalCustomer.contactPerson", - "label": "Contact person" - } - }, - { - "type": "text", - "properties": { - "name": "legalCustomer.telephone", - "id": ".legalCustomer.telephone", - "label": "Telephone" - } - }, - { - "type": "text", - "properties": { - "name": "legalCustomer.email", - "id": ".legalCustomer.email", - "label": "E-mail" - } - }, - { - "type": "text", - "properties": { - "name": "legalCustomer.document", - "id": ".legalCustomer.document", - "label": "Identification document", - "help": "Not older than 12 month" - } - }, - { - "type": "file", + "type": "group", "properties": { - "name": "legalCustomer.documentAttachment", - "id": ".legalCustomer.documentAttachment", - "label": "Document attachment", - "required": true, - "maxBites": 2097152, - "accept": ".png", - "help": "PNG file with max size of 2 mega bytes" + "label": "Natural customer form", + "name": "algo", + "id": "algo", + "before": "a) Country risk (nationality)", + "after": "a) Country risk (nationality)", + "fields": [ + { + "type": "text", + "properties": { + "name": "legalCustomer.companyName", + "id": ".legalCustomer.companyName", + "label": "Company name", + "required": true + } + }, + { + "type": "text", + "properties": { + "name": "legalCustomer.domicile", + "id": ".legalCustomer.domicile", + "label": "Domicile", + "required": true + } + }, + { + "type": "text", + "properties": { + "name": "legalCustomer.contactPerson", + "id": ".legalCustomer.contactPerson", + "label": "Contact person" + } + }, + { + "type": "text", + "properties": { + "name": "legalCustomer.telephone", + "id": ".legalCustomer.telephone", + "label": "Telephone" + } + }, + { + "type": "text", + "properties": { + "name": "legalCustomer.email", + "id": ".legalCustomer.email", + "label": "E-mail" + } + }, + { + "type": "text", + "properties": { + "name": "legalCustomer.document", + "id": ".legalCustomer.document", + "label": "Identification document", + "help": "Not older than 12 month" + } + }, + { + "type": "file", + "properties": { + "name": "legalCustomer.documentAttachment", + "id": ".legalCustomer.documentAttachment", + "label": "Document attachment", + "required": true, + "maxBites": 2097152, + "accept": ".png", + "help": "PNG file with max size of 2 mega bytes" + } + } + ] } } ] diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts index 033d1d950..e9194d86d 100644 --- a/packages/aml-backoffice-ui/src/hooks/form.ts +++ b/packages/aml-backoffice-ui/src/hooks/form.ts @@ -19,11 +19,14 @@ import { AmountJson, TalerExchangeApi, TranslatedString, - assertUnreachable, } from "@gnu-taler/taler-util"; -import { Addon, InternationalizationAPI, UIFieldBaseDescription, UIFieldHandler, UIFormField, UIFormFieldBaseConfig, UIFormFieldConfig, UIHandlerId } from "@gnu-taler/web-util/browser"; +import { + UIFieldHandler, + UIFormFieldConfig, + UIHandlerId, +} from "@gnu-taler/web-util/browser"; import { useState } from "preact/hooks"; -import { getConverterById } from "../utils/converter.js"; +import { undefinedIfEmpty } from "../pages/CreateAccount.js"; // export type UIField = { // value: string | undefined; @@ -61,10 +64,10 @@ export type FormErrors<T> = { : T[k] extends AmountJson ? TranslatedString : T[k] extends AbsoluteTime - ? TranslatedString - : T[k] extends TalerExchangeApi.AmlState ? TranslatedString - : FormErrors<T[k]>; + : T[k] extends TalerExchangeApi.AmlState + ? TranslatedString + : FormErrors<T[k]>; }; export type FormStatus<T> = @@ -85,17 +88,19 @@ function constructFormHandler<T>( updateForm: (d: RecursivePartial<FormValues<T>>) => void, errors: FormErrors<T> | undefined, ): FormHandler<T> { - const handler = shape.reduce((handleForm, fieldId) => { + const path = fieldId.split("."); - const path = fieldId.split(".") - function updater(newValue: unknown) { updateForm(setValueDeeper(form, path, newValue)); } - const currentValue = getValueDeeper<string>(form as any, path, undefined) - const currentError = getValueDeeper<TranslatedString>(errors as any, path, undefined) + const currentValue = getValueDeeper<string>(form as any, path, undefined); + const currentError = getValueDeeper<TranslatedString>( + errors as any, + path, + undefined, + ); const field: UIFieldHandler = { error: currentError, value: currentValue, @@ -103,14 +108,12 @@ function constructFormHandler<T>( state: {}, //FIXME: add the state of the field (hidden, ) }; - return setValueDeeper(handleForm, path, field) - + return setValueDeeper(handleForm, path, field); }, {} as FormHandler<T>); return handler; } - /** * FIXME: Consider sending this to web-utils * @@ -135,7 +138,7 @@ export function useFormState<T>( interface Tree<T> extends Record<string, Tree<T> | T> {} -function getValueDeeper<T>( +export function getValueDeeper<T>( object: Tree<T> | undefined, names: string[], notFoundValue?: T, @@ -146,225 +149,79 @@ function getValueDeeper<T>( return getValueDeeper(object, rest, notFoundValue); } if (object === undefined) { - return notFoundValue + return notFoundValue; } return getValueDeeper(object[head] as Tree<T>, rest, notFoundValue); } -function getValueDeeper2( - object: Record<string, any>, - names: string[], -): UIFieldHandler { - if (names.length === 0) return object as UIFieldHandler; - const [head, ...rest] = names; - if (!head) { - return getValueDeeper2(object, rest); - } - if (object === undefined) { - throw Error("handler not found"); - } - return getValueDeeper2(object[head], rest); -} - - -function setValueDeeper(object: any, names: string[], value: any): any { +export 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 undefinedIfEmpty({ [head]: setValueDeeper({}, rest, value) }); } - 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, p), - handler: getValueDeeper2(form, p.id.split(".")), - name: p.name, - required: p.required, - disabled: p.disabled, - help: p.help, - placeholder: p.placeholder, - tooltip: p.tooltip, - label: p.label as TranslatedString, - }; + return undefinedIfEmpty({ ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) }); } -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}`, - }; -} - -export 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; +export function getShapeFromFields( + fields: UIFormFieldConfig[], +): Array<UIHandlerId> { + const shape: Array<UIHandlerId> = []; + fields.forEach((field) => { + if ("id" in field.properties) { + // FIXME: this should be a validation when loading the form + // consistency check + if (shape.indexOf(field.properties.id) !== -1) { + throw Error(`already present: ${field.properties.id}`); } + shape.push(field.properties.id); + } else if (field.type === "group") { + Array.prototype.push.apply( + shape, + getShapeFromFields(field.properties.fields), + ); } - // Input Fields - switch (config.type) { - case "array": { - return { - type: "array", - properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties), - labelField: config.properties.labelFieldId, - fields: convertUiField(i18n_, config.properties.fields, form), - }, - } as UIFormField; - } - case "absoluteTime": { - return { - type: "absoluteTime", - properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties), - }, - } as UIFormField; - } - case "amount": { - return { - type: "amount", - properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties), - }, - } as UIFormField; - } - case "choiceHorizontal": { - return { - type: "choiceHorizontal", - properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties), - choices: config.properties.choices, - }, - } as UIFormField; - } - case "choiceStacked": { - return { - type: "choiceStacked", - properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties), - choices: config.properties.choices, - - }, - }as UIFormField; - } - case "file":{ - console.log("ASDASD", config.properties.accept) - return { - type: "file", - properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties), - accept: config.properties.accept, - maxBites: config.properties.maxBytes, - }, - } as UIFormField; - } - case "integer":{ - return { - type: "integer", - properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties), - }, - } as UIFormField; - } - case "selectMultiple":{ - return { - type: "selectMultiple", - properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties), - choices: config.properties.choices, - }, - } as UIFormField; - } - case "selectOne": { - return { - type: "selectOne", - properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties), - choices: config.properties.choices, - }, - } as UIFormField; - } - 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 { - type: "toggle", - properties: { - ...converBaseFieldsProps(i18n_, config.properties), - ...converInputFieldsProps(form, config.properties), - }, - } as UIFormField; + }); + return shape; +} + +export function getRequiredFields( + fields: UIFormFieldConfig[], +): Array<UIHandlerId> { + const shape: Array<UIHandlerId> = []; + fields.forEach((field) => { + if ("id" in field.properties) { + // FIXME: this should be a validation when loading the form + // consistency check + if (shape.indexOf(field.properties.id) !== -1) { + throw Error(`already present: ${field.properties.id}`); } - default: { - assertUnreachable(config); + if (!field.properties.required) { + return; } + shape.push(field.properties.id); + } else if (field.type === "group") { + Array.prototype.push.apply( + shape, + getRequiredFields(field.properties.fields), + ); } }); + return shape; +} +export function validateRequiredFields<FormType>( + errors: FormErrors<FormType> | undefined, + form: object, + fields: Array<UIHandlerId>, +): FormErrors<FormType> | undefined { + let result: FormErrors<FormType> | undefined = errors; + fields.forEach((f) => { + const path = f.split("."); + const v = getValueDeeper(form as any, path); + result = setValueDeeper(result, path, !v ? "required" : undefined); + }); + return result; } diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx index 2f3ee054d..712a1fed9 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx @@ -29,17 +29,23 @@ import { LocalNotificationBanner, RenderAllFieldsByUiConfig, UIHandlerId, + convertUiField, + getConverterById, useExchangeApiContext, useLocalNotificationHandler, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { privatePages } from "../Routing.js"; -import { - useUiFormsContext -} from "../context/ui-forms.js"; +import { useUiFormsContext } from "../context/ui-forms.js"; import { preloadedForms } from "../forms/index.js"; -import { FormErrors, convertUiField, useFormState } from "../hooks/form.js"; +import { + FormErrors, + getRequiredFields, + getShapeFromFields, + useFormState, + validateRequiredFields, +} from "../hooks/form.js"; import { useOfficer } from "../hooks/officer.js"; import { Justification } from "./CaseDetails.js"; import { undefinedIfEmpty } from "./CreateAccount.js"; @@ -101,25 +107,27 @@ export function CaseUpdate({ } const shape: Array<UIHandlerId> = []; + const requiredFields: Array<UIHandlerId> = []; + theForm.config.design.forEach((section) => { - section.fields.forEach((field) => { - if ("id" in field.properties) { - //FIXME: this should be a validation - if (shape.indexOf(field.properties.id) !== -1) { - throw Error(`already present: ${field.properties.id}`) - } - shape.push(field.properties.id); - } - }); + Array.prototype.push.apply(shape, getShapeFromFields(section.fields)); + Array.prototype.push.apply( + requiredFields, + getRequiredFields(section.fields), + ); }); const [form, state] = useFormState<FormType>(shape, initial, (st) => { - const errors = undefinedIfEmpty<FormErrors<FormType>>({ + const partialErrors = undefinedIfEmpty<FormErrors<FormType>>({ state: st.state === undefined ? 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, }); + + const errors = undefinedIfEmpty<FormErrors<FormType> | undefined>( + validateRequiredFields(partialErrors, st, requiredFields), + ); + if (errors === undefined) { return { status: "ok", @@ -127,6 +135,7 @@ export function CaseUpdate({ errors: undefined, }; } + return { status: "fail", result: st as any, @@ -136,6 +145,7 @@ export function CaseUpdate({ const validatedForm = state.status !== "ok" ? undefined : state.result; + console.log(state.errors); const submitHandler = validatedForm === undefined ? undefined @@ -180,7 +190,6 @@ export function CaseUpdate({ } }, ); - return ( <Fragment> <LocalNotificationBanner notification={notification} /> @@ -207,7 +216,12 @@ 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(i18n, section.fields, form)} + fields={convertUiField( + i18n, + section.fields, + form, + getConverterById, + )} /> </div> </div> @@ -269,4 +283,3 @@ export function SelectForm({ account }: { account: string }) { </div> ); } - diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx index 7b848487a..3a7fc89f2 100644 --- a/packages/aml-backoffice-ui/src/pages/Cases.tsx +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -25,6 +25,7 @@ import { InputChoiceHorizontal, Loading, UIHandlerId, + amlStateConverter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -35,7 +36,6 @@ import { privatePages } from "../Routing.js"; import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js"; import { undefinedIfEmpty } from "./CreateAccount.js"; import { Officer } from "./Officer.js"; -import { amlStateConverter } from "../utils/converter.js"; type FormType = { state: TalerExchangeApi.AmlState; diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx index f4904933b..87310aa27 100644 --- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx +++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx @@ -89,7 +89,8 @@ function createFormValidator( }; } -export function undefinedIfEmpty<T extends object>(obj: T): T | undefined { +export function undefinedIfEmpty<T extends object | undefined>(obj: T): T | undefined { + if (obj === undefined) return undefined; return Object.keys(obj).some( (k) => (obj as Record<string, T>)[k] !== undefined, ) diff --git a/packages/web-util/src/forms/Group.tsx b/packages/web-util/src/forms/Group.tsx index d5626be1d..f63fa4a9b 100644 --- a/packages/web-util/src/forms/Group.tsx +++ b/packages/web-util/src/forms/Group.tsx @@ -1,8 +1,11 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { VNode, h } from "preact"; import { LabelWithTooltipMaybeRequired, RenderAddon } from "./InputLine.js"; -import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js"; -import { Addon } from "./FormProvider.js"; +import { RenderAllFieldsByUiConfig, UIFormField, convertUiField } from "./forms.js"; +import { Addon, FormProvider } from "./FormProvider.js"; +import { useField } from "./useField.js"; +import { useTranslationContext } from "../index.browser.js"; +import { getConverterById } from "./converter.js"; interface Props { label: TranslatedString; @@ -32,7 +35,9 @@ export function Group({ </p> )} <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-2 sm:grid-cols-6"> - <RenderAllFieldsByUiConfig fields={fields} /> + <RenderAllFieldsByUiConfig + fields={fields} + /> </div> </div> ); diff --git a/packages/aml-backoffice-ui/src/utils/converter.ts b/packages/web-util/src/forms/converter.ts index 187a5412f..3a522bf7e 100644 --- a/packages/aml-backoffice-ui/src/utils/converter.ts +++ b/packages/web-util/src/forms/converter.ts @@ -20,8 +20,8 @@ import { Amounts, TalerExchangeApi, } from "@gnu-taler/taler-util"; -import { StringConverter } from "@gnu-taler/web-util/browser"; import { format, parse } from "date-fns"; +import { StringConverter } from "./FormProvider.js"; export const amlStateConverter = { toStringUI: stringifyAmlState, diff --git a/packages/web-util/src/forms/forms.ts b/packages/web-util/src/forms/forms.ts index 6bda2f674..4bd6b4924 100644 --- a/packages/web-util/src/forms/forms.ts +++ b/packages/web-util/src/forms/forms.ts @@ -13,7 +13,10 @@ import { InputSelectOne } from "./InputSelectOne.js"; import { InputText } from "./InputText.js"; import { InputTextArea } from "./InputTextArea.js"; import { InputToggle } from "./InputToggle.js"; - +import { Addon, StringConverter, UIFieldHandler } from "./FormProvider.js"; +import { InternationalizationAPI, UIFieldBaseDescription } from "../index.browser.js"; +import { assertUnreachable, TranslatedString } from "@gnu-taler/taler-util"; +import {UIFormFieldBaseConfig, UIFormFieldConfig} from "./ui-form.js"; /** * Constrain the type with the ui props */ @@ -142,3 +145,227 @@ export function RenderAllFieldsByUiConfig({ // InputChoiceHorizontal: res.InputChoiceHorizontal(), // }; // } + +/** + * convert field configuration to render function + * + * @param i18n_ + * @param fieldConfig + * @param form + * @returns + */ +export function convertUiField( + i18n_: InternationalizationAPI, + fieldConfig: UIFormFieldConfig[], + form: object, + getConverterById: GetConverterById, +): 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, getConverterById), + }, + }; + return resp; + } + } + // Input Fields + switch (config.type) { + case "array": { + return { + type: "array", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + labelField: config.properties.labelFieldId, + fields: convertUiField(i18n_, config.properties.fields, form, getConverterById), + }, + } as UIFormField; + } + case "absoluteTime": { + return { + type: "absoluteTime", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + }, + } as UIFormField; + } + case "amount": { + return { + type: "amount", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + }, + } as UIFormField; + } + case "choiceHorizontal": { + return { + type: "choiceHorizontal", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + choices: config.properties.choices, + }, + } as UIFormField; + } + case "choiceStacked": { + return { + type: "choiceStacked", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + choices: config.properties.choices, + + }, + }as UIFormField; + } + case "file":{ + return { + type: "file", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + accept: config.properties.accept, + maxBites: config.properties.maxBytes, + }, + } as UIFormField; + } + case "integer":{ + return { + type: "integer", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + }, + } as UIFormField; + } + case "selectMultiple":{ + return { + type: "selectMultiple", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + choices: config.properties.choices, + }, + } as UIFormField; + } + case "selectOne": { + return { + type: "selectOne", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + choices: config.properties.choices, + }, + } as UIFormField; + } + case "text": { + return { + type: "text", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + }, + } as UIFormField; + } + case "textArea": { + return { + type: "text", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + }, + } as UIFormField; + } + case "toggle": { + return { + type: "toggle", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + }, + } as UIFormField; + } + default: { + assertUnreachable(config); + } + } + }); +} + + + +function getAddonById(_id: string | undefined): Addon { + return undefined!; +} + + +type GetConverterById = ( + id: string | undefined, + config: unknown, +) => StringConverter<unknown>; + + +function converInputFieldsProps( + form: object, + p: UIFormFieldBaseConfig, + getConverterById: GetConverterById, +) { + return { + converter: getConverterById(p.converterId, p), + handler: getValueDeeper2(form, p.id.split(".")), + name: p.name, + required: p.required, + disabled: p.disabled, + help: p.help, + placeholder: p.placeholder, + tooltip: p.tooltip, + label: p.label as TranslatedString, + }; +} + +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 getValueDeeper2( + object: Record<string, any>, + names: string[], +): UIFieldHandler { + if (names.length === 0) return object as UIFieldHandler; + const [head, ...rest] = names; + if (!head) { + return getValueDeeper2(object, rest); + } + if (object === undefined) { + throw Error("handler not found"); + } + return getValueDeeper2(object[head], rest); +} + + diff --git a/packages/web-util/src/forms/index.ts b/packages/web-util/src/forms/index.ts index 8c6c23ec5..7320c70d0 100644 --- a/packages/web-util/src/forms/index.ts +++ b/packages/web-util/src/forms/index.ts @@ -20,5 +20,6 @@ export * from "./InputToggle.js" export * from "./TimePicker.js" export * from "./forms.js" export * from "./ui-form.js" +export * from "./converter.js" export * from "./useField.js" diff --git a/packages/web-util/src/forms/ui-form.ts b/packages/web-util/src/forms/ui-form.ts index 9bbc2e96c..212efd506 100644 --- a/packages/web-util/src/forms/ui-form.ts +++ b/packages/web-util/src/forms/ui-form.ts @@ -262,6 +262,7 @@ const codecForUIFormFieldArrayConfig = (): Codec< > => codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigArray["properties"]>() .property("labelFieldId", codecForUiFieldId()) + // eslint-disable-next-line @typescript-eslint/no-use-before-define .property("fields", codecForList(codecForUiFormField())) .build("UIFormFieldConfigArray.properties"); @@ -328,6 +329,7 @@ const codecForUIFormFieldWithFieldsConfig = (): Codec< codecForUIFormFieldBaseDescriptionTemplate< UIFormFieldConfigGroup["properties"] >() + // eslint-disable-next-line @typescript-eslint/no-use-before-define .property("fields", codecForList(codecForUiFormField())) .build("UIFormFieldConfigGroup.properties"); |