aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/aml-backoffice-ui/src/forms.json336
-rw-r--r--packages/aml-backoffice-ui/src/hooks/form.ts291
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx51
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx2
-rw-r--r--packages/aml-backoffice-ui/src/pages/CreateAccount.tsx3
-rw-r--r--packages/web-util/src/forms/Group.tsx11
-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.ts229
-rw-r--r--packages/web-util/src/forms/index.ts1
-rw-r--r--packages/web-util/src/forms/ui-form.ts2
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");