gnunet-svn
[Top][All Lists]
Advanced

[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.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]