[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-wallet-core] 02/04: use the new form implementation in aml
From: |
gnunet |
Subject: |
[taler-wallet-core] 02/04: use the new form implementation in aml |
Date: |
Fri, 10 Jan 2025 19:54:28 +0100 |
This is an automated email from the git hooks/post-receive script.
sebasjm pushed a commit to branch master
in repository wallet-core.
commit b1394a32c8245d05ef04e598705cd94ebaf68ccd
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Fri Jan 10 15:42:46 2025 -0300
use the new form implementation in aml
---
packages/aml-backoffice-ui/src/forms/simplest.ts | 6 +-
packages/aml-backoffice-ui/src/hooks/form.ts | 204 ++++++++++----------
.../src/pages/AmlDecisionRequestWizard.tsx | 1 -
.../aml-backoffice-ui/src/pages/CaseDetails.tsx | 118 +++++-------
.../aml-backoffice-ui/src/pages/CaseUpdate.tsx | 166 +---------------
.../aml-backoffice-ui/src/pages/CreateAccount.tsx | 102 ++++------
packages/aml-backoffice-ui/src/pages/Search.tsx | 209 +++++----------------
.../src/pages/ShowConsolidated.tsx | 59 +-----
.../aml-backoffice-ui/src/pages/UnlockAccount.tsx | 37 ++--
packages/web-util/src/forms/forms-ui.tsx | 5 +
10 files changed, 275 insertions(+), 632 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts
b/packages/aml-backoffice-ui/src/forms/simplest.ts
index 215b0ba51..2abf5699f 100644
--- a/packages/aml-backoffice-ui/src/forms/simplest.ts
+++ b/packages/aml-backoffice-ui/src/forms/simplest.ts
@@ -15,15 +15,15 @@
*/
import type {
- DoubleColumnForm,
+ DoubleColumnFormDesign,
DoubleColumnFormSection,
InternationalizationAPI,
UIHandlerId,
} from "@gnu-taler/web-util/browser";
-export const v1 = (i18n: InternationalizationAPI): DoubleColumnForm => ({
+export const v1 = (i18n: InternationalizationAPI): DoubleColumnFormDesign => ({
type: "double-column" as const,
- design: [
+ sections: [
{
title: i18n.str`Simple form`,
fields: [
diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts
b/packages/aml-backoffice-ui/src/hooks/form.ts
index 375dbb190..6305e4cf5 100644
--- a/packages/aml-backoffice-ui/src/hooks/form.ts
+++ b/packages/aml-backoffice-ui/src/hooks/form.ts
@@ -82,59 +82,51 @@ export type FormStatus<T> =
errors: FormErrors<T>;
};
-function constructFormHandler<T>(
- shape: Array<UIHandlerId>,
- form: RecursivePartial<FormValues<T>>,
- updateForm: (d: RecursivePartial<FormValues<T>>) => void,
- errors: FormErrors<T> | undefined,
-): FormHandler<T> {
- const handler = shape.reduce((handleForm, fieldId) => {
- 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 field: UIFieldHandler = {
- error: currentError,
- value: currentValue,
- onChange: updater,
- state: {}, //FIXME: add the state of the field (hidden, )
- };
-
- return setValueDeeper(handleForm, path, field);
- }, {} as FormHandler<T>);
-
- return handler;
-}
-
-/**
- * FIXME: Consider sending this to web-utils
- *
- *
- * @param defaultValue
- * @param check
- * @returns
- */
-export function useFormState<T>(
- shape: Array<UIHandlerId>,
- defaultValue: RecursivePartial<FormValues<T>>,
- check: (f: RecursivePartial<FormValues<T>>) => FormStatus<T>,
-): { handler: FormHandler<T>; status: FormStatus<T> } {
- const [form, updateForm] =
- useState<RecursivePartial<FormValues<T>>>(defaultValue);
-
- const status = check(form);
- const handler = constructFormHandler(shape, form, updateForm, status.errors);
-
- return { handler, status };
-}
+// function constructFormHandler<T>(
+// shape: Array<UIHandlerId>,
+// form: RecursivePartial<FormValues<T>>,
+// updateForm: (d: RecursivePartial<FormValues<T>>) => void,
+// errors: FormErrors<T> | undefined,
+// ): FormHandler<T> {
+// const handler = shape.reduce((handleForm, fieldId) => {
+// 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 field: UIFieldHandler = {
+// error: currentError,
+// value: currentValue,
+// onChange: updater,
+// state: {}, //FIXME: add the state of the field (hidden, )
+// };
+
+// return setValueDeeper(handleForm, path, field);
+// }, {} as FormHandler<T>);
+
+// return handler;
+// }
+
+// export function useFormState<T>(
+// shape: Array<UIHandlerId>,
+// defaultValue: RecursivePartial<FormValues<T>>,
+// check: (f: RecursivePartial<FormValues<T>>) => FormStatus<T>,
+// ): { handler: FormHandler<T>; status: FormStatus<T> } {
+// const [form, updateForm] =
+// useState<RecursivePartial<FormValues<T>>>(defaultValue);
+
+// const status = check(form);
+// const handler = constructFormHandler(shape, form, updateForm,
status.errors);
+
+// return { handler, status };
+// }
interface Tree<T> extends Record<string, Tree<T> | T> {}
@@ -169,56 +161,56 @@ export function setValueDeeper(object: any, names:
string[], value: any): any {
});
}
-export function getShapeFromFields(
- fields: UIFormElementConfig[],
-): Array<UIHandlerId> {
- const shape: Array<UIHandlerId> = [];
- fields.forEach((field) => {
- if ("id" in field) {
- // FIXME: this should be a validation when loading the form
- // consistency check
- if (shape.indexOf(field.id) !== -1) {
- throw Error(`already present: ${field.id}`);
- }
- shape.push(field.id);
- } else if (field.type === "group") {
- Array.prototype.push.apply(shape, getShapeFromFields(field.fields));
- }
- });
- return shape;
-}
-
-export function getRequiredFields(
- fields: UIFormElementConfig[],
-): Array<UIHandlerId> {
- const shape: Array<UIHandlerId> = [];
- fields.forEach((field) => {
- if ("id" in field) {
- // FIXME: this should be a validation when loading the form
- // consistency check
- if (shape.indexOf(field.id) !== -1) {
- throw Error(`already present: ${field.id}`);
- }
- if (!field.required) {
- return;
- }
- shape.push(field.id);
- } else if (field.type === "group") {
- Array.prototype.push.apply(shape, getRequiredFields(field.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;
-}
+// export function getShapeFromFields(
+// fields: UIFormElementConfig[],
+// ): Array<UIHandlerId> {
+// const shape: Array<UIHandlerId> = [];
+// fields.forEach((field) => {
+// if ("id" in field) {
+// // FIXME: this should be a validation when loading the form
+// // consistency check
+// if (shape.indexOf(field.id) !== -1) {
+// throw Error(`already present: ${field.id}`);
+// }
+// shape.push(field.id);
+// } else if (field.type === "group") {
+// Array.prototype.push.apply(shape, getShapeFromFields(field.fields));
+// }
+// });
+// return shape;
+// }
+
+// export function getRequiredFields(
+// fields: UIFormElementConfig[],
+// ): Array<UIHandlerId> {
+// const shape: Array<UIHandlerId> = [];
+// fields.forEach((field) => {
+// if ("id" in field) {
+// // FIXME: this should be a validation when loading the form
+// // consistency check
+// if (shape.indexOf(field.id) !== -1) {
+// throw Error(`already present: ${field.id}`);
+// }
+// if (!field.required) {
+// return;
+// }
+// shape.push(field.id);
+// } else if (field.type === "group") {
+// Array.prototype.push.apply(shape, getRequiredFields(field.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/AmlDecisionRequestWizard.tsx
b/packages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx
index 47ed5deec..8491abe0b 100644
--- a/packages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx
+++ b/packages/aml-backoffice-ui/src/pages/AmlDecisionRequestWizard.tsx
@@ -25,7 +25,6 @@ import {
useCurrentDecisionRequest,
} from "../hooks/decision-request.js";
import { ShowDecisionLimitInfo } from "./CaseDetails.js";
-import { useFormState } from "../hooks/form.js";
export type WizardSteps =
| "rules" // define the limits
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index 02146c9e7..46abb19be 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -18,64 +18,55 @@ import {
AmlDecisionRequest,
AmountJson,
Amounts,
+ assertUnreachable,
+ buildCodecForObject,
Codec,
+ codecForNumber,
+ codecForString,
+ codecOptional,
CurrencySpecification,
HttpStatusCode,
KycRule,
- LegitimizationRuleSet,
OperationFail,
OperationOk,
- PaytoString,
TalerError,
TalerErrorDetail,
TalerExchangeApi,
TranslatedString,
- assertUnreachable,
- buildCodecForObject,
- codecForNumber,
- codecForString,
- codecOptional,
} from "@gnu-taler/taler-util";
import {
Attention,
Button,
- convertUiField,
CopyButton,
- DefaultForm,
- FormConfiguration,
+ FormDesign,
FormMetadata,
+ FormUI,
getConverterById,
InternationalizationAPI,
Loading,
LocalNotificationBanner,
RenderAllFieldsByUiConfig,
- ShowInputErrorLabel,
Time,
UIFormElementConfig,
UIHandlerId,
useExchangeApiContext,
+ useForm,
useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format, formatDuration, intervalToDuration } from "date-fns";
-import { Fragment, Ref, VNode, h } from "preact";
+import { Fragment, h, Ref, VNode } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
import { useUiFormsContext } from "../context/ui-forms.js";
import { preloadedForms } from "../forms/index.js";
import { useAccountInformation, useServerMeasures } from "../hooks/account.js";
+import { DecisionRequest } from "../hooks/decision-request.js";
import { useAccountDecisions } from "../hooks/decisions.js";
-import { ShowConsolidated } from "./ShowConsolidated.js";
import { useOfficer } from "../hooks/officer.js";
-import { getShapeFromFields, useFormState } from "../hooks/form.js";
-import { privatePages } from "../Routing.js";
import { CurrentMeasureTable, MeasureInfo } from "./MeasuresTable.js";
import { Officer } from "./Officer.js";
-import {
- AmlDecisionRequestWizard,
- WizardSteps,
-} from "./AmlDecisionRequestWizard.js";
-import { DecisionRequest } from "../hooks/decision-request.js";
+import { ShowConsolidated } from "./ShowConsolidated.js";
export type AmlEvent =
| AmlFormEvent
@@ -503,22 +494,25 @@ function SubmitNewDecision({
const { lib } = useExchangeApiContext();
const [notification, withErrorHandler] = useLocalNotificationHandler();
- const formDesign: UIFormElementConfig[] = [
- {
- id: "justification" as UIHandlerId,
- type: "textArea",
- required: true,
- label: i18n.str`Justification`,
- },
- ];
+ const formDesign: FormDesign = {
+ type: "single-column",
+ fields: [
+ {
+ id: "justification" as UIHandlerId,
+ type: "textArea",
+ required: true,
+ label: i18n.str`Justification`,
+ },
+ ],
+ };
if (decision.askInformation) {
- formDesign.push({
+ formDesign.fields.push({
type: "caption",
label: i18n.str`Form definition`,
help: i18n.str`The user will need to complete this form.`,
});
- formDesign.push({
+ formDesign.fields.push({
id: "fields" as UIHandlerId,
type: "array",
required: true,
@@ -556,46 +550,35 @@ function SubmitNewDecision({
}
const officer = useOfficer();
const session = officer.state === "ready" ? officer.account : undefined;
- const decisionForm = useFormState<{ justification: string; fields: object }>(
- getShapeFromFields(formDesign),
+ const decisionForm = useForm<{ justification: string; fields: object }>(
+ formDesign,
{ justification: "" },
- (d) => {
- d.justification;
- return {
- status: "ok",
- errors: undefined,
- result: d as any,
- };
- },
);
const customFields = decisionForm.status.result.fields as [
{ name: string; type: string },
];
- const customForm: FormConfiguration | undefined = !decisionForm.status.result
- .fields
- ? undefined
- : {
- type: "double-column",
- design: [
- {
- fields: customFields.map((f) => {
- return {
- id: f.name,
- label: f.name,
- type: f.type,
- } as UIFormElementConfig;
- }),
- title: "Required information",
- },
- ],
- };
+ // const customForm: FormDesign | undefined =
!decisionForm.status.result.fields
+ // ? undefined
+ // : {
+ // type: "double-column",
+ // sections: [
+ // {
+ // fields: customFields.map((f) => {
+ // return {
+ // id: f.name,
+ // label: f.name,
+ // type: f.type,
+ // } as UIFormElementConfig;
+ // }),
+ // title: "Required information",
+ // },
+ // ],
+ // };
const submitHandler =
- decisionForm === undefined ||
- !session ||
- (decision.askInformation && customForm === undefined)
+ decisionForm === undefined || !session || decision.askInformation //&&
customForm === undefined)
? undefined
: withErrorHandler(
() => {
@@ -613,7 +596,7 @@ function SubmitNewDecision({
...decision.request.new_rules.custom_measures,
askMoreInfo: {
context: {
- form: customForm,
+ // form: customForm,
},
// check of type form, it will use the officer defined form
check_name: "askContext",
@@ -659,16 +642,7 @@ function SubmitNewDecision({
autoCapitalize="none"
autoCorrect="off"
>
- <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
- <RenderAllFieldsByUiConfig
- fields={convertUiField(
- i18n,
- formDesign,
- decisionForm.handler,
- getConverterById,
- )}
- />
- </div>
+ <FormUI design={formDesign} handler={decisionForm.handler} />
<div class="mt-6 flex items-center justify-end gap-x-6">
<button
diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
index 1bd50141f..afb6813ee 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
@@ -25,13 +25,11 @@ import {
import {
Button,
FormMetadata,
+ FormUI,
InternationalizationAPI,
LocalNotificationBanner,
- RenderAllFieldsByUiConfig,
- UIHandlerId,
- convertUiField,
- getConverterById,
useExchangeApiContext,
+ useForm,
useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
@@ -39,16 +37,8 @@ import { Fragment, VNode, h } from "preact";
import { privatePages } from "../Routing.js";
import { useUiFormsContext } from "../context/ui-forms.js";
import { preloadedForms } from "../forms/index.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";
import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
function searchForm(
@@ -106,59 +96,10 @@ export function CaseUpdate({
return <div>form with id {formId} not found</div>;
}
- const shape: Array<UIHandlerId> = [];
- const requiredFields: Array<UIHandlerId> = [];
-
- switch (theForm.config.type) {
- case "double-column": {
- theForm.config.design.forEach((section) => {
- Array.prototype.push.apply(shape, getShapeFromFields(section.fields));
- Array.prototype.push.apply(
- requiredFields,
- getRequiredFields(section.fields),
- );
- });
- break;
- }
- case "single-column": {
- Array.prototype.push.apply(
- shape,
- getShapeFromFields(theForm.config.fields),
- );
- Array.prototype.push.apply(
- requiredFields,
- getRequiredFields(theForm.config.fields),
- );
- }
- }
-
- const { handler, status } = useFormState<FormType>(shape, initial, (st) => {
- 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,
- });
-
- const errors = undefinedIfEmpty<FormErrors<FormType> | undefined>(
- validateRequiredFields(partialErrors, st, requiredFields),
- );
+ const form = useForm<FormType>(theForm.config, initial);
- if (errors === undefined) {
- return {
- status: "ok",
- result: st as any,
- errors: undefined,
- };
- }
-
- return {
- status: "fail",
- result: st as any,
- errors,
- };
- });
-
- const validatedForm = status.status !== "ok" ? undefined : status.result;
+ const validatedForm =
+ form.status.status !== "ok" ? undefined : form.status.result;
const submitHandler =
validatedForm === undefined
@@ -210,106 +151,13 @@ export function CaseUpdate({
}
},
);
+
return (
<Fragment>
<LocalNotificationBanner notification={notification} />
<div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
- {(function () {
- switch (theForm.config.type) {
- case "double-column": {
- return theForm.config.design.map((section, i) => {
- if (!section) return <Fragment />;
- return (
- <div
- key={i}
- class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5
md:grid-cols-3"
- >
- <div class="px-4 sm:px-0">
- <h2 class="text-base font-semibold leading-7
text-gray-900">
- {section.title}
- </h2>
- {section.description && (
- <p class="mt-1 text-sm leading-6 text-gray-600">
- {section.description}
- </p>
- )}
- </div>
- <div class="bg-white shadow-sm ring-1 ring-gray-900/5
rounded-md md:col-span-2">
- <div class="p-3">
- <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,
- handler,
- getConverterById,
- )}
- />
- </div>
- </div>
- </div>
- </div>
- );
- });
- }
- case "single-column": {
- return (
- <div class="bg-white shadow-sm ring-1 ring-gray-900/5
rounded-md md:col-span-2">
- <div class="p-3">
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8
sm:grid-cols-6">
- <RenderAllFieldsByUiConfig
- fields={convertUiField(
- i18n,
- theForm.config.fields,
- handler,
- getConverterById,
- )}
- />
- </div>
- </div>
- </div>
- );
- }
- }
- })()}
+ <FormUI design={theForm.config} handler={form} />
</div>
- {/* {theForm.config.design.map((section, i) => {
- if (!section) return <Fragment />;
- return (
- <div
- key={i}
- class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3"
- >
- <div class="px-4 sm:px-0">
- <h2 class="text-base font-semibold leading-7 text-gray-900">
- {section.title}
- </h2>
- {section.description && (
- <p class="mt-1 text-sm leading-6 text-gray-600">
- {section.description}
- </p>
- )}
- </div>
- <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md
md:col-span-2">
- <div class="p-3">
- <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,
- handler,
- getConverterById,
- )}
- />
- </div>
- </div>
- </div>
- </div>
- );
- })}
- </div> */}
<div class="mt-6 flex items-center justify-end gap-x-6">
<a
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
index 328d8459b..ce409458a 100644
--- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -15,10 +15,13 @@
*/
import {
Button,
+ FormDesign,
+ FormUI,
InputLine,
InternationalizationAPI,
LocalNotificationBanner,
UIHandlerId,
+ useForm,
useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
@@ -28,7 +31,6 @@ import {
FormStatus,
FormValues,
RecursivePartial,
- useFormState,
} from "../hooks/form.js";
import { useOfficer } from "../hooks/officer.js";
import { usePreferences } from "../hooks/preferences.js";
@@ -43,8 +45,8 @@ function createFormValidator(
) {
return function check(
state: RecursivePartial<FormValues<FormType>>,
- ): FormStatus<FormType> {
- const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ ): FormErrors<FormType> | undefined {
+ return undefinedIfEmpty<FormErrors<FormType>>({
password: !state.password
? i18n.str`required`
: allowInsecurePassword
@@ -65,27 +67,6 @@ function createFormValidator(
? i18n.str`doesn't match`
: undefined,
});
-
- if (errors === undefined) {
- const result: FormType = {
- password: state.password!,
- repeat: state.repeat!,
- };
- return {
- status: "ok",
- result,
- errors,
- };
- }
- const result: RecursivePartial<FormType> = {
- password: state.password,
- repeat: state.repeat,
- };
- return {
- status: "fail",
- result,
- errors,
- };
};
}
@@ -100,6 +81,24 @@ export function undefinedIfEmpty<T extends object |
undefined>(
: undefined;
}
+const createAccountForm = (i18n: InternationalizationAPI): FormDesign => ({
+ type: "single-column",
+ fields: [
+ {
+ id: "password" as UIHandlerId,
+ type: "text",
+ label: i18n.str`Password`,
+ required: true,
+ },
+ {
+ id: "repeat" as UIHandlerId,
+ type: "text",
+ label: i18n.str`Repeat password`,
+ required: true,
+ },
+ ],
+});
+
export function CreateAccount(): VNode {
const { i18n } = useTranslationContext();
const [settings] = usePreferences();
@@ -107,8 +106,10 @@ export function CreateAccount(): VNode {
const [notification, withErrorHandler] = useLocalNotificationHandler();
- const { handler, status } = useFormState<FormType>(
- [".password", ".repeat"] as Array<UIHandlerId>,
+ const design = createAccountForm(i18n);
+
+ const { handler, status } = useForm<FormType>(
+ design,
{
password: undefined,
repeat: undefined,
@@ -134,47 +135,16 @@ export function CreateAccount(): VNode {
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
- <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
- <form
- class="space-y-6"
- noValidate
- onSubmit={(e) => {
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
+ <FormUI design={design} handler={handler} />
+ <div class="mt-8">
+ <Button
+ type="submit"
+ disabled={!createAccountHandler}
+ class="disabled:opacity-50 disabled:cursor-default flex w-full
justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold
leading-6 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"
+ handler={createAccountHandler}
>
- <div class="mt-2">
- <InputLine<FormType, "password">
- label={i18n.str`Password`}
- name="password"
- type="password"
- required
- handler={handler.password}
- />
- </div>
-
- <div class="mt-2">
- <InputLine<FormType, "repeat">
- label={i18n.str`Repeat password`}
- name="repeat"
- type="password"
- required
- handler={handler.repeat}
- />
- </div>
-
- <div class="mt-8">
- <Button
- type="submit"
- disabled={!createAccountHandler}
- class="disabled:opacity-50 disabled:cursor-default flex w-full
justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold
leading-6 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"
- handler={createAccountHandler}
- >
- <i18n.Translate>Create</i18n.Translate>
- </Button>
- </div>
- </form>
+ <i18n.Translate>Create</i18n.Translate>
+ </Button>
</div>
</div>
</div>
diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx
b/packages/aml-backoffice-ui/src/pages/Search.tsx
index 1b2ea1ea4..8c7912dea 100644
--- a/packages/aml-backoffice-ui/src/pages/Search.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Search.tsx
@@ -29,8 +29,9 @@ import {
} from "@gnu-taler/taler-util";
import {
Attention,
- convertUiField,
encodeCrockForURI,
+ FormDesign,
+ FormUI,
getConverterById,
InternationalizationAPI,
Loading,
@@ -39,6 +40,7 @@ import {
UIFormElementConfig,
UIHandlerId,
useExchangeApiContext,
+ useForm,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
@@ -49,9 +51,7 @@ import {
FormErrors,
FormStatus,
FormValues,
- getShapeFromFields,
RecursivePartial,
- useFormState,
} from "../hooks/form.js";
import { useOfficer } from "../hooks/officer.js";
import { privatePages } from "../Routing.js";
@@ -65,8 +65,12 @@ export function Search() {
const [paytoUri, setPayto] = useState<PaytoUri | undefined>(undefined);
- const paytoForm = useFormState(
- getShapeFromFields(paytoTypeField(i18n)),
+ const design: FormDesign = {
+ type: "single-column",
+ fields: paytoTypeField(i18n),
+ };
+ const paytoForm = useForm<FormPayto>(
+ design,
{ paytoType: "iban" },
createFormValidator(i18n),
);
@@ -89,16 +93,7 @@ export function Search() {
autoCapitalize="none"
autoCorrect="off"
>
- <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
- <RenderAllFieldsByUiConfig
- fields={convertUiField(
- i18n,
- paytoTypeField(i18n),
- paytoForm.handler,
- getConverterById,
- )}
- />
- </div>
+ <FormUI design={design} handler={paytoForm.handler} />
</form>
{(function () {
@@ -310,9 +305,12 @@ function XTalerBankForm({
onSearch: (p: PaytoUri | undefined) => void;
}): VNode {
const { i18n } = useTranslationContext();
- const fields = talerBankFields(i18n);
- const form = useFormState(
- getShapeFromFields(fields),
+ const design: FormDesign = {
+ type: "single-column",
+ fields: talerBankFields(i18n),
+ };
+ const form = useForm<PaytoUriTalerBankForm>(
+ design,
{},
createTalerBankPaytoValidator(i18n),
);
@@ -335,11 +333,8 @@ function XTalerBankForm({
autoCapitalize="none"
autoCorrect="off"
>
- <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
- <RenderAllFieldsByUiConfig
- fields={convertUiField(i18n, fields, form.handler, getConverterById)}
- />
- </div>
+ <FormUI design={design} handler={form.handler} />
+
<button
disabled={form.status.status === "fail"}
class="disabled:bg-gray-100 disabled:text-gray-500 m-4 rounded-md
w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm
hover:bg-indigo-700"
@@ -356,9 +351,12 @@ function IbanForm({
onSearch: (p: PaytoUri | undefined) => void;
}): VNode {
const { i18n } = useTranslationContext();
- const fields = ibanFields(i18n);
- const form = useFormState(
- getShapeFromFields(fields),
+ const design: FormDesign = {
+ type: "single-column",
+ fields: ibanFields(i18n),
+ };
+ const form = useForm<PaytoUriIBANForm>(
+ design,
{},
createIbanPaytoValidator(i18n),
);
@@ -377,11 +375,8 @@ function IbanForm({
autoCapitalize="none"
autoCorrect="off"
>
- <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
- <RenderAllFieldsByUiConfig
- fields={convertUiField(i18n, fields, form.handler, getConverterById)}
- />
- </div>
+ <FormUI design={design} handler={form.handler} />
+
<button
disabled={form.status.status === "fail"}
class="disabled:bg-gray-100 disabled:text-gray-500 m-4 rounded-md
w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm
hover:bg-indigo-700"
@@ -399,9 +394,12 @@ function WalletForm({
}): VNode {
const { i18n } = useTranslationContext();
const { config } = useExchangeApiContext();
- const fields = walletFields(i18n);
- const form = useFormState(
- getShapeFromFields(fields),
+ const design: FormDesign = {
+ type: "single-column",
+ fields: walletFields(i18n),
+ };
+ const form = useForm<PaytoUriTalerForm>(
+ design,
{
exchange: getURLHostnamePortPath(config.keys.base_url),
},
@@ -426,11 +424,8 @@ function WalletForm({
autoCapitalize="none"
autoCorrect="off"
>
- <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
- <RenderAllFieldsByUiConfig
- fields={convertUiField(i18n, fields, form.handler, getConverterById)}
- />
- </div>
+ <FormUI design={design} handler={form.handler} />
+
<button
disabled={form.status.status === "fail"}
class="disabled:bg-gray-100 disabled:text-gray-500 m-4 rounded-md
w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm
hover:bg-indigo-700"
@@ -448,9 +443,12 @@ function GenericForm({
onSearch: (p: PaytoUri | undefined) => void;
}): VNode {
const { i18n } = useTranslationContext();
- const fields = genericFields(i18n);
- const form = useFormState(
- getShapeFromFields(fields),
+ const design: FormDesign = {
+ type: "single-column",
+ fields: genericFields(i18n),
+ };
+ const form = useForm<PaytoUriGenericForm>(
+ design,
{},
createGenericPaytoValidator(i18n),
);
@@ -468,17 +466,13 @@ function GenericForm({
autoCapitalize="none"
autoCorrect="off"
>
- <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
- <RenderAllFieldsByUiConfig
- fields={convertUiField(i18n, fields, form.handler, getConverterById)}
- />
- </div>
+ <FormUI design={design} handler={form.handler} />
<button
disabled={form.status.status === "fail"}
class="disabled:bg-gray-100 disabled:text-gray-500 m-4 rounded-md
w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm
hover:bg-indigo-700"
onClick={() => onSearch(paytoUri)}
>
- Search
+ <i18n.Translate>Search</i18n.Translate>
</button>
</form>
);
@@ -491,29 +485,10 @@ interface FormPayto {
function createFormValidator(i18n: InternationalizationAPI) {
return function check(
state: RecursivePartial<FormValues<FormPayto>>,
- ): FormStatus<FormPayto> {
- const errors = undefinedIfEmpty<FormErrors<FormPayto>>({
+ ): FormErrors<FormPayto> | undefined {
+ return undefinedIfEmpty<FormErrors<FormPayto>>({
paytoType: !state?.paytoType ? i18n.str`required` : undefined,
});
-
- if (errors === undefined) {
- const result: FormPayto = {
- paytoType: state.paytoType! as any,
- };
- return {
- status: "ok",
- result,
- errors,
- };
- }
- const result: RecursivePartial<FormPayto> = {
- paytoType: state?.paytoType,
- };
- return {
- status: "fail",
- result,
- errors,
- };
};
}
@@ -524,33 +499,14 @@ interface PaytoUriGenericForm {
function createGenericPaytoValidator(i18n: InternationalizationAPI) {
return function check(
state: RecursivePartial<FormValues<PaytoUriGenericForm>>,
- ): FormStatus<PaytoUriGenericForm> {
- const errors = undefinedIfEmpty<FormErrors<PaytoUriGenericForm>>({
+ ): FormErrors<PaytoUriGenericForm> | undefined {
+ return undefinedIfEmpty<FormErrors<PaytoUriGenericForm>>({
payto: !state.payto
? i18n.str`required`
: parsePaytoUri(state.payto) === undefined
? i18n.str`invalid`
: undefined,
});
-
- if (errors === undefined) {
- const result: PaytoUriGenericForm = {
- payto: state.payto! as any,
- };
- return {
- status: "ok",
- result,
- errors,
- };
- }
- const result: RecursivePartial<PaytoUriGenericForm> = {
- // targetType: state.iban
- };
- return {
- status: "fail",
- result,
- errors,
- };
};
}
@@ -561,29 +517,10 @@ interface PaytoUriIBANForm {
function createIbanPaytoValidator(i18n: InternationalizationAPI) {
return function check(
state: RecursivePartial<FormValues<PaytoUriIBANForm>>,
- ): FormStatus<PaytoUriIBANForm> {
- const errors = undefinedIfEmpty<FormErrors<PaytoUriIBANForm>>({
+ ): FormErrors<PaytoUriIBANForm> | undefined {
+ return undefinedIfEmpty<FormErrors<PaytoUriIBANForm>>({
account: !state.account ? i18n.str`required` : undefined,
});
-
- if (errors === undefined) {
- const result: PaytoUriIBANForm = {
- account: state.account!,
- };
- return {
- status: "ok",
- result,
- errors,
- };
- }
- const result: RecursivePartial<PaytoUriIBANForm> = {
- account: state.account,
- };
- return {
- status: "fail",
- result,
- errors,
- };
};
}
interface PaytoUriTalerBankForm {
@@ -593,32 +530,11 @@ interface PaytoUriTalerBankForm {
function createTalerBankPaytoValidator(i18n: InternationalizationAPI) {
return function check(
state: RecursivePartial<FormValues<PaytoUriTalerBankForm>>,
- ): FormStatus<PaytoUriTalerBankForm> {
- const errors = undefinedIfEmpty<FormErrors<PaytoUriTalerBankForm>>({
+ ): FormErrors<PaytoUriTalerBankForm> | undefined {
+ return undefinedIfEmpty<FormErrors<PaytoUriTalerBankForm>>({
account: !state.account ? i18n.str`required` : undefined,
hostname: !state.hostname ? i18n.str`required` : undefined,
});
-
- if (errors === undefined) {
- const result: PaytoUriTalerBankForm = {
- account: state.account!,
- hostname: state.hostname!,
- };
- return {
- status: "ok",
- result,
- errors,
- };
- }
- const result: RecursivePartial<PaytoUriTalerBankForm> = {
- account: state.account,
- hostname: state.hostname,
- };
- return {
- status: "fail",
- result,
- errors,
- };
};
}
@@ -629,8 +545,8 @@ interface PaytoUriTalerForm {
function createTalerPaytoValidator(i18n: InternationalizationAPI) {
return function check(
state: RecursivePartial<FormValues<PaytoUriTalerForm>>,
- ): FormStatus<PaytoUriTalerForm> {
- const errors = undefinedIfEmpty<FormErrors<PaytoUriTalerForm>>({
+ ): FormErrors<PaytoUriTalerForm> | undefined {
+ return undefinedIfEmpty<FormErrors<PaytoUriTalerForm>>({
exchange: !state.exchange ? i18n.str`required` : undefined,
reservePub: !state.reservePub
? i18n.str`required`
@@ -638,27 +554,6 @@ function createTalerPaytoValidator(i18n:
InternationalizationAPI) {
? i18n.str`Should be 16 charaters`
: undefined,
});
-
- if (errors === undefined) {
- const result: PaytoUriTalerForm = {
- exchange: state.exchange!,
- reservePub: state.reservePub!,
- };
- return {
- status: "ok",
- result,
- errors,
- };
- }
- const result: RecursivePartial<PaytoUriTalerForm> = {
- exchange: state.exchange,
- reservePub: state.reservePub,
- };
- return {
- status: "fail",
- result,
- errors,
- };
};
}
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
index b7812ed49..50426095a 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
@@ -20,17 +20,17 @@ import {
TranslatedString,
} from "@gnu-taler/taler-util";
import {
- FormConfiguration,
+ FormDesign,
+ FormUI,
RenderAllFieldsByUiConfig,
UIFormElementConfig,
UIHandlerId,
- convertUiField,
getConverterById,
+ useForm,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
-import { getShapeFromFields, useFormState } from "../hooks/form.js";
import { AmlEvent } from "./CaseDetails.js";
/**
@@ -66,9 +66,9 @@ export function ShowConsolidated({
const fixed = fixProvidedInfo(cons.kyc);
- const formConfig: FormConfiguration = {
+ const design: FormDesign = {
type: "double-column",
- design:
+ sections:
Object.entries(fixed).length > 0
? [
{
@@ -98,55 +98,10 @@ export function ShowConsolidated({
]
: [],
};
- const shape: Array<UIHandlerId> = formConfig.design.flatMap((field) =>
- getShapeFromFields(field.fields),
- );
- const { handler } = useFormState<{}>(shape, fixed, (result) => {
- return { status: "ok", errors: undefined, result };
- });
+ const { handler } = useForm(design, fixed);
- return (
- <Fragment>
- <div class="space-y-10 divide-y divide-gray-900/10">
- {formConfig.design.map((section, i) => {
- if (!section) return <Fragment />;
- return (
- <div
- key={i}
- class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3"
- >
- <div class="px-4 sm:px-0">
- <h2 class="text-base font-semibold leading-7 text-gray-900">
- {section.title}
- </h2>
- {section.description && (
- <p class="mt-1 text-sm leading-6 text-gray-600">
- {section.description}
- </p>
- )}
- </div>
- <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md
md:col-span-2">
- <div class="p-3">
- <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,
- handler,
- getConverterById,
- )}
- />
- </div>
- </div>
- </div>
- </div>
- );
- })}
- </div>
- </Fragment>
- );
+ return <FormUI design={design} handler={handler} />;
}
interface Consolidated {
diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
index 72656bb98..e8014fe32 100644
--- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -15,14 +15,17 @@
*/
import {
Button,
+ FormDesign,
InputLine,
+ InternationalizationAPI,
LocalNotificationBanner,
UIHandlerId,
+ useForm,
useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
-import { FormErrors, useFormState } from "../hooks/form.js";
+import { FormErrors } from "../hooks/form.js";
import { useOfficer } from "../hooks/officer.js";
import { undefinedIfEmpty } from "./CreateAccount.js";
@@ -30,33 +33,35 @@ type FormType = {
password: string;
};
+const unlockAccountForm = (i18n: InternationalizationAPI): FormDesign => ({
+ type: "single-column",
+ fields: [
+ {
+ id: "password" as UIHandlerId,
+ type: "text",
+ label: i18n.str`Password`,
+ required: true,
+ },
+ ],
+});
+
export function UnlockAccount(): VNode {
const { i18n } = useTranslationContext();
const officer = useOfficer();
const [notification, withErrorHandler] = useLocalNotificationHandler();
- const { handler, status } = useFormState<FormType>(
- [".password"] as Array<UIHandlerId>,
+ const design = unlockAccountForm(i18n);
+
+ const { handler, status } = useForm<FormType>(
+ design,
{
password: undefined,
},
(state) => {
- const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ return undefinedIfEmpty<FormErrors<FormType>>({
password: !state.password ? i18n.str`required` : undefined,
});
- if (errors === undefined) {
- return {
- status: "ok",
- result: state as FormType,
- errors,
- };
- }
- return {
- status: "fail",
- result: state,
- errors,
- };
},
);
diff --git a/packages/web-util/src/forms/forms-ui.tsx
b/packages/web-util/src/forms/forms-ui.tsx
index 3f9dc4f5f..55ee29cb3 100644
--- a/packages/web-util/src/forms/forms-ui.tsx
+++ b/packages/web-util/src/forms/forms-ui.tsx
@@ -33,6 +33,11 @@ export function DefaultForm<T>({
);
}
+/**
+ * FIXME: formDesign should be embedded in formHandler
+ * @param param0
+ * @returns
+ */
export function FormUI<T>({
design,
handler,
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.