commit 178ce8d6b8bdf5f3139d14c91058e2e54044bffd
parent 3b318663bd9e729a18cdf18ac10244f02bd42df2
Author: Sebastian <sebasjm@gmail.com>
Date: Mon, 9 Dec 2024 14:13:27 -0300
array input fixed
Diffstat:
8 files changed, 450 insertions(+), 85 deletions(-)
diff --git a/packages/kyc-ui/src/forms/accept-tos.ts b/packages/kyc-ui/src/forms/accept-tos.ts
@@ -37,7 +37,7 @@ export const acceptTos = (i18n: InternationalizationAPI, context?: any): DoubleC
} : undefined,
{
type: "choiceHorizontal",
- id: "asd" as UIHandlerId,
+ id: "ACCEPTED_TERMS_OF_SERVICE" as UIHandlerId,
required: true,
label: i18n.str`Do you accept terms of service`,
choices: [
diff --git a/packages/kyc-ui/src/hooks/form.ts b/packages/kyc-ui/src/hooks/form.ts
@@ -92,7 +92,6 @@ function constructFormHandler<T>(
const path = fieldId.split(".");
function updater(newValue: unknown) {
- console.log("----",path, newValue)
updateForm(setValueDeeper(form, path, newValue));
}
diff --git a/packages/kyc-ui/src/pages/FillForm.tsx b/packages/kyc-ui/src/pages/FillForm.tsx
@@ -45,6 +45,7 @@ import {
} from "../hooks/form.js";
import { undefinedIfEmpty } from "./Start.js";
import { useUiFormsContext } from "../context/ui-forms.js";
+import { usePreferences } from "../context/preferences.js";
type Props = {
token: AccessToken;
@@ -78,6 +79,7 @@ export function FillForm({
const { config, lib } = useExchangeApiContext();
// const { forms } = useUiFormsContext();
const [notification, withErrorHandler] = useLocalNotificationHandler();
+ const [preferences] = usePreferences();
const customForm =
requirement.context && "form" in requirement.context
@@ -183,44 +185,48 @@ export function FillForm({
<div class="rounded-lg bg-white px-5 py-6 shadow m-4">
<LocalNotificationBanner notification={notification} />
<div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
- {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,
- form,
- getConverterById,
- )}
- />
- </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,
+ form,
+ getConverterById,
+ )}
+ />
</div>
</div>
</div>
- );
- })}
+ </div>
+ );
+ })}
</div>
- <pre>{JSON.stringify(state.result, undefined, 2)}</pre>
+ {preferences.showDebugInfo ? (
+ <pre>{JSON.stringify(state.result, undefined, 2)}</pre>
+ ) : (
+ <Fragment />
+ )}
<div class="mt-6 flex items-center justify-end gap-x-6">
<button
onClick={onComplete}
diff --git a/packages/web-util/src/forms/InputArray.tsx b/packages/web-util/src/forms/InputArray.tsx
@@ -1,11 +1,23 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
+import { useEffect, useState } from "preact/hooks";
import { FormProvider, UIFormProps } from "./FormProvider.js";
import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
+import {
+ convertUiField,
+ RenderAllFieldsByUiConfig,
+ UIFormField,
+} from "./forms.js";
import { useField } from "./useField.js";
import { UIFormElementConfig, UIHandlerId } from "./ui-form.js";
+import {
+ FormErrors,
+ undefinedIfEmpty,
+ useFormState,
+ useFormStateFromConfig,
+ validateRequiredFields,
+} from "../hooks/useForm.js";
+import { getConverterById, useTranslationContext } from "../index.browser.js";
function Option({
label,
@@ -80,9 +92,30 @@ export function noHandlerPropsAndNoContextForField(
);
}
-function getShapeFromFields(
- fields: UIFormElementConfig[],
-): Array<UIHandlerId> {
+// function getRequiredFields(fields: UIFormField[]): Array<UIHandlerId> {
+// const shape: Array<UIHandlerId> = [];
+// fields.forEach((field) => {
+// if ("name" in field.properties) {
+// // FIXME: this should be a validation when loading the form
+// // consistency check
+// if (shape.indexOf(field.properties.name) !== -1) {
+// throw Error(`already present: ${field.properties.name}`);
+// }
+// if (!field.properties.required) {
+// return;
+// }
+// shape.push(field.properties.name);
+// } else if (field.type === "group") {
+// Array.prototype.push.apply(
+// shape,
+// getRequiredFields(field.properties.fields),
+// );
+// }
+// });
+// return shape;
+// }
+
+function getRequiredFields(fields: UIFormElementConfig[]): Array<UIHandlerId> {
const shape: Array<UIHandlerId> = [];
fields.forEach((field) => {
if ("id" in field) {
@@ -91,32 +124,66 @@ function getShapeFromFields(
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,
- getShapeFromFields(field.fields),
- );
+ Array.prototype.push.apply(shape, getRequiredFields(field.fields));
}
});
return shape;
}
+// function getShapeFromFields(fields: UIFormField[]): Array<UIHandlerId> {
+// const shape: Array<UIHandlerId> = [];
+// fields.forEach((field) => {
+// if ("name" in field.properties) {
+// // FIXME: this should be a validation when loading the form
+// // consistency check
+// if (shape.indexOf(field.properties.name) !== -1) {
+// throw Error(`already present: ${field.properties.name}`);
+// }
+// shape.push(field.properties.name);
+// } else if (field.type === "group") {
+// Array.prototype.push.apply(
+// shape,
+// getShapeFromFields(field.properties.fields),
+// );
+// }
+// });
+// return shape;
+// }
+
+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;
+}
+
+type FormType = {};
+
export function InputArray<T extends object, K extends keyof T>(
props: {
- fields: UIFormField[];
+ fields: UIFormElementConfig[];
labelField: string;
} & UIFormProps<T, K>,
): VNode {
const { fields, labelField, name, label, required, tooltip } = props;
- // const { value, onChange, state } = useField<T, K>(name);
- //FIXME: remove deprecated
- const fieldCtx = useField<T, K>(props.name);
- if (!props.handler && !fieldCtx) {
- throw Error("");
- }
+
const { value, onChange, state } =
- props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name);
+ props.handler ?? noHandlerPropsAndNoContextForField(props.name);
const list = (value ?? []) as Array<Record<string, string | undefined>>;
const [selectedIndex, setSelectedIndex] = useState<number | undefined>(
@@ -125,15 +192,48 @@ export function InputArray<T extends object, K extends keyof T>(
const selected =
selectedIndex === undefined ? undefined : list[selectedIndex];
- const shape: Array<UIHandlerId> = [];
- const requiredFields: Array<UIHandlerId> = [];
+ // const shape: Array<UIHandlerId> = [];
+ // const requiredFields: Array<UIHandlerId> = [];
+ // Array.prototype.push.apply(shape, getShapeFromFields(fields));
+ // Array.prototype.push.apply(requiredFields, getRequiredFields(fields));
- Array.prototype.push.apply(shape, getShapeFromFields(fields));
- Array.prototype.push.apply(
- requiredFields,
- getRequiredFields(fields),
- );
-
+ const [form, formState] = useFormStateFromConfig<FormType>(
+ fields,
+ selected ?? {},
+ );
+ // const [form, formState] = useFormState<FormType>(
+ // shape,
+ // selected ?? {},
+ // (st) => {
+ // const partialErrors = undefinedIfEmpty<FormErrors<FormType>>({});
+
+ // const errors = undefinedIfEmpty<FormErrors<FormType> | undefined>(
+ // validateRequiredFields(partialErrors, st, requiredFields),
+ // );
+
+ // if (errors === undefined) {
+ // return {
+ // status: "ok",
+ // result: st as any,
+ // errors: undefined,
+ // };
+ // }
+
+ // return {
+ // status: "fail",
+ // result: st as any,
+ // errors,
+ // };
+ // },
+ // );
+
+ useEffect(() => {
+ if (selectedIndex === undefined) return;
+ const newValue = [...list];
+ newValue.splice(selectedIndex, 1, formState.result);
+ onChange(newValue as any);
+ }, [formState.result, selectedIndex]);
+ const { i18n } = useTranslationContext();
return (
<div class="sm:col-span-6">
@@ -210,11 +310,13 @@ export function InputArray<T extends object, K extends keyof T>(
// onChange(newValue as any);
// }}
// >
- <div class="px-4 py-6">
- <div class="grid grid-cols-1 gap-y-8 ">
- <RenderAllFieldsByUiConfig fields={fields} />
- </div>
+ <div class="px-4 py-6">
+ <div class="grid grid-cols-1 gap-y-8 ">
+ <RenderAllFieldsByUiConfig
+ fields={convertUiField(i18n, fields, form, getConverterById)}
+ />
</div>
+ </div>
// </FormProvider>
)}
{selectedIndex !== undefined && (
@@ -226,7 +328,7 @@ export function InputArray<T extends object, K extends keyof T>(
}}
class="block px-3 py-2 text-sm font-semibold leading-6 text-gray-900"
>
- Close
+ <i18n.Translate>Close</i18n.Translate>
</button>
<button
@@ -240,7 +342,7 @@ export function InputArray<T extends object, K extends keyof T>(
}}
class="block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
>
- Remove
+ <i18n.Translate>Remove</i18n.Translate>
</button>
</div>
)}
diff --git a/packages/web-util/src/forms/InputChoiceStacked.tsx b/packages/web-util/src/forms/InputChoiceStacked.tsx
@@ -30,9 +30,8 @@ export function InputChoiceStacked<T extends object, K extends keyof T>(
} = props;
//FIXME: remove deprecated
- const fieldCtx = useField<T, K>(props.name);
const { value, onChange, state } =
- props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name);
+ props.handler ?? noHandlerPropsAndNoContextForField(props.name);
if (state.hidden) {
return <Fragment />;
diff --git a/packages/web-util/src/forms/InputLine.tsx b/packages/web-util/src/forms/InputLine.tsx
@@ -161,23 +161,14 @@ export function InputLine<T extends object, K extends keyof T>(
props: { type: InputType } & UIFormProps<T, K>,
): VNode {
const { name, placeholder, before, after, converter, type, disabled } = props;
- //FIXME: remove deprecated
- const fieldCtx = useField<T, K>(props.name);
+
const { value, onChange, state, error } =
props.handler ?? noHandlerPropsAndNoContextForField(props.name);
- // const [text, setText] = useState("");
const fromString: (s: string) => any =
converter?.fromStringUI ?? defaultFromString;
const toString: (s: any) => string = converter?.toStringUI ?? defaultToString;
- // useEffect(() => {
- // const newValue = toString(value);
- // if (newValue) {
- // setText(newValue);
- // }
- // }, [value]);
-
if (state.hidden) return <div />;
let clazz =
diff --git a/packages/web-util/src/forms/forms.ts b/packages/web-util/src/forms/forms.ts
@@ -212,12 +212,13 @@ export function convertUiField(
...converBaseFieldsProps(i18n_, config),
...converInputFieldsProps(form, config, getConverterById),
labelField: config.labelFieldId,
- fields: convertUiField(
- i18n_,
- config.fields,
- (form as any)[config.id].value ?? {},
- getConverterById,
- ),
+ fields: config.fields,
+ // convertUiField(
+ // i18n_,
+ // config.fields,
+ // (form as any)[config.id].value ?? {},
+ // getConverterById,
+ // ),
},
} as UIFormField;
}
@@ -349,7 +350,7 @@ function converInputFieldsProps(
getConverterById: GetConverterById,
) {
const names = p.id.split(".");
- console.log("NAMES", names, getValueDeeper2(form, names), form !== undefined)
+ console.log("NAMES", names, getValueDeeper2(form, names), form)
return {
converter: getConverterById(p.converterId, p),
handler: getValueDeeper2(form, names),
diff --git a/packages/web-util/src/hooks/useForm.ts b/packages/web-util/src/hooks/useForm.ts
@@ -0,0 +1,267 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AbsoluteTime,
+ AmountJson,
+ TalerExchangeApi,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+ UIFieldHandler,
+ UIFormElementConfig,
+ UIHandlerId,
+} from "@gnu-taler/web-util/browser";
+import { useState } from "preact/hooks";
+
+export type FormHandler<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? UIFieldHandler
+ : T[k] extends AmountJson
+ ? UIFieldHandler
+ : T[k] extends TalerExchangeApi.AmlState
+ ? UIFieldHandler
+ : FormHandler<T[k]>;
+};
+
+export type FormValues<T> = {
+ [k in keyof T]: T[k] extends string ? string | undefined : FormValues<T[k]>;
+};
+
+export type RecursivePartial<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? string
+ : T[k] extends AmountJson
+ ? AmountJson
+ : T[k] extends TalerExchangeApi.AmlState
+ ? TalerExchangeApi.AmlState
+ : RecursivePartial<T[k]>;
+};
+
+export type FormErrors<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? TranslatedString
+ : T[k] extends AmountJson
+ ? TranslatedString
+ : T[k] extends AbsoluteTime
+ ? TranslatedString
+ : T[k] extends TalerExchangeApi.AmlState
+ ? TranslatedString
+ : FormErrors<T[k]>;
+};
+
+export type FormStatus<T> =
+ | {
+ status: "ok";
+ result: T;
+ errors: undefined;
+ }
+ | {
+ status: "fail";
+ result: RecursivePartial<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;
+}
+
+export function useFormStateFromConfig<T>(
+ fields: Array<UIFormElementConfig>,
+ defaultValue: RecursivePartial<FormValues<T>>,
+ check?: (f: RecursivePartial<FormValues<T>>) => FormStatus<T>,
+): [FormHandler<T>, FormStatus<T>] {
+ const shape: Array<UIHandlerId> = [];
+ const requiredFields: Array<UIHandlerId> = [];
+ Array.prototype.push.apply(shape, getShapeFromFields(fields));
+ Array.prototype.push.apply(requiredFields, getRequiredFields(fields));
+
+ const [form, updateForm] =
+ useState<RecursivePartial<FormValues<T>>>(defaultValue);
+
+ function defaultCheckAllRequired(st: RecursivePartial<FormValues<T>>) {
+ const partialErrors = undefinedIfEmpty<FormErrors<T>>({});
+
+ const errors = undefinedIfEmpty<FormErrors<T> | undefined>(
+ validateRequiredFields(partialErrors, st, requiredFields),
+ );
+
+ if (errors !== undefined) {
+ return {
+ status: "fail" as const,
+ result: st as any,
+ errors,
+ };
+ }
+
+ return undefined;
+ }
+ // check required fields
+ const requiredCheckResult = requiredFields.length > 0 ? defaultCheckAllRequired(form) : undefined;
+ // verify if there is a custom check function and all required fields are ok
+ // if there no custom check return "ok"
+ const status = requiredCheckResult ?? (check ? check(form) : {status: "ok" as const, result: form as any, errors: undefined})
+ const handler = constructFormHandler(shape, form, updateForm, requiredCheckResult?.errors);
+
+ return [handler, status];
+}
+
+/**
+ * @deprecated use `useFormStateFromConfig`
+ *
+ * @param defaultValue
+ * @param check
+ * @returns
+ */
+export function useFormState<T>(
+ shape: Array<UIHandlerId>,
+ defaultValue: RecursivePartial<FormValues<T>>,
+ check: (f: RecursivePartial<FormValues<T>>) => FormStatus<T>,
+): [FormHandler<T>, 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> {}
+
+export function getValueDeeper<T>(
+ object: Tree<T> | undefined,
+ names: string[],
+ notFoundValue?: T,
+): T | undefined {
+ if (names.length === 0) return object as T;
+ const [head, ...rest] = names;
+ if (!head) {
+ return getValueDeeper(object, rest, notFoundValue);
+ }
+ if (object === undefined) {
+ return notFoundValue;
+ }
+ return getValueDeeper(object[head] as Tree<T>, rest, notFoundValue);
+}
+
+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 undefinedIfEmpty({ [head]: setValueDeeper({}, rest, value) });
+ }
+ return undefinedIfEmpty({
+ ...object,
+ [head]: setValueDeeper(object[head] ?? {}, rest, value),
+ });
+}
+
+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,
+ )
+ ? obj
+ : undefined;
+}
+
+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;
+}