[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-wallet-core] branch master updated (e78d0f8e4 -> 9c6146309)
From: |
gnunet |
Subject: |
[taler-wallet-core] branch master updated (e78d0f8e4 -> 9c6146309) |
Date: |
Fri, 10 Jan 2025 19:54:26 +0100 |
This is an automated email from the git hooks/post-receive script.
sebasjm pushed a change to branch master
in repository wallet-core.
from e78d0f8e4 wallet-core: refresh transitions preserve user-visible
balance
new b1090b6b3 remove duplicated form implementation
new b1394a32c use the new form implementation in aml
new ca62771fc use the new form implementation in kyc
new 9c6146309 remove broken imports
The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
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 | 121 +++---
.../aml-backoffice-ui/src/pages/CaseUpdate.tsx | 166 +--------
.../aml-backoffice-ui/src/pages/CreateAccount.tsx | 102 ++---
packages/aml-backoffice-ui/src/pages/Search.tsx | 218 +++--------
.../src/pages/ShowConsolidated.tsx | 61 +--
.../aml-backoffice-ui/src/pages/UnlockAccount.tsx | 37 +-
packages/kyc-ui/src/forms/VQF_902_1.ts | 8 +-
packages/kyc-ui/src/forms/VQF_902_11.ts | 8 +-
packages/kyc-ui/src/forms/VQF_902_12.ts | 8 +-
packages/kyc-ui/src/forms/VQF_902_13.ts | 8 +-
packages/kyc-ui/src/forms/VQF_902_14.ts | 8 +-
packages/kyc-ui/src/forms/VQF_902_15.ts | 8 +-
packages/kyc-ui/src/forms/VQF_902_4.ts | 8 +-
packages/kyc-ui/src/forms/VQF_902_5.ts | 8 +-
packages/kyc-ui/src/forms/VQF_902_9.ts | 8 +-
packages/kyc-ui/src/forms/accept-tos.ts | 5 +-
packages/kyc-ui/src/forms/nameAndBirthdate.ts | 6 +-
packages/kyc-ui/src/forms/personal-info.ts | 6 +-
packages/kyc-ui/src/forms/simplest.ts | 8 +-
packages/kyc-ui/src/pages/FillForm.tsx | 190 +---------
packages/kyc-ui/src/pages/TriggerKyc.tsx | 157 +-------
packages/web-util/src/forms/DefaultForm.tsx | 105 ------
packages/web-util/src/forms/FormProvider.tsx | 41 +-
packages/web-util/src/forms/Group.tsx | 12 +-
.../src/forms/InputAbsoluteTime.stories.tsx | 15 +-
packages/web-util/src/forms/InputAbsoluteTime.tsx | 7 +-
.../web-util/src/forms/InputAmount.stories.tsx | 14 +-
packages/web-util/src/forms/InputAmount.tsx | 5 +-
packages/web-util/src/forms/InputArray.stories.tsx | 24 +-
packages/web-util/src/forms/InputArray.tsx | 98 ++---
.../src/forms/InputChoiceHorizontal.stories.tsx | 14 +-
.../web-util/src/forms/InputChoiceHorizontal.tsx | 11 +-
.../src/forms/InputChoiceStacked.stories.tsx | 14 +-
packages/web-util/src/forms/InputChoiceStacked.tsx | 2 -
packages/web-util/src/forms/InputFile.stories.tsx | 22 +-
packages/web-util/src/forms/InputFile.tsx | 14 +-
.../web-util/src/forms/InputInteger.stories.tsx | 18 +-
packages/web-util/src/forms/InputLine.stories.tsx | 65 ----
packages/web-util/src/forms/InputLine.tsx | 1 -
.../src/forms/InputSelectMultiple.stories.tsx | 16 +-
.../web-util/src/forms/InputSelectMultiple.tsx | 16 +-
.../web-util/src/forms/InputSelectOne.stories.tsx | 14 +-
packages/web-util/src/forms/InputSelectOne.tsx | 8 +-
packages/web-util/src/forms/InputText.stories.tsx | 14 +-
.../web-util/src/forms/InputTextArea.stories.tsx | 16 +-
.../web-util/src/forms/InputToggle.stories.tsx | 53 +--
packages/web-util/src/forms/InputToggle.tsx | 8 +-
packages/web-util/src/forms/converter.ts | 130 -------
packages/web-util/src/forms/field-types.ts | 117 ++++++
.../src/forms/{ui-form.ts => forms-types.ts} | 49 ++-
packages/web-util/src/forms/forms-ui.tsx | 134 +++++++
packages/web-util/src/forms/forms-utils.ts | 382 +++++++++++++++++++
packages/web-util/src/forms/forms.ts | 411 ---------------------
packages/web-util/src/forms/index.stories.ts | 1 -
packages/web-util/src/forms/index.ts | 49 ++-
packages/web-util/src/forms/useField.ts | 30 +-
packages/web-util/src/hooks/index.ts | 8 +-
packages/web-util/src/hooks/useForm.ts | 315 ++++++++--------
packages/web-util/src/stories.html | 6 +-
62 files changed, 1404 insertions(+), 2215 deletions(-)
delete mode 100644 packages/web-util/src/forms/DefaultForm.tsx
delete mode 100644 packages/web-util/src/forms/InputLine.stories.tsx
delete mode 100644 packages/web-util/src/forms/converter.ts
create mode 100644 packages/web-util/src/forms/field-types.ts
rename packages/web-util/src/forms/{ui-form.ts => forms-types.ts} (92%)
create mode 100644 packages/web-util/src/forms/forms-ui.tsx
create mode 100644 packages/web-util/src/forms/forms-utils.ts
delete mode 100644 packages/web-util/src/forms/forms.ts
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..48e9a1e82 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -18,64 +18,52 @@ 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,
- getConverterById,
+ FormUI,
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 +491,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 +547,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 +593,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 +639,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..d537f86b4 100644
--- a/packages/aml-backoffice-ui/src/pages/Search.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Search.tsx
@@ -29,30 +29,23 @@ import {
} from "@gnu-taler/taler-util";
import {
Attention,
- convertUiField,
encodeCrockForURI,
- getConverterById,
+ FormDesign,
+ FormUI,
InternationalizationAPI,
Loading,
- RenderAllFieldsByUiConfig,
Time,
UIFormElementConfig,
UIHandlerId,
useExchangeApiContext,
+ useForm,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
import { useAccountDecisions } from "../hooks/decisions.js";
-import {
- FormErrors,
- FormStatus,
- FormValues,
- getShapeFromFields,
- RecursivePartial,
- useFormState,
-} from "../hooks/form.js";
+import { FormErrors, FormValues, RecursivePartial } from "../hooks/form.js";
import { useOfficer } from "../hooks/officer.js";
import { privatePages } from "../Routing.js";
import { Pagination, ToInvestigateIcon } from "./Cases.js";
@@ -65,8 +58,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 +86,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 +298,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 +326,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 +344,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 +368,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 +387,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 +417,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 +436,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 +459,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 +478,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 +492,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 +510,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 +523,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 +538,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 +547,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..a9b32e690 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
@@ -20,17 +20,15 @@ import {
TranslatedString,
} from "@gnu-taler/taler-util";
import {
- FormConfiguration,
- RenderAllFieldsByUiConfig,
+ FormDesign,
+ FormUI,
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 +64,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 +96,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/kyc-ui/src/forms/VQF_902_1.ts
b/packages/kyc-ui/src/forms/VQF_902_1.ts
index 467596a03..fa0077b65 100644
--- a/packages/kyc-ui/src/forms/VQF_902_1.ts
+++ b/packages/kyc-ui/src/forms/VQF_902_1.ts
@@ -1,8 +1,8 @@
import {
InternationalizationAPI,
- DoubleColumnForm,
UIHandlerId,
SelectUiChoice,
+ DoubleColumnFormDesign,
} from "@gnu-taler/web-util/browser";
import { TalerAmlAttributes } from "./taler_form_attributes.js";
@@ -14,10 +14,12 @@ export function countryList(i18n: InternationalizationAPI):
SelectUiChoice[] {
];
}
-export function VQF_902_1(i18n: InternationalizationAPI): DoubleColumnForm {
+export function VQF_902_1(
+ i18n: InternationalizationAPI,
+): DoubleColumnFormDesign {
return {
type: "double-column",
- design: [
+ sections: [
{
title: i18n.str`This form was completed by`,
fields: [
diff --git a/packages/kyc-ui/src/forms/VQF_902_11.ts
b/packages/kyc-ui/src/forms/VQF_902_11.ts
index 0fd5f7cf0..fd1b9b220 100644
--- a/packages/kyc-ui/src/forms/VQF_902_11.ts
+++ b/packages/kyc-ui/src/forms/VQF_902_11.ts
@@ -1,14 +1,16 @@
import {
InternationalizationAPI,
- DoubleColumnForm,
+ DoubleColumnFormDesign,
UIHandlerId,
} from "@gnu-taler/web-util/browser";
import { TalerAmlAttributes } from "./taler_form_attributes.js";
-export function VQF_902_11(i18n: InternationalizationAPI): DoubleColumnForm {
+export function VQF_902_11(
+ i18n: InternationalizationAPI,
+): DoubleColumnFormDesign {
return {
type: "double-column",
- design: [
+ sections: [
{
title: i18n.str`This form was completed by`,
fields: [
diff --git a/packages/kyc-ui/src/forms/VQF_902_12.ts
b/packages/kyc-ui/src/forms/VQF_902_12.ts
index 52483a15e..199913108 100644
--- a/packages/kyc-ui/src/forms/VQF_902_12.ts
+++ b/packages/kyc-ui/src/forms/VQF_902_12.ts
@@ -1,15 +1,17 @@
import {
InternationalizationAPI,
- DoubleColumnForm,
+ DoubleColumnFormDesign,
UIHandlerId,
} from "@gnu-taler/web-util/browser";
import { TalerAmlAttributes } from "./taler_form_attributes.js";
import { countryList } from "./VQF_902_1.js";
-export function VQF_902_12(i18n: InternationalizationAPI): DoubleColumnForm {
+export function VQF_902_12(
+ i18n: InternationalizationAPI,
+): DoubleColumnFormDesign {
return {
type: "double-column",
- design: [
+ sections: [
{
title: i18n.str`This form was completed by`,
fields: [
diff --git a/packages/kyc-ui/src/forms/VQF_902_13.ts
b/packages/kyc-ui/src/forms/VQF_902_13.ts
index 7f5bddc26..638cf86af 100644
--- a/packages/kyc-ui/src/forms/VQF_902_13.ts
+++ b/packages/kyc-ui/src/forms/VQF_902_13.ts
@@ -1,15 +1,17 @@
import {
InternationalizationAPI,
- DoubleColumnForm,
+ DoubleColumnFormDesign,
UIHandlerId,
} from "@gnu-taler/web-util/browser";
import { TalerAmlAttributes } from "./taler_form_attributes.js";
import { countryList } from "./VQF_902_1.js";
-export function VQF_902_13(i18n: InternationalizationAPI): DoubleColumnForm {
+export function VQF_902_13(
+ i18n: InternationalizationAPI,
+): DoubleColumnFormDesign {
return {
type: "double-column",
- design: [
+ sections: [
{
title: i18n.str`This form was completed by`,
fields: [
diff --git a/packages/kyc-ui/src/forms/VQF_902_14.ts
b/packages/kyc-ui/src/forms/VQF_902_14.ts
index f0045f0ac..f5f66fecd 100644
--- a/packages/kyc-ui/src/forms/VQF_902_14.ts
+++ b/packages/kyc-ui/src/forms/VQF_902_14.ts
@@ -1,14 +1,16 @@
import {
InternationalizationAPI,
- DoubleColumnForm,
+ DoubleColumnFormDesign,
UIHandlerId,
} from "@gnu-taler/web-util/browser";
import { TalerAmlAttributes } from "./taler_form_attributes.js";
-export function VQF_902_14(i18n: InternationalizationAPI): DoubleColumnForm {
+export function VQF_902_14(
+ i18n: InternationalizationAPI,
+): DoubleColumnFormDesign {
return {
type: "double-column",
- design: [
+ sections: [
{
title: i18n.str`This form was completed by`,
fields: [
diff --git a/packages/kyc-ui/src/forms/VQF_902_15.ts
b/packages/kyc-ui/src/forms/VQF_902_15.ts
index f55f84ebb..beb944477 100644
--- a/packages/kyc-ui/src/forms/VQF_902_15.ts
+++ b/packages/kyc-ui/src/forms/VQF_902_15.ts
@@ -1,15 +1,17 @@
import {
+ DoubleColumnFormDesign,
InternationalizationAPI,
- DoubleColumnForm,
UIHandlerId,
} from "@gnu-taler/web-util/browser";
import { TalerAmlAttributes } from "./taler_form_attributes.js";
import { countryList } from "./VQF_902_1.js";
-export function VQF_902_15(i18n: InternationalizationAPI): DoubleColumnForm {
+export function VQF_902_15(
+ i18n: InternationalizationAPI,
+): DoubleColumnFormDesign {
return {
type: "double-column",
- design: [
+ sections: [
{
title: i18n.str`This form was completed by`,
fields: [
diff --git a/packages/kyc-ui/src/forms/VQF_902_4.ts
b/packages/kyc-ui/src/forms/VQF_902_4.ts
index d30e13f16..b3e5a0de5 100644
--- a/packages/kyc-ui/src/forms/VQF_902_4.ts
+++ b/packages/kyc-ui/src/forms/VQF_902_4.ts
@@ -1,14 +1,16 @@
import {
InternationalizationAPI,
- DoubleColumnForm,
+ DoubleColumnFormDesign,
UIHandlerId,
} from "@gnu-taler/web-util/browser";
import { TalerAmlAttributes } from "./taler_form_attributes.js";
-export function VQF_902_4(i18n: InternationalizationAPI): DoubleColumnForm {
+export function VQF_902_4(
+ i18n: InternationalizationAPI,
+): DoubleColumnFormDesign {
return {
type: "double-column",
- design: [
+ sections: [
{
title: i18n.str`This form was completed by`,
fields: [
diff --git a/packages/kyc-ui/src/forms/VQF_902_5.ts
b/packages/kyc-ui/src/forms/VQF_902_5.ts
index df63d9eff..46dbdb810 100644
--- a/packages/kyc-ui/src/forms/VQF_902_5.ts
+++ b/packages/kyc-ui/src/forms/VQF_902_5.ts
@@ -1,14 +1,16 @@
import {
+ DoubleColumnFormDesign,
InternationalizationAPI,
- DoubleColumnForm,
UIHandlerId,
} from "@gnu-taler/web-util/browser";
import { TalerAmlAttributes } from "./taler_form_attributes.js";
-export function VQF_902_5(i18n: InternationalizationAPI): DoubleColumnForm {
+export function VQF_902_5(
+ i18n: InternationalizationAPI,
+): DoubleColumnFormDesign {
return {
type: "double-column",
- design: [
+ sections: [
{
title: i18n.str`This form was completed by`,
fields: [
diff --git a/packages/kyc-ui/src/forms/VQF_902_9.ts
b/packages/kyc-ui/src/forms/VQF_902_9.ts
index 19fcc7aff..90e17be8b 100644
--- a/packages/kyc-ui/src/forms/VQF_902_9.ts
+++ b/packages/kyc-ui/src/forms/VQF_902_9.ts
@@ -1,15 +1,17 @@
import {
InternationalizationAPI,
- DoubleColumnForm,
+ DoubleColumnFormDesign,
UIHandlerId,
} from "@gnu-taler/web-util/browser";
import { TalerAmlAttributes } from "./taler_form_attributes.js";
import { countryList } from "./VQF_902_1.js";
-export function VQF_902_9(i18n: InternationalizationAPI): DoubleColumnForm {
+export function VQF_902_9(
+ i18n: InternationalizationAPI,
+): DoubleColumnFormDesign {
return {
type: "double-column",
- design: [
+ sections: [
{
title: i18n.str`This form was completed by`,
fields: [
diff --git a/packages/kyc-ui/src/forms/accept-tos.ts
b/packages/kyc-ui/src/forms/accept-tos.ts
index c95689603..a00ecb7e6 100644
--- a/packages/kyc-ui/src/forms/accept-tos.ts
+++ b/packages/kyc-ui/src/forms/accept-tos.ts
@@ -15,9 +15,8 @@
*/
import type {
- DoubleColumnForm,
InternationalizationAPI,
- SingleColumnForm,
+ SingleColumnFormDesign,
UIFormElementConfig,
UIHandlerId,
} from "@gnu-taler/web-util/browser";
@@ -29,7 +28,7 @@ function filterUndefined<T>(ar: Array<T | undefined>):
Array<T> {
export const acceptTos = (
i18n: InternationalizationAPI,
context?: any,
-): SingleColumnForm => ({
+): SingleColumnFormDesign => ({
type: "single-column" as const,
fields: filterUndefined<UIFormElementConfig>([
{
diff --git a/packages/kyc-ui/src/forms/nameAndBirthdate.ts
b/packages/kyc-ui/src/forms/nameAndBirthdate.ts
index dc4aa52e5..f55d228cb 100644
--- a/packages/kyc-ui/src/forms/nameAndBirthdate.ts
+++ b/packages/kyc-ui/src/forms/nameAndBirthdate.ts
@@ -15,16 +15,16 @@
*/
import type {
- DoubleColumnForm,
+ DoubleColumnFormDesign,
InternationalizationAPI,
UIHandlerId,
} from "@gnu-taler/web-util/browser";
export const nameAndDob = (
i18n: InternationalizationAPI,
-): DoubleColumnForm => ({
+): DoubleColumnFormDesign => ({
type: "double-column" as const,
- design: [
+ sections: [
{
title: i18n.str`Simple form`,
fields: [
diff --git a/packages/kyc-ui/src/forms/personal-info.ts
b/packages/kyc-ui/src/forms/personal-info.ts
index 799a8eb3a..15ca58022 100644
--- a/packages/kyc-ui/src/forms/personal-info.ts
+++ b/packages/kyc-ui/src/forms/personal-info.ts
@@ -15,16 +15,16 @@
*/
import type {
- DoubleColumnForm,
+ DoubleColumnFormDesign,
InternationalizationAPI,
UIHandlerId,
} from "@gnu-taler/web-util/browser";
export const personalInfo = (
i18n: InternationalizationAPI,
-): DoubleColumnForm => ({
+): DoubleColumnFormDesign => ({
type: "double-column" as const,
- design: [
+ sections: [
{
title: i18n.str`Simple form`,
fields: [
diff --git a/packages/kyc-ui/src/forms/simplest.ts
b/packages/kyc-ui/src/forms/simplest.ts
index 9cadbc95c..19ad05162 100644
--- a/packages/kyc-ui/src/forms/simplest.ts
+++ b/packages/kyc-ui/src/forms/simplest.ts
@@ -15,15 +15,17 @@
*/
import type {
- DoubleColumnForm,
+ DoubleColumnFormDesign,
DoubleColumnFormSection,
InternationalizationAPI,
UIHandlerId,
} from "@gnu-taler/web-util/browser";
-export const simplest = (i18n: InternationalizationAPI): DoubleColumnForm => ({
+export const simplest = (
+ i18n: InternationalizationAPI,
+): DoubleColumnFormDesign => ({
type: "double-column" as const,
- design: [
+ sections: [
{
title: i18n.str`Simple form`,
fields: [
diff --git a/packages/kyc-ui/src/pages/FillForm.tsx
b/packages/kyc-ui/src/pages/FillForm.tsx
index 58b059144..d01dbb141 100644
--- a/packages/kyc-ui/src/pages/FillForm.tsx
+++ b/packages/kyc-ui/src/pages/FillForm.tsx
@@ -25,30 +25,19 @@ import {
import {
Button,
FormMetadata,
- FormProvider,
+ FormUI,
InternationalizationAPI,
LocalNotificationBanner,
- RenderAllFieldsByUiConfig,
- UIFormElementConfig,
- UIHandlerId,
- convertUiField,
- getConverterById,
useExchangeApiContext,
+ useForm,
useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
-import { preloadedForms } from "../forms/index.js";
-import {
- FormErrors,
- useFormState,
- validateRequiredFields,
-} from "../hooks/form.js";
-import { undefinedIfEmpty } from "./Start.js";
-import { useUiFormsContext } from "../context/ui-forms.js";
-import { usePreferences } from "../context/preferences.js";
-import { format } from "date-fns";
import { useNotifierContext } from "../context/notifier.js";
+import { usePreferences } from "../context/preferences.js";
+import { useUiFormsContext } from "../context/ui-forms.js";
+import { preloadedForms } from "../forms/index.js";
type Props = {
token: AccessToken;
@@ -105,69 +94,11 @@ export function FillForm({
if (!reqId) {
return <div>no id for this form, can't upload</div>;
}
- const shape: Array<UIHandlerId> = [];
- const requiredFields: Array<UIHandlerId> = [];
- notifier.emit({
- type: "NEW_FORM",
- payload: theForm.label,
- });
-
- 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 [form, state] = useFormState<FormType>(
- shape,
- {
- OFFICER_FULL_NAME: "asd",
- FORM_FILLING_DATE: format(new Date(), "dd/MM/yyyy"),
- CUSTOMER_ID: "123",
- },
- (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,
- };
- },
- );
- const validatedForm = state.status !== "ok" ? undefined : state.result;
+ const { handler, status } = useForm<FormType>(theForm.config, {
+ CUSTOMER_ID: "123",
+ });
+ const validatedForm = status.status !== "ok" ? undefined : status.result;
const submitHandler =
validatedForm === undefined
@@ -219,65 +150,7 @@ 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">
- {(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,
- form,
- 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,
- form,
- getConverterById,
- )}
- />
- </div>
- </div>
- </div>
- );
- }
- }
- })()}
+ <FormUI handler={handler} design={theForm.config} />
</div>
{preferences.showDebugInfo ? (
@@ -288,7 +161,7 @@ export function FillForm({
</i18n.Translate>
</p>
<pre class="text-sm text-gray-300">
- {JSON.stringify(state.result, undefined, 2)}
+ {JSON.stringify(status.result, undefined, 2)}
</pre>
</div>
) : (
@@ -314,47 +187,6 @@ export function FillForm({
);
}
-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;
-}
-function getShapeFromFields(
- fields: UIFormElementConfig[],
- parent: string,
-): 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, parent),
- );
- }
- });
- return shape;
-}
function searchForm(
i18n: InternationalizationAPI,
forms: FormMetadata[],
diff --git a/packages/kyc-ui/src/pages/TriggerKyc.tsx
b/packages/kyc-ui/src/pages/TriggerKyc.tsx
index 4c4f4f95b..c201b78f8 100644
--- a/packages/kyc-ui/src/pages/TriggerKyc.tsx
+++ b/packages/kyc-ui/src/pages/TriggerKyc.tsx
@@ -13,39 +13,27 @@
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 {
- Attention,
- Button,
- convertUiField,
- FormMetadata,
- getConverterById,
- LocalNotificationBanner,
- RenderAllFieldsByUiConfig,
- UIHandlerId,
- useExchangeApiContext,
- useLocalNotificationHandler,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
-import {
- FormErrors,
- getRequiredFields,
- getShapeFromFields,
- useFormState,
- validateRequiredFields,
-} from "../hooks/form.js";
-import { undefinedIfEmpty } from "./Start.js";
import {
AbsoluteTime,
AccessToken,
AmountJson,
Amounts,
- AmountString,
assertUnreachable,
- createNewOfficerAccount,
createNewWalletKycAccount,
HttpStatusCode,
} from "@gnu-taler/taler-util";
+import {
+ Button,
+ FormMetadata,
+ FormUI,
+ LocalNotificationBanner,
+ UIHandlerId,
+ useExchangeApiContext,
+ useForm,
+ useLocalNotificationHandler,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, h, VNode } from "preact";
import { useEffect, useMemo, useState } from "preact/hooks";
type FormType = {
@@ -67,7 +55,7 @@ export function TriggerKyc({ onKycStarted }: Props): VNode {
label: i18n.str`Trigger KYC balance`,
config: {
type: "double-column",
- design: [
+ sections: [
{
title: i18n.str`Trigger KYC Balance`,
fields: [
@@ -85,58 +73,9 @@ export function TriggerKyc({ onKycStarted }: Props): VNode {
},
};
- 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 [form, state] = useFormState<FormType>(
- shape,
- {
- amount: Amounts.parseOrThrow(`${config.config.currency}:1000000`),
- },
- (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,
- };
- },
- );
+ const { handler, status } = useForm<FormType>(theForm.config, {
+ amount: Amounts.parseOrThrow(`${config.config.currency}:1000000`),
+ });
const accountPromise = useMemo(async () => {
const resp = await lib.exchange.getSeed();
@@ -220,9 +159,9 @@ export function TriggerKyc({ onKycStarted }: Props): VNode {
}
const sendFormValue =
- theForm === undefined || state.status === "fail"
+ theForm === undefined || status.status === "fail"
? undefined
- : triggerAmount(state.result.amount);
+ : triggerAmount(status.result.amount);
if (kycAccount) {
return <div>loading...</div>;
@@ -232,65 +171,7 @@ export function TriggerKyc({ onKycStarted }: Props): VNode
{
<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">
- {(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,
- form,
- 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,
- form,
- getConverterById,
- )}
- />
- </div>
- </div>
- </div>
- );
- }
- }
- })()}
+ <FormUI handler={handler} design={theForm.config} />
</div>
<div class="mt-6 flex items-center justify-end gap-x-6">
diff --git a/packages/web-util/src/forms/DefaultForm.tsx
b/packages/web-util/src/forms/DefaultForm.tsx
deleted file mode 100644
index 239577e24..000000000
--- a/packages/web-util/src/forms/DefaultForm.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
-import { Fragment, VNode, h } from "preact";
-import {
- UIFormElementConfig,
- getConverterById,
- useTranslationContext,
-} from "../index.browser.js";
-import { FormProvider, FormProviderProps, FormState } from "./FormProvider.js";
-import {
- RenderAllFieldsByUiConfig,
- UIFormField,
- convertUiField,
-} from "./forms.js";
-// import { FlexibleForm } from "./ui-form.js";
-
-/**
- * Flexible form uses a DoubleColumForm for design
- * and may have a dynamic properties defined by
- * behavior function.
- */
-export interface FlexibleForm_Deprecated<T extends object> {
- design: DoubleColumnForm_Deprecated;
- behavior?: (form: Partial<T>) => FormState<T>;
-}
-
-/**
- * Double column form
- *
- * Form with sections, every sections have a title and may
- * have a description.
- * Every sections contain a set of fields.
- */
-export type DoubleColumnForm_Deprecated = Array<
- DoubleColumnFormSection_Deprecated | undefined
->;
-
-export type DoubleColumnFormSection_Deprecated = {
- title: TranslatedString;
- description?: TranslatedString;
- fields: UIFormElementConfig[];
-};
-
-/**
- * Form Provider implementation that use FlexibleForm
- * to defined behavior and fields.
- */
-export function DefaultForm<T extends object>({
- initial,
- onUpdate,
- form,
- onSubmit,
- children,
- readOnly,
-}: Omit<FormProviderProps<T>, "computeFormState"> & {
- form: FlexibleForm_Deprecated<T>;
-}): VNode {
- const { i18n } = useTranslationContext();
- return (
- <FormProvider
- initial={initial}
- onUpdate={onUpdate}
- onSubmit={onSubmit}
- readOnly={readOnly}
- >
- <div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
- {form.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>
- {children}
- </FormProvider>
- );
-}
diff --git a/packages/web-util/src/forms/FormProvider.tsx
b/packages/web-util/src/forms/FormProvider.tsx
index fe886030a..57baa9a39 100644
--- a/packages/web-util/src/forms/FormProvider.tsx
+++ b/packages/web-util/src/forms/FormProvider.tsx
@@ -78,9 +78,9 @@ export interface UIFormProps<T extends object, K extends
keyof T>
handler?: UIFieldHandler;
}
-export type UIFieldHandler = {
- value: string | undefined;
- onChange: (s: string) => void;
+export type UIFieldHandler<T = any> = {
+ value: T | undefined;
+ onChange: (s: T) => void;
state: FieldUIOptions;
error?: TranslatedString;
};
@@ -113,38 +113,3 @@ export type FormProviderProps<T extends object> =
Omit<FormType<T>, "value"> & {
onSubmit?: (v: Partial<T>, s: FormState<T> | undefined) => void;
children?: ComponentChildren;
};
-
-export function FormProvider<T extends object>({
- children,
- initial,
- onUpdate: notify,
- onSubmit,
- computeFormState,
- readOnly,
-}: FormProviderProps<T>): VNode {
- const [state, setState] = useState<Partial<T>>(initial ?? {});
- const value = { current: state };
- const onUpdate = (v: typeof state) => {
- setState(v);
- if (notify) notify(v);
- };
- return (
- <FormContext.Provider
- value={{ initial, value, onUpdate, computeFormState, readOnly }}
- >
- <form
- onSubmit={(e) => {
- e.preventDefault();
- //@ts-ignore
- if (onSubmit)
- onSubmit(
- value.current,
- !computeFormState ? undefined : computeFormState(value.current),
- );
- }}
- >
- {children}
- </form>
- </FormContext.Provider>
- );
-}
diff --git a/packages/web-util/src/forms/Group.tsx
b/packages/web-util/src/forms/Group.tsx
index f63fa4a9b..83504fc96 100644
--- a/packages/web-util/src/forms/Group.tsx
+++ b/packages/web-util/src/forms/Group.tsx
@@ -1,11 +1,9 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import { VNode, h } from "preact";
+import { Addon } from "./FormProvider.js";
import { LabelWithTooltipMaybeRequired, RenderAddon } from "./InputLine.js";
-import { RenderAllFieldsByUiConfig, UIFormField, convertUiField } from
"./forms.js";
-import { Addon, FormProvider } from "./FormProvider.js";
-import { useField } from "./useField.js";
-import { useTranslationContext } from "../index.browser.js";
-import { getConverterById } from "./converter.js";
+import { RenderAllFieldsByUiConfig } from "./forms-ui.js";
+import { UIFormField } from "./field-types.js";
interface Props {
label: TranslatedString;
@@ -35,9 +33,7 @@ export function Group({
</p>
)}
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-2 sm:grid-cols-6">
- <RenderAllFieldsByUiConfig
- fields={fields}
- />
+ <RenderAllFieldsByUiConfig fields={fields} />
</div>
</div>
);
diff --git a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
index 858349a00..28afd71b1 100644
--- a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
+++ b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
@@ -21,12 +21,8 @@
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "../tests/hook.js";
-import {
- FlexibleForm_Deprecated,
- DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
-
+import { FormDesign, UIHandlerId } from "./forms-types.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
export default {
title: "Input Absolute Time",
};
@@ -44,8 +40,9 @@ const initial: TargetObject = {
today: AbsoluteTime.now(),
};
-const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [
+const design: FormDesign = {
+ type: "double-column",
+ sections: [
{
title: "this is a simple form" as TranslatedString,
fields: [
@@ -62,5 +59,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
export const SimpleComment = tests.createExample(TestedComponent, {
initial,
- form,
+ design,
});
diff --git a/packages/web-util/src/forms/InputAbsoluteTime.tsx
b/packages/web-util/src/forms/InputAbsoluteTime.tsx
index f5fd4fc50..a1287cc9b 100644
--- a/packages/web-util/src/forms/InputAbsoluteTime.tsx
+++ b/packages/web-util/src/forms/InputAbsoluteTime.tsx
@@ -6,7 +6,6 @@ import { Calendar } from "./Calendar.js";
import { Dialog } from "./Dialog.js";
import { UIFormProps } from "./FormProvider.js";
import { InputLine } from "./InputLine.js";
-import { useField } from "./useField.js";
import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
export function InputAbsoluteTime<T extends object, K extends keyof T>(
@@ -15,10 +14,8 @@ export function InputAbsoluteTime<T extends object, K
extends keyof T>(
const pattern = properties.pattern ?? "dd/MM/yyyy";
const [open, setOpen] = useState(false);
- //FIXME: remove deprecated
- const fieldCtx = useField<T, K>(properties.name);
const { value, onChange } =
- properties.handler ?? fieldCtx ??
noHandlerPropsAndNoContextForField(properties.name);
+ properties.handler ?? noHandlerPropsAndNoContextForField(properties.name);
return (
<Fragment>
<InputLine<T, K>
@@ -71,7 +68,7 @@ export function InputAbsoluteTime<T extends object, K extends
keyof T>(
{open && (
<Dialog onClose={() => setOpen(false)}>
<Calendar
- value={(value as AbsoluteTime) ?? AbsoluteTime.now()}
+ value={(value as any as AbsoluteTime) ?? AbsoluteTime.now()}
onChange={(v) => {
onChange(v as any);
setOpen(false);
diff --git a/packages/web-util/src/forms/InputAmount.stories.tsx
b/packages/web-util/src/forms/InputAmount.stories.tsx
index 4351a9655..18516db14 100644
--- a/packages/web-util/src/forms/InputAmount.stories.tsx
+++ b/packages/web-util/src/forms/InputAmount.stories.tsx
@@ -21,11 +21,8 @@
import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "../tests/hook.js";
-import {
- FlexibleForm_Deprecated,
- DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
export default {
title: "Input Amount",
@@ -44,8 +41,9 @@ const initial: TargetObject = {
amount: Amounts.parseOrThrow("USD:10"),
};
-const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [
+const design: FormDesign = {
+ type: "double-column",
+ sections: [
{
title: "this is a simple form" as TranslatedString,
fields: [
@@ -62,5 +60,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
export const SimpleComment = tests.createExample(TestedComponent, {
initial,
- form,
+ design,
});
diff --git a/packages/web-util/src/forms/InputAmount.tsx
b/packages/web-util/src/forms/InputAmount.tsx
index 647d2c823..874fa8686 100644
--- a/packages/web-util/src/forms/InputAmount.tsx
+++ b/packages/web-util/src/forms/InputAmount.tsx
@@ -2,16 +2,13 @@ import { AmountJson, Amounts, TranslatedString } from
"@gnu-taler/taler-util";
import { VNode, h } from "preact";
import { UIFormProps } from "./FormProvider.js";
import { InputLine } from "./InputLine.js";
-import { useField } from "./useField.js";
import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
export function InputAmount<T extends object, K extends keyof T>(
props: { currency?: string } & UIFormProps<T, K>,
): VNode {
- //FIXME: remove deprecated
- const fieldCtx = useField<T, K>(props.name);
const { value } =
- props.handler ?? fieldCtx ??
noHandlerPropsAndNoContextForField(props.name);
+ props.handler ?? noHandlerPropsAndNoContextForField(props.name);
const currency =
!value || !(value as any).currency
? props.currency
diff --git a/packages/web-util/src/forms/InputArray.stories.tsx
b/packages/web-util/src/forms/InputArray.stories.tsx
index 6f478fd07..e9807d2e6 100644
--- a/packages/web-util/src/forms/InputArray.stories.tsx
+++ b/packages/web-util/src/forms/InputArray.stories.tsx
@@ -21,11 +21,8 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "../tests/hook.js";
-import {
- FlexibleForm_Deprecated,
- DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
export default {
title: "Input Array",
@@ -52,8 +49,9 @@ const initial: TargetObject = {
],
};
-const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [
+const design: FormDesign = {
+ type: "double-column",
+ sections: [
{
title: "this is a simple form" as TranslatedString,
description: "to test how arrays are used" as TranslatedString,
@@ -85,14 +83,14 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
export const FormWithArray = tests.createExample(TestedComponent, {
initial,
- form,
+ design,
});
-const initial2: any = {
-};
+const initial2: any = {};
-const form2: FlexibleForm_Deprecated<TargetObject> = {
- design: [
+const design2: FormDesign = {
+ type: "double-column",
+ sections: [
{
title: "Personal information" as TranslatedString,
fields: [
@@ -135,5 +133,5 @@ const form2: FlexibleForm_Deprecated<TargetObject> = {
export const NonMixingProperties = tests.createExample(TestedComponent, {
initial: initial2,
- form: form2,
+ design: design2,
});
diff --git a/packages/web-util/src/forms/InputArray.tsx
b/packages/web-util/src/forms/InputArray.tsx
index 60a13afae..f1fd00b11 100644
--- a/packages/web-util/src/forms/InputArray.tsx
+++ b/packages/web-util/src/forms/InputArray.tsx
@@ -1,23 +1,14 @@
import { TranslatedString } from "@gnu-taler/taler-util";
-import { Fragment, VNode, h } from "preact";
+import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
-import { FormProvider, UIFormProps } from "./FormProvider.js";
-import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import {
- convertUiField,
- RenderAllFieldsByUiConfig,
- UIFormField,
-} from "./forms.js";
-import { useField } from "./useField.js";
-import { UIFormElementConfig, UIHandlerId } from "./ui-form.js";
+import { getValueFromPath, useForm } from "../hooks/useForm.js";
import {
- FormErrors,
- undefinedIfEmpty,
- useFormState,
- useFormStateFromConfig,
- validateRequiredFields,
-} from "../hooks/useForm.js";
-import { getConverterById, useTranslationContext } from "../index.browser.js";
+ SingleColumnFormSectionUI,
+ useTranslationContext,
+} from "../index.browser.js";
+import { UIFormProps } from "./FormProvider.js";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
+import { UIFormElementConfig, UIHandlerId } from "./forms-types.js";
function Option({
label,
@@ -192,47 +183,20 @@ 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> = [];
- // Array.prototype.push.apply(shape, getShapeFromFields(fields));
- // Array.prototype.push.apply(requiredFields, getRequiredFields(fields));
-
- const [form, formState] = useFormStateFromConfig<FormType>(
- fields,
+ const form = useForm<FormType>(
+ {
+ type: "single-column",
+ 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);
+ newValue.splice(selectedIndex, 1, form.status.result);
onChange(newValue as any);
- }, [formState.result, selectedIndex]);
+ }, [form.status.result, selectedIndex]);
const { i18n } = useTranslationContext();
return (
@@ -247,7 +211,7 @@ export function InputArray<T extends object, K extends
keyof T>(
<div class="-space-y-px rounded-md bg-white ">
{list.map((v, idx) => {
const label =
- getValueDeeper(v, labelField.split(".")) ?? "<<incomplete>>";
+ getValueFromPath(v, labelField.split(".")) ?? "<<incomplete>>";
return (
<Option
label={label as TranslatedString}
@@ -312,9 +276,18 @@ export function InputArray<T extends object, K extends
keyof T>(
// >
<div class="px-4 py-6">
<div class="grid grid-cols-1 gap-y-8 ">
- <RenderAllFieldsByUiConfig
- fields={convertUiField(i18n, fields, form, getConverterById)}
+ <SingleColumnFormSectionUI
+ fields={fields}
+ handler={form.handler}
/>
+ {/* <RenderAllFieldsByUiConfig
+ fields={convertUiField(
+ i18n,
+ fields,
+ form.handler,
+ getConverterById,
+ )}
+ /> */}
</div>
</div>
// </FormProvider>
@@ -350,20 +323,3 @@ export function InputArray<T extends object, K extends
keyof T>(
</div>
);
}
-
-export function getValueDeeper(
- object: Record<string, any>,
- names: string[],
-): string {
- if (names.length === 0) {
- return object as any as string;
- }
- const [head, ...rest] = names;
- if (!head) {
- return getValueDeeper(object, rest);
- }
- if (object === undefined) {
- return "";
- }
- return getValueDeeper(object[head], rest);
-}
diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
index a00bcd6a1..f2c86779a 100644
--- a/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
@@ -21,11 +21,8 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "../tests/hook.js";
-import {
- FlexibleForm_Deprecated,
- DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
export default {
title: "Input Choice Horizontal",
@@ -44,8 +41,9 @@ const initial: TargetObject = {
comment: "0",
};
-const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [
+const design: FormDesign = {
+ type: "double-column",
+ sections: [
{
title: "this is a simple form" as TranslatedString,
fields: [
@@ -75,5 +73,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
export const SimpleComment = tests.createExample(TestedComponent, {
initial,
- form,
+ design,
});
diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.tsx
b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
index 86d3aa926..bc50b59ed 100644
--- a/packages/web-util/src/forms/InputChoiceHorizontal.tsx
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
@@ -2,7 +2,6 @@ import { TranslatedString } from "@gnu-taler/taler-util";
import { Fragment, VNode, h } from "preact";
import { UIFormProps } from "./FormProvider.js";
import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
export interface ChoiceH<V> {
@@ -16,10 +15,8 @@ export function InputChoiceHorizontal<T extends object, K
extends keyof T>(
} & UIFormProps<T, K>,
): VNode {
const { choices, label, tooltip, help, required, converter } = 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 />;
}
@@ -34,7 +31,7 @@ export function InputChoiceHorizontal<T extends object, K
extends keyof T>(
<fieldset class="mt-2">
<div class="isolate inline-flex rounded-md shadow-sm">
{choices.map((choice, idx) => {
- const convertedValue = converter?.fromStringUI(choice.value as any)
+ const convertedValue = converter?.fromStringUI(choice.value as
any);
const isFirst = idx === 0;
const isLast = idx === choices.length - 1;
let clazz =
@@ -62,7 +59,9 @@ export function InputChoiceHorizontal<T extends object, K
extends keyof T>(
class={clazz}
onClick={(e) => {
onChange(
- (value === choice.value ? undefined : convertedValue) as
any,
+ (value === choice.value
+ ? undefined
+ : convertedValue) as any,
);
}}
>
diff --git a/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
index 6e6a1a126..d08069893 100644
--- a/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
+++ b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
@@ -21,11 +21,8 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "../tests/hook.js";
-import {
- FlexibleForm_Deprecated,
- DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
export default {
title: "Input Choice Stacked",
@@ -44,8 +41,9 @@ const initial: TargetObject = {
comment: "some initial comment",
};
-const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [
+const design: FormDesign = {
+ type: "double-column",
+ sections: [
{
title: "this is a simple form" as TranslatedString,
fields: [
@@ -75,5 +73,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
export const SimpleComment = tests.createExample(TestedComponent, {
initial,
- form,
+ design,
});
diff --git a/packages/web-util/src/forms/InputChoiceStacked.tsx
b/packages/web-util/src/forms/InputChoiceStacked.tsx
index 34b06ec0c..2415cd5db 100644
--- a/packages/web-util/src/forms/InputChoiceStacked.tsx
+++ b/packages/web-util/src/forms/InputChoiceStacked.tsx
@@ -2,7 +2,6 @@ import { TranslatedString } from "@gnu-taler/taler-util";
import { Fragment, VNode, h } from "preact";
import { UIFormProps } from "./FormProvider.js";
import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
export interface ChoiceS<V> {
@@ -29,7 +28,6 @@ export function InputChoiceStacked<T extends object, K
extends keyof T>(
converter,
} = props;
- //FIXME: remove deprecated
const { value, onChange, state } =
props.handler ?? noHandlerPropsAndNoContextForField(props.name);
diff --git a/packages/web-util/src/forms/InputFile.stories.tsx
b/packages/web-util/src/forms/InputFile.stories.tsx
index 75b1fb918..24f983ad4 100644
--- a/packages/web-util/src/forms/InputFile.stories.tsx
+++ b/packages/web-util/src/forms/InputFile.stories.tsx
@@ -21,11 +21,8 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "../tests/hook.js";
-import {
- FlexibleForm_Deprecated,
- DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
export default {
title: "Input File",
@@ -38,14 +35,15 @@ export namespace Simplest {
}
type TargetObject = {
- comment: string;
+ file?: string;
};
const initial: TargetObject = {
- comment: "some initial comment",
+ file: undefined,
};
-const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [
+const design: FormDesign = {
+ type: "double-column",
+ sections: [
{
title: "this is a simple form" as TranslatedString,
fields: [
@@ -53,7 +51,7 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
type: "file",
label: "label of the field" as TranslatedString,
required: true,
- id: "comment" as UIHandlerId,
+ id: "file" as UIHandlerId,
accept: ".png",
tooltip:
"this is a very long tooltip that explain what the field does
without being short" as TranslatedString,
@@ -64,7 +62,7 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
],
};
-export const SimpleComment = tests.createExample(TestedComponent, {
+export const AcceptPNG = tests.createExample(TestedComponent, {
initial,
- form,
+ design,
});
diff --git a/packages/web-util/src/forms/InputFile.tsx
b/packages/web-util/src/forms/InputFile.tsx
index cd0a96d1c..78eb9cf29 100644
--- a/packages/web-util/src/forms/InputFile.tsx
+++ b/packages/web-util/src/forms/InputFile.tsx
@@ -2,23 +2,13 @@ import { Fragment, VNode, h } from "preact";
import { UIFormProps } from "./FormProvider.js";
import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
export function InputFile<T extends object, K extends keyof T>(
props: { maxBites: number; accept?: string } & UIFormProps<T, K>,
): VNode {
- const {
- label,
- tooltip,
- required,
- help: propsHelp,
- maxBites,
- accept,
- } = props;
- //FIXME: remove deprecated
- const fieldCtx = useField<T, K>(props.name);
+ const { label, tooltip, required, help: propsHelp, maxBites, accept } =
props;
const { value, onChange, state } =
- props.handler ?? fieldCtx ??
noHandlerPropsAndNoContextForField(props.name);
+ props.handler ?? noHandlerPropsAndNoContextForField(props.name);
const help = propsHelp ?? state.help;
if (state.hidden) {
diff --git a/packages/web-util/src/forms/InputInteger.stories.tsx
b/packages/web-util/src/forms/InputInteger.stories.tsx
index 76d9e8668..369be670d 100644
--- a/packages/web-util/src/forms/InputInteger.stories.tsx
+++ b/packages/web-util/src/forms/InputInteger.stories.tsx
@@ -21,11 +21,8 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "../tests/hook.js";
-import {
- FlexibleForm_Deprecated,
- DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
export default {
title: "Input Integer",
@@ -38,15 +35,16 @@ const initial: TargetObject = {
age: 5,
};
-const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [
+const design: FormDesign = {
+ type: "double-column",
+ sections: [
{
title: "this is a simple form" as TranslatedString,
fields: [
{
type: "integer",
- label: "label of the field" as TranslatedString,
- id: "comment" as UIHandlerId,
+ label: "Age" as TranslatedString,
+ id: "age" as UIHandlerId,
tooltip: "just numbers" as TranslatedString,
},
],
@@ -56,5 +54,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
export const SimpleComment = tests.createExample(TestedComponent, {
initial,
- form,
+ design,
});
diff --git a/packages/web-util/src/forms/InputLine.stories.tsx
b/packages/web-util/src/forms/InputLine.stories.tsx
deleted file mode 100644
index e5209f4d4..000000000
--- a/packages/web-util/src/forms/InputLine.stories.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2022 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/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { TranslatedString } from "@gnu-taler/taler-util";
-import * as tests from "../tests/hook.js";
-import {
- FlexibleForm_Deprecated,
- DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
-
-export default {
- title: "Input Line",
-};
-
-export namespace Simplest {
- export interface Form {
- comment: string;
- }
-}
-
-type TargetObject = {
- comment: string;
-};
-const initial: TargetObject = {
- comment: "some initial comment",
-};
-
-const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [
- {
- title: "this is a simple form" as TranslatedString,
- fields: [
- {
- type: "text",
- label: "label of the field" as TranslatedString,
- id: "comment" as UIHandlerId,
- },
- ],
- },
- ],
-};
-
-export const SimpleComment = tests.createExample(TestedComponent, {
- initial,
- form,
-});
diff --git a/packages/web-util/src/forms/InputLine.tsx
b/packages/web-util/src/forms/InputLine.tsx
index 7eceea88e..6f160abf4 100644
--- a/packages/web-util/src/forms/InputLine.tsx
+++ b/packages/web-util/src/forms/InputLine.tsx
@@ -2,7 +2,6 @@ import { TranslatedString } from "@gnu-taler/taler-util";
import { ComponentChildren, Fragment, VNode, h } from "preact";
import { Addon, UIFormProps } from "./FormProvider.js";
import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
-import { useField } from "./useField.js";
//@ts-ignore
const TooltipIcon = (
diff --git a/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
index 9cb997490..a39f5fb26 100644
--- a/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
+++ b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
@@ -21,11 +21,8 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "../tests/hook.js";
-import {
- FlexibleForm_Deprecated,
- DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
export default {
title: "Input Select Multiple",
@@ -46,14 +43,15 @@ const initial: TargetObject = {
things: [],
};
-const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [
+const design: FormDesign = {
+ type: "double-column",
+ sections: [
{
title: "this is a simple form" as TranslatedString,
fields: [
{
type: "selectMultiple",
- label: "allow diplicates" as TranslatedString,
+ label: "allow duplicates" as TranslatedString,
id: "pets" as UIHandlerId,
placeholder: "search..." as TranslatedString,
choices: [
@@ -99,5 +97,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
export const SimpleComment = tests.createExample(TestedComponent, {
initial,
- form,
+ design,
});
diff --git a/packages/web-util/src/forms/InputSelectMultiple.tsx
b/packages/web-util/src/forms/InputSelectMultiple.tsx
index 1bcf85061..1a52ce9d1 100644
--- a/packages/web-util/src/forms/InputSelectMultiple.tsx
+++ b/packages/web-util/src/forms/InputSelectMultiple.tsx
@@ -4,7 +4,6 @@ import { UIFormProps } from "./FormProvider.js";
import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
import { ChoiceS } from "./InputChoiceStacked.js";
import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
export function InputSelectMultiple<T extends object, K extends keyof T>(
props: {
@@ -13,11 +12,18 @@ export function InputSelectMultiple<T extends object, K
extends keyof T>(
max?: number;
} & UIFormProps<T, K>,
): VNode {
- const { converter, label, choices, placeholder, tooltip, required, unique,
max } = props;
- //FIXME: remove deprecated
- const fieldCtx = useField<T, K>(props.name);
+ const {
+ converter,
+ label,
+ choices,
+ placeholder,
+ tooltip,
+ required,
+ unique,
+ max,
+ } = props;
const { value, onChange, state } =
- props.handler ?? fieldCtx ??
noHandlerPropsAndNoContextForField(props.name);
+ props.handler ?? noHandlerPropsAndNoContextForField(props.name);
const [filter, setFilter] = useState<string | undefined>(undefined);
const regex = new RegExp(`.*${filter}.*`, "i");
diff --git a/packages/web-util/src/forms/InputSelectOne.stories.tsx
b/packages/web-util/src/forms/InputSelectOne.stories.tsx
index 25b96f0c0..ba14d180d 100644
--- a/packages/web-util/src/forms/InputSelectOne.stories.tsx
+++ b/packages/web-util/src/forms/InputSelectOne.stories.tsx
@@ -21,11 +21,8 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "../tests/hook.js";
-import {
- FlexibleForm_Deprecated,
- DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
export default {
title: "Input Select One",
@@ -44,8 +41,9 @@ const initial: TargetObject = {
things: "one",
};
-const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [
+const design: FormDesign = {
+ type: "double-column",
+ sections: [
{
title: "this is a simple form" as TranslatedString,
fields: [
@@ -76,5 +74,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
export const SimpleComment = tests.createExample(TestedComponent, {
initial,
- form,
+ design,
});
diff --git a/packages/web-util/src/forms/InputSelectOne.tsx
b/packages/web-util/src/forms/InputSelectOne.tsx
index 26f887b08..331f4720b 100644
--- a/packages/web-util/src/forms/InputSelectOne.tsx
+++ b/packages/web-util/src/forms/InputSelectOne.tsx
@@ -1,10 +1,9 @@
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { UIFormProps } from "./FormProvider.js";
+import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
import { ChoiceS } from "./InputChoiceStacked.js";
import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
-import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
export function InputSelectOne<T extends object, K extends keyof T>(
props: {
@@ -12,11 +11,8 @@ export function InputSelectOne<T extends object, K extends
keyof T>(
} & UIFormProps<T, K>,
): VNode {
const { label, choices, placeholder, tooltip, required } = props;
- //FIXME: remove deprecated
- const fieldCtx = useField<T, K>(props.name);
const { value, onChange } =
- props.handler ?? fieldCtx ??
noHandlerPropsAndNoContextForField(props.name);
-
+ props.handler ?? noHandlerPropsAndNoContextForField(props.name);
const [filter, setFilter] = useState<string | undefined>(undefined);
const regex = new RegExp(`.*${filter}.*`, "i");
diff --git a/packages/web-util/src/forms/InputText.stories.tsx
b/packages/web-util/src/forms/InputText.stories.tsx
index 6d0db938b..e4ea621a2 100644
--- a/packages/web-util/src/forms/InputText.stories.tsx
+++ b/packages/web-util/src/forms/InputText.stories.tsx
@@ -21,11 +21,8 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "../tests/hook.js";
-import {
- FlexibleForm_Deprecated,
- DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
export default {
title: "Input Text",
@@ -44,8 +41,9 @@ const initial: TargetObject = {
comment: "some initial comment",
};
-const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [
+const design: FormDesign = {
+ type: "double-column",
+ sections: [
{
title: "this is a simple form" as TranslatedString,
fields: [
@@ -61,5 +59,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
export const SimpleComment = tests.createExample(TestedComponent, {
initial,
- form,
+ design,
});
diff --git a/packages/web-util/src/forms/InputTextArea.stories.tsx
b/packages/web-util/src/forms/InputTextArea.stories.tsx
index a3b135c36..5d3cf4f12 100644
--- a/packages/web-util/src/forms/InputTextArea.stories.tsx
+++ b/packages/web-util/src/forms/InputTextArea.stories.tsx
@@ -21,11 +21,8 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "../tests/hook.js";
-import {
- DefaultForm as TestedComponent,
- FlexibleForm_Deprecated,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
export default {
title: "Input Text Area",
@@ -44,13 +41,14 @@ const initial: TargetObject = {
comment: "some initial comment",
};
-const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [
+const design: FormDesign = {
+ type: "double-column",
+ sections: [
{
title: "this is a simple form" as TranslatedString,
fields: [
{
- type: "text",
+ type: "textArea",
label: "label of the field" as TranslatedString,
id: "comment" as UIHandlerId,
},
@@ -61,5 +59,5 @@ const form: FlexibleForm_Deprecated<TargetObject> = {
export const SimpleComment = tests.createExample(TestedComponent, {
initial,
- form,
+ design,
});
diff --git a/packages/web-util/src/forms/InputToggle.stories.tsx
b/packages/web-util/src/forms/InputToggle.stories.tsx
index 75a81edd3..c4ed868ef 100644
--- a/packages/web-util/src/forms/InputToggle.stories.tsx
+++ b/packages/web-util/src/forms/InputToggle.stories.tsx
@@ -21,11 +21,8 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "../tests/hook.js";
-import {
- FlexibleForm_Deprecated,
- DefaultForm as TestedComponent,
-} from "./DefaultForm.js";
-import { UIHandlerId } from "./ui-form.js";
+import { DefaultForm as TestedComponent } from "./forms-ui.js";
+import { FormDesign, UIHandlerId } from "./forms-types.js";
export default {
title: "Input Toggle",
@@ -38,29 +35,37 @@ export namespace Simplest {
}
type TargetObject = {
- comment: string;
+ accept: boolean;
};
const initial: TargetObject = {
- comment: "some initial comment",
+ accept: true,
};
-const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [
- {
- title: "this is a simple form" as TranslatedString,
- fields: [
- {
- type: "toggle",
- label: "label of the field" as TranslatedString,
- threeState: false,
- id: "comment" as UIHandlerId,
- },
- ],
- },
- ],
-};
+export const SimpleUsage = tests.createExample(TestedComponent, {
+ initial,
+ design: {
+ type: "single-column",
+ fields: [
+ {
+ type: "toggle",
+ label: "do you accept?" as TranslatedString,
+ id: "accept" as UIHandlerId,
+ },
+ ],
+ },
+});
-export const SimpleComment = tests.createExample(TestedComponent, {
+export const WithThreeState = tests.createExample(TestedComponent, {
initial,
- form,
+ design: {
+ type: "single-column",
+ fields: [
+ {
+ type: "toggle",
+ label: "do you accept?" as TranslatedString,
+ threeState: true,
+ id: "accept" as UIHandlerId,
+ },
+ ],
+ },
});
diff --git a/packages/web-util/src/forms/InputToggle.tsx
b/packages/web-util/src/forms/InputToggle.tsx
index d310bc0f2..b1c800ad3 100644
--- a/packages/web-util/src/forms/InputToggle.tsx
+++ b/packages/web-util/src/forms/InputToggle.tsx
@@ -2,7 +2,6 @@ import { VNode, h } from "preact";
import { UIFormProps } from "./FormProvider.js";
import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
-import { useField } from "./useField.js";
export function InputToggle<T extends object, K extends keyof T>(
props: { threeState: boolean } & UIFormProps<T, K>,
@@ -19,14 +18,13 @@ export function InputToggle<T extends object, K extends
keyof T>(
converter,
threeState,
} = props;
- //FIXME: remove deprecated
- const fieldCtx = useField<T, K>(props.name);
const { value, onChange } =
- props.handler ?? fieldCtx ??
noHandlerPropsAndNoContextForField(props.name);
+ props.handler ?? noHandlerPropsAndNoContextForField(props.name);
const isOn = !!value;
return (
<div class="sm:col-span-6">
+ v = {JSON.stringify({ value, isOn })}
<div class="flex items-center justify-between">
<LabelWithTooltipMaybeRequired
label={label}
@@ -51,7 +49,7 @@ export function InputToggle<T extends object, K extends keyof
T>(
<span
data-state={isOn ? "on" : value === undefined ? "undefined" :
"off"}
class="translate-x-6 data-[state=off]:translate-x-0
data-[state=undefined]:translate-x-3 pointer-events-none inline-block h-5 w-5
transform rounded-full bg-white shadow ring-0 transition duration-200
ease-in-out"
- ></span>
+ ></span>
</button>
</div>
</div>
diff --git a/packages/web-util/src/forms/converter.ts
b/packages/web-util/src/forms/converter.ts
deleted file mode 100644
index eee891776..000000000
--- a/packages/web-util/src/forms/converter.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- 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,
- Amounts,
- TalerExchangeApi,
-} from "@gnu-taler/taler-util";
-import { format, parse } from "date-fns";
-import { StringConverter } from "./FormProvider.js";
-
-export const amlStateConverter = {
- toStringUI: stringifyAmlState,
- fromStringUI: parseAmlState,
-};
-
-function stringifyAmlState(s: TalerExchangeApi.AmlState | undefined): string {
- if (s === undefined) return "";
- switch (s) {
- case TalerExchangeApi.AmlState.normal:
- return "normal";
- case TalerExchangeApi.AmlState.pending:
- return "pending";
- case TalerExchangeApi.AmlState.frozen:
- return "frozen";
- }
-}
-
-function parseAmlState(s: string | undefined): TalerExchangeApi.AmlState {
- switch (s) {
- case "normal":
- return TalerExchangeApi.AmlState.normal;
- case "pending":
- return TalerExchangeApi.AmlState.pending;
- case "frozen":
- return TalerExchangeApi.AmlState.frozen;
- default:
- throw Error(`unknown AML state: ${s}`);
- }
-}
-
-const nullConverter: StringConverter<string> = {
- fromStringUI(v: string | undefined): string {
- return v ?? "";
- },
- toStringUI(v: unknown): string {
- return v as string;
- },
-};
-
-function amountConverter(config: any): StringConverter<AmountJson> {
- const currency = config["currency"];
- if (!currency || typeof currency !== "string") {
- throw Error(`amount converter needs a currency`);
- }
- return {
- fromStringUI(v: string | undefined): AmountJson {
- // FIXME: requires currency
- return (
- Amounts.parse(`${currency}:${v}`) ?? Amounts.zeroOfCurrency(currency)
- );
- },
- toStringUI(v: unknown): string {
- return v === undefined ? "" : Amounts.stringifyValue(v as AmountJson);
- },
- };
-}
-
-function absTimeConverter(config: any): StringConverter<AbsoluteTime> {
- const pattern = config["pattern"];
- if (!pattern || typeof pattern !== "string") {
- throw Error(`absTime converter needs a pattern`);
- }
- return {
- fromStringUI(v: string | undefined): AbsoluteTime {
- if (v === undefined) {
- return AbsoluteTime.never();
- }
- try {
- const time = parse(v, pattern, new Date());
- return AbsoluteTime.fromMilliseconds(time.getTime());
- } catch (e) {
- return AbsoluteTime.never();
- }
- },
- toStringUI(v: unknown): string {
- if (v === undefined) return "";
- const d = v as AbsoluteTime;
- if (d.t_ms === "never") return "never";
- try {
- return format(d.t_ms, pattern);
- } catch (e) {
- return "";
- }
- },
- };
-}
-
-export function getConverterById(
- id: string | undefined,
- config: unknown,
-): StringConverter<unknown> {
- if (id === "Taler.AbsoluteTime") {
- // @ts-expect-error check this
- return absTimeConverter(config);
- }
- if (id === "Taler.Amount") {
- // @ts-expect-error check this
- return amountConverter(config);
- }
- if (id === "TalerExchangeApi.AmlState") {
- // @ts-expect-error check this
- return amlStateConverter;
- }
- return nullConverter as StringConverter<unknown>;
-}
diff --git a/packages/web-util/src/forms/field-types.ts
b/packages/web-util/src/forms/field-types.ts
new file mode 100644
index 000000000..809467b24
--- /dev/null
+++ b/packages/web-util/src/forms/field-types.ts
@@ -0,0 +1,117 @@
+import { h as create, Fragment, VNode } from "preact";
+import { Caption } from "./Caption.js";
+import { Group } from "./Group.js";
+import { InputAbsoluteTime } from "./InputAbsoluteTime.js";
+import { InputAmount } from "./InputAmount.js";
+import { InputArray } from "./InputArray.js";
+import { InputChoiceHorizontal } from "./InputChoiceHorizontal.js";
+import { InputChoiceStacked } from "./InputChoiceStacked.js";
+import { InputFile } from "./InputFile.js";
+import { InputInteger } from "./InputInteger.js";
+import { InputSelectMultiple } from "./InputSelectMultiple.js";
+import { InputSelectOne } from "./InputSelectOne.js";
+import { InputText } from "./InputText.js";
+import { InputTextArea } from "./InputTextArea.js";
+import { InputToggle } from "./InputToggle.js";
+import { Addon, StringConverter, UIFieldHandler } from "./FormProvider.js";
+import {
+ InternationalizationAPI,
+ UIFieldElementDescription,
+} from "../index.browser.js";
+import { assertUnreachable, TranslatedString } from "@gnu-taler/taler-util";
+import { UIFormFieldBaseConfig, UIFormElementConfig } from "./forms-types.js";
+import { HtmlIframe } from "./HtmlIframe.js";
+import { DownloadLink } from "./DownloadLink.js";
+/**
+ * Constrain the type with the ui props
+ */
+type FieldType<T extends object = any, K extends keyof T = any> = {
+ group: Parameters<typeof Group>[0];
+ caption: Parameters<typeof Caption>[0];
+ "download-link": Parameters<typeof DownloadLink>[0];
+ htmlIframe: Parameters<typeof HtmlIframe>[0];
+ array: Parameters<typeof InputArray<T, K>>[0];
+ file: Parameters<typeof InputFile<T, K>>[0];
+ selectOne: Parameters<typeof InputSelectOne<T, K>>[0];
+ selectMultiple: Parameters<typeof InputSelectMultiple<T, K>>[0];
+ text: Parameters<typeof InputText<T, K>>[0];
+ textArea: Parameters<typeof InputTextArea<T, K>>[0];
+ choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0];
+ choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0];
+ absoluteTimeText: Parameters<typeof InputAbsoluteTime<T, K>>[0];
+ integer: Parameters<typeof InputInteger<T, K>>[0];
+ toggle: Parameters<typeof InputToggle<T, K>>[0];
+ amount: Parameters<typeof InputAmount<T, K>>[0];
+};
+
+/**
+ * List all the form fields so typescript can type-check the form instance
+ */
+export type UIFormField =
+ | { type: "group"; properties: FieldType["group"] }
+ | { type: "caption"; properties: FieldType["caption"] }
+ | { type: "download-link"; properties: FieldType["download-link"] }
+ | { type: "htmlIframe"; properties: FieldType["htmlIframe"] }
+ | { type: "array"; properties: FieldType["array"] }
+ | { type: "file"; properties: FieldType["file"] }
+ | { type: "amount"; properties: FieldType["amount"] }
+ | { type: "selectOne"; properties: FieldType["selectOne"] }
+ | {
+ type: "selectMultiple";
+ properties: FieldType["selectMultiple"];
+ }
+ | { type: "text"; properties: FieldType["text"] }
+ | { type: "textArea"; properties: FieldType["textArea"] }
+ | {
+ type: "choiceStacked";
+ properties: FieldType["choiceStacked"];
+ }
+ | {
+ type: "choiceHorizontal";
+ properties: FieldType["choiceHorizontal"];
+ }
+ | { type: "integer"; properties: FieldType["integer"] }
+ | { type: "toggle"; properties: FieldType["toggle"] }
+ | {
+ type: "absoluteTimeText";
+ properties: FieldType["absoluteTimeText"];
+ };
+
+export type FieldComponentFunction<key extends keyof FieldType> = (
+ props: FieldType[key],
+) => VNode;
+
+type UIFormFieldMap = {
+ [key in keyof FieldType]: FieldComponentFunction<key>;
+};
+
+/**
+ * Maps input type with component implementation
+ */
+export const UIFormConfiguration: UIFormFieldMap = {
+ group: Group,
+ "download-link": DownloadLink,
+ caption: Caption,
+ htmlIframe: HtmlIframe,
+ //@ts-ignore
+ array: InputArray,
+ text: InputText,
+ //@ts-ignore
+ file: InputFile,
+ textArea: InputTextArea,
+ //@ts-ignore
+ absoluteTimeText: InputAbsoluteTime,
+ //@ts-ignore
+ choiceStacked: InputChoiceStacked,
+ //@ts-ignore
+ choiceHorizontal: InputChoiceHorizontal,
+ integer: InputInteger,
+ //@ts-ignore
+ selectOne: InputSelectOne,
+ //@ts-ignore
+ selectMultiple: InputSelectMultiple,
+ //@ts-ignore
+ toggle: InputToggle,
+ //@ts-ignore
+ amount: InputAmount,
+};
diff --git a/packages/web-util/src/forms/ui-form.ts
b/packages/web-util/src/forms/forms-types.ts
similarity index 92%
rename from packages/web-util/src/forms/ui-form.ts
rename to packages/web-util/src/forms/forms-types.ts
index 14f22cc1f..e604ce274 100644
--- a/packages/web-util/src/forms/ui-form.ts
+++ b/packages/web-util/src/forms/forms-types.ts
@@ -3,7 +3,6 @@ import {
buildCodecForUnion,
Codec,
codecForBoolean,
- codecForCanonBaseUrl,
codecForConstString,
codecForLazy,
codecForList,
@@ -17,17 +16,22 @@ import {
TalerProtocolTimestamp,
TranslatedString,
} from "@gnu-taler/taler-util";
-import { InternationalizationAPI } from "../index.browser.js";
-export type FormConfiguration = DoubleColumnForm | SingleColumnForm;
+export type FormDesign = DoubleColumnFormDesign | SingleColumnFormDesign;
-export type DoubleColumnForm = {
+/**
+ * form with composed by multiple sections
+ */
+export type DoubleColumnFormDesign = {
type: "double-column";
- design: DoubleColumnFormSection[];
+ sections: DoubleColumnFormSection[];
// behavior?: (form: Partial<T>) => FormState<T>;
};
-export type SingleColumnForm = {
+/**
+ * single section form
+ */
+export type SingleColumnFormDesign = {
type: "single-column";
fields: UIFormElementConfig[];
};
@@ -38,11 +42,6 @@ export type DoubleColumnFormSection = {
fields: UIFormElementConfig[];
};
-// export interface BaseForm {
-// state: TalerExchangeApi.AmlState;
-// threshold: AmountJson;
-// }
-
export type UIFormElementConfig =
| UIFormElementGroup
| UIFormElementCaption
@@ -373,24 +372,24 @@ const codecForDoubleColumnFormSection = ():
Codec<DoubleColumnFormSection> =>
.property("fields", codecForList(codecForUiFormField()))
.build("DoubleColumnFormSection");
-const codecForDoubleColumnForm = (): Codec<DoubleColumnForm> =>
- buildCodecForObject<DoubleColumnForm>()
+const codecForDoubleColumnFormDesign = (): Codec<DoubleColumnFormDesign> =>
+ buildCodecForObject<DoubleColumnFormDesign>()
.property("type", codecForConstString("double-column"))
- .property("design", codecForList(codecForDoubleColumnFormSection()))
- .build("DoubleColumnForm");
+ .property("sections", codecForList(codecForDoubleColumnFormSection()))
+ .build("DoubleColumnFormDesign");
-const codecForSingleColumnForm = (): Codec<SingleColumnForm> =>
- buildCodecForObject<SingleColumnForm>()
+const codecForSingleColumnFormDesign = (): Codec<SingleColumnFormDesign> =>
+ buildCodecForObject<SingleColumnFormDesign>()
.property("type", codecForConstString("single-column"))
.property("fields", codecForList(codecForUiFormField()))
- .build("SingleColumnForm");
+ .build("SingleColumnFormDesign");
-const codecForFormConfiguration = (): Codec<FormConfiguration> =>
- buildCodecForUnion<FormConfiguration>()
+const codecForFormDesign = (): Codec<FormDesign> =>
+ buildCodecForUnion<FormDesign>()
.discriminateOn("type")
- .alternative("double-column", codecForDoubleColumnForm())
- .alternative("single-column", codecForSingleColumnForm())
- .build<FormConfiguration>("FormConfiguration");
+ .alternative("double-column", codecForDoubleColumnFormDesign())
+ .alternative("single-column", codecForSingleColumnFormDesign())
+ .build<FormDesign>("FormDesign");
const codecForFormMetadata = (): Codec<FormMetadata> =>
buildCodecForObject<FormMetadata>()
@@ -398,7 +397,7 @@ const codecForFormMetadata = (): Codec<FormMetadata> =>
.property("description", codecOptional(codecForString()))
.property("id", codecForString())
.property("version", codecForNumber())
- .property("config", codecForFormConfiguration())
+ .property("config", codecForFormDesign())
.build("FormMetadata");
export const codecForUIForms = (): Codec<UiForms> =>
@@ -411,7 +410,7 @@ export type FormMetadata = {
description?: string;
id: string;
version: number;
- config: FormConfiguration;
+ config: FormDesign;
};
export interface UiForms {
diff --git a/packages/web-util/src/forms/forms-ui.tsx
b/packages/web-util/src/forms/forms-ui.tsx
new file mode 100644
index 000000000..55ee29cb3
--- /dev/null
+++ b/packages/web-util/src/forms/forms-ui.tsx
@@ -0,0 +1,134 @@
+import { Fragment, h, h as create, VNode } from "preact";
+import { FormHandler, useForm } from "../hooks/useForm.js";
+// import { getConverterById, useTranslationContext } from
"../index.browser.js";
+import { convertFormConfigToUiField } from "./forms-utils.js";
+import {
+ DoubleColumnFormSection,
+ FormDesign,
+ UIFormElementConfig,
+} from "./forms-types.js";
+import {
+ FieldComponentFunction,
+ UIFormConfiguration,
+ UIFormField,
+} from "./field-types.js";
+import { useTranslationContext } from "../index.browser.js";
+
+export function DefaultForm<T>({
+ design,
+ initial,
+}: {
+ design: FormDesign;
+ initial: object;
+}): VNode {
+ const { handler, status } = useForm(design, initial);
+
+ return (
+ <div>
+ <FormUI design={design} handler={handler} />
+ <pre class="break-all whitespace-pre-wrap">
+ {JSON.stringify({ status }, undefined, 2)}
+ </pre>
+ </div>
+ );
+}
+
+/**
+ * FIXME: formDesign should be embedded in formHandler
+ * @param param0
+ * @returns
+ */
+export function FormUI<T>({
+ design,
+ handler,
+}: {
+ design: FormDesign;
+ handler: FormHandler<T>;
+}): VNode {
+ switch (design.type) {
+ case "double-column": {
+ const ui = design.sections.map((section, i) => {
+ if (!section) return <Fragment />;
+ return (
+ <DoubleColumnFormSectionUI section={section} handler={handler} />
+ );
+ });
+ return <Fragment>{ui}</Fragment>;
+ }
+ case "single-column": {
+ return (
+ <SingleColumnFormSectionUI fields={design.fields} handler={handler} />
+ );
+ }
+ }
+}
+
+export function DoubleColumnFormSectionUI<T>({
+ section,
+ handler,
+}: {
+ handler: FormHandler<T>;
+ section: DoubleColumnFormSection;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div 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
+ fields={convertFormConfigToUiField(i18n, section.fields,
handler)}
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+export function SingleColumnFormSectionUI<T>({
+ fields,
+ handler,
+}: {
+ handler: FormHandler<T>;
+ fields: UIFormElementConfig[];
+}): VNode {
+ const { i18n } = useTranslationContext();
+ 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={convertFormConfigToUiField(i18n, fields, handler)}
+ />
+ </div>
+ </div>
+ </div>
+ );
+}
+
+export function RenderAllFieldsByUiConfig({
+ fields,
+}: {
+ fields: UIFormField[];
+}): VNode {
+ return create(
+ Fragment,
+ {},
+ fields.map((field, i) => {
+ const Component = UIFormConfiguration[
+ field.type
+ ] as FieldComponentFunction<any>;
+ return Component(field.properties);
+ }),
+ );
+}
diff --git a/packages/web-util/src/forms/forms-utils.ts
b/packages/web-util/src/forms/forms-utils.ts
new file mode 100644
index 000000000..5322913cf
--- /dev/null
+++ b/packages/web-util/src/forms/forms-utils.ts
@@ -0,0 +1,382 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ assertUnreachable,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+ InternationalizationAPI,
+ UIFieldElementDescription,
+} from "../index.browser.js";
+import { Addon, StringConverter, UIFieldHandler } from "./FormProvider.js";
+import { UIFormElementConfig, UIFormFieldBaseConfig } from "./forms-types.js";
+import { UIFormField } from "./field-types.js";
+import { format, parse } from "date-fns";
+
+/**
+ * convert field configuration to render function
+ * FIXME: change this mapping for something not so insane
+ *
+ * @param i18n_
+ * @param fieldConfig
+ * @param form
+ * @returns
+ */
+export function convertFormConfigToUiField(
+ i18n_: InternationalizationAPI,
+ fieldConfig: UIFormElementConfig[],
+ form: object,
+): UIFormField[] {
+ return fieldConfig.map((config) => {
+ // NON input fields
+ switch (config.type) {
+ case "caption": {
+ const resp: UIFormField = {
+ type: config.type,
+ properties: converBaseFieldsProps(i18n_, config),
+ };
+ return resp;
+ }
+ case "download-link": {
+ const resp: UIFormField = {
+ type: config.type,
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ label: i18n_.str`${config.label}`,
+ url: config.url,
+ media: config.media,
+ },
+ };
+ return resp;
+ }
+ case "htmlIframe": {
+ const resp: UIFormField = {
+ type: config.type,
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ url: config.url,
+ },
+ };
+ return resp;
+ }
+ case "group": {
+ const resp: UIFormField = {
+ type: config.type,
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ fields: convertFormConfigToUiField(i18n_, config.fields, form),
+ },
+ };
+ return resp;
+ }
+ }
+ // Input Fields
+ switch (config.type) {
+ case "array": {
+ return {
+ type: "array",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(
+ form,
+ config,
+ getConverterByFieldType(config.type, config),
+ ),
+ labelField: config.labelFieldId,
+ fields: config.fields,
+ // convertFormConfigToUiField(
+ // i18n_,
+ // config.fields,
+ // (form as any)[config.id].value ?? {},
+ // getConverterByFieldType,
+ // ),
+ },
+ } as UIFormField;
+ }
+ case "absoluteTimeText": {
+ return {
+ type: "absoluteTimeText",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(
+ form,
+ config,
+ getConverterByFieldType(config.type, config),
+ ),
+ },
+ } as UIFormField;
+ }
+ case "amount": {
+ return {
+ type: "amount",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(
+ form,
+ config,
+ getConverterByFieldType(config.type, config),
+ ),
+ currency: config.currency,
+ },
+ } as UIFormField;
+ }
+ case "choiceHorizontal": {
+ return {
+ type: "choiceHorizontal",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(
+ form,
+ config,
+ getConverterByFieldType(config.type, config),
+ ),
+ choices: config.choices,
+ },
+ } as UIFormField;
+ }
+ case "choiceStacked": {
+ return {
+ type: "choiceStacked",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(
+ form,
+ config,
+ getConverterByFieldType(config.type, config),
+ ),
+ choices: config.choices,
+ },
+ } as UIFormField;
+ }
+ case "file": {
+ return {
+ type: "file",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(
+ form,
+ config,
+ getConverterByFieldType(config.type, config),
+ ),
+ accept: config.accept,
+ maxBites: config.maxBytes,
+ },
+ } as UIFormField;
+ }
+ case "integer": {
+ return {
+ type: "integer",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(
+ form,
+ config,
+ getConverterByFieldType(config.type, config),
+ ),
+ },
+ } as UIFormField;
+ }
+ case "selectMultiple": {
+ return {
+ type: "selectMultiple",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(
+ form,
+ config,
+ getConverterByFieldType(config.type, config),
+ ),
+ choices: config.choices,
+ unique: config.unique,
+ },
+ } as UIFormField;
+ }
+ case "selectOne": {
+ return {
+ type: "selectOne",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(
+ form,
+ config,
+ getConverterByFieldType(config.type, config),
+ ),
+ choices: config.choices,
+ },
+ } as UIFormField;
+ }
+ case "text": {
+ return {
+ type: "text",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(
+ form,
+ config,
+ getConverterByFieldType(config.type, config),
+ ),
+ },
+ } as UIFormField;
+ }
+ case "textArea": {
+ return {
+ type: "textArea",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(
+ form,
+ config,
+ getConverterByFieldType(config.type, config),
+ ),
+ },
+ } as UIFormField;
+ }
+ case "toggle": {
+ return {
+ type: "toggle",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config),
+ ...converInputFieldsProps(
+ form,
+ config,
+ getConverterByFieldType(config.type, config),
+ ),
+ threeState: config.threeState,
+ },
+ } as UIFormField;
+ }
+ default: {
+ assertUnreachable(config);
+ }
+ }
+ });
+}
+
+function getAddonById(_id: string | undefined): Addon {
+ return undefined!;
+}
+
+function getConverterByFieldType(
+ fieldType: string | undefined,
+ config: unknown,
+): StringConverter<unknown> {
+ if (fieldType === "absoluteTimeText") {
+ // @ts-expect-error check this
+ return absTimeConverter(config);
+ }
+ if (fieldType === "amount") {
+ // @ts-expect-error check this
+ return amountConverter(config);
+ }
+ if (fieldType === "TalerExchangeApi.AmlState") {
+ // @ts-expect-error check this
+ return amlStateConverter;
+ }
+ return nullConverter as StringConverter<unknown>;
+}
+
+function converInputFieldsProps(
+ form: object,
+ p: UIFormFieldBaseConfig,
+ converter: StringConverter<unknown>,
+) {
+ const names = p.id.split(".");
+ // console.log("NAMES", names, getValueDeeper2(form, names), form)
+ return {
+ converter,
+ handler: getValueDeeper2(form, names),
+ required: p.required,
+ disabled: p.disabled,
+ name: names[names.length - 1],
+ help: p.help,
+ placeholder: p.placeholder,
+ tooltip: p.tooltip,
+ label: p.label as TranslatedString,
+ };
+}
+
+function converBaseFieldsProps(
+ i18n_: InternationalizationAPI,
+ p: UIFieldElementDescription,
+) {
+ return {
+ after: getAddonById(p.addonAfterId),
+ before: getAddonById(p.addonBeforeId),
+ hidden: p.hidden,
+ help: i18n_.str`${p.help}`,
+ label: i18n_.str`${p.label}`,
+ tooltip: i18n_.str`${p.tooltip}`,
+ };
+}
+
+function getValueDeeper2(
+ object: Record<string, any>,
+ names: string[],
+): UIFieldHandler {
+ if (names.length === 0) return object as UIFieldHandler;
+ const [head, ...rest] = names;
+ if (!head) {
+ return getValueDeeper2(object, rest);
+ }
+ if (object === undefined) {
+ throw Error("handler not found");
+ }
+ return getValueDeeper2(object[head], rest);
+}
+
+const nullConverter: StringConverter<string> = {
+ fromStringUI(v: string | undefined): string {
+ return v ?? "";
+ },
+ toStringUI(v: unknown): string {
+ return v as string;
+ },
+};
+
+function amountConverter(config: any): StringConverter<AmountJson> {
+ const currency = config["currency"];
+ if (!currency || typeof currency !== "string") {
+ throw Error(`amount converter needs a currency`);
+ }
+ return {
+ fromStringUI(v: string | undefined): AmountJson {
+ return (
+ Amounts.parse(`${currency}:${v}`) ?? Amounts.zeroOfCurrency(currency)
+ );
+ },
+ toStringUI(v: unknown): string {
+ return v === undefined ? "" : Amounts.stringifyValue(v as AmountJson);
+ },
+ };
+}
+
+function absTimeConverter(config: any): StringConverter<AbsoluteTime> {
+ const pattern = config["pattern"];
+ if (!pattern || typeof pattern !== "string") {
+ throw Error(`absTime converter needs a pattern`);
+ }
+ return {
+ fromStringUI(v: string | undefined): AbsoluteTime {
+ if (v === undefined) {
+ return AbsoluteTime.never();
+ }
+ try {
+ const time = parse(v, pattern, new Date());
+ return AbsoluteTime.fromMilliseconds(time.getTime());
+ } catch (e) {
+ return AbsoluteTime.never();
+ }
+ },
+ toStringUI(v: unknown): string {
+ if (v === undefined) return "";
+ const d = v as AbsoluteTime;
+ if (d.t_ms === "never") return "never";
+ try {
+ return format(d.t_ms, pattern);
+ } catch (e) {
+ return "";
+ }
+ },
+ };
+}
diff --git a/packages/web-util/src/forms/forms.ts
b/packages/web-util/src/forms/forms.ts
deleted file mode 100644
index 178a3b626..000000000
--- a/packages/web-util/src/forms/forms.ts
+++ /dev/null
@@ -1,411 +0,0 @@
-import { h as create, Fragment, VNode } from "preact";
-import { Caption } from "./Caption.js";
-import { Group } from "./Group.js";
-import { InputAbsoluteTime } from "./InputAbsoluteTime.js";
-import { InputAmount } from "./InputAmount.js";
-import { InputArray } from "./InputArray.js";
-import { InputChoiceHorizontal } from "./InputChoiceHorizontal.js";
-import { InputChoiceStacked } from "./InputChoiceStacked.js";
-import { InputFile } from "./InputFile.js";
-import { InputInteger } from "./InputInteger.js";
-import { InputSelectMultiple } from "./InputSelectMultiple.js";
-import { InputSelectOne } from "./InputSelectOne.js";
-import { InputText } from "./InputText.js";
-import { InputTextArea } from "./InputTextArea.js";
-import { InputToggle } from "./InputToggle.js";
-import { Addon, StringConverter, UIFieldHandler } from "./FormProvider.js";
-import {
- InternationalizationAPI,
- UIFieldElementDescription,
-} from "../index.browser.js";
-import { assertUnreachable, TranslatedString } from "@gnu-taler/taler-util";
-import { UIFormFieldBaseConfig, UIFormElementConfig } from "./ui-form.js";
-import { HtmlIframe } from "./HtmlIframe.js";
-import { DownloadLink } from "./DownloadLink.js";
-/**
- * Constrain the type with the ui props
- */
-type FieldType<T extends object = any, K extends keyof T = any> = {
- group: Parameters<typeof Group>[0];
- caption: Parameters<typeof Caption>[0];
- "download-link": Parameters<typeof DownloadLink>[0];
- htmlIframe: Parameters<typeof HtmlIframe>[0];
- array: Parameters<typeof InputArray<T, K>>[0];
- file: Parameters<typeof InputFile<T, K>>[0];
- selectOne: Parameters<typeof InputSelectOne<T, K>>[0];
- selectMultiple: Parameters<typeof InputSelectMultiple<T, K>>[0];
- text: Parameters<typeof InputText<T, K>>[0];
- textArea: Parameters<typeof InputTextArea<T, K>>[0];
- choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0];
- choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0];
- absoluteTimeText: Parameters<typeof InputAbsoluteTime<T, K>>[0];
- integer: Parameters<typeof InputInteger<T, K>>[0];
- toggle: Parameters<typeof InputToggle<T, K>>[0];
- amount: Parameters<typeof InputAmount<T, K>>[0];
-};
-
-/**
- * List all the form fields so typescript can type-check the form instance
- */
-export type UIFormField =
- | { type: "group"; properties: FieldType["group"] }
- | { type: "caption"; properties: FieldType["caption"] }
- | { type: "download-link"; properties: FieldType["download-link"] }
- | { type: "htmlIframe"; properties: FieldType["htmlIframe"] }
- | { type: "array"; properties: FieldType["array"] }
- | { type: "file"; properties: FieldType["file"] }
- | { type: "amount"; properties: FieldType["amount"] }
- | { type: "selectOne"; properties: FieldType["selectOne"] }
- | {
- type: "selectMultiple";
- properties: FieldType["selectMultiple"];
- }
- | { type: "text"; properties: FieldType["text"] }
- | { type: "textArea"; properties: FieldType["textArea"] }
- | {
- type: "choiceStacked";
- properties: FieldType["choiceStacked"];
- }
- | {
- type: "choiceHorizontal";
- properties: FieldType["choiceHorizontal"];
- }
- | { type: "integer"; properties: FieldType["integer"] }
- | { type: "toggle"; properties: FieldType["toggle"] }
- | {
- type: "absoluteTimeText";
- properties: FieldType["absoluteTimeText"];
- };
-
-type FieldComponentFunction<key extends keyof FieldType> = (
- props: FieldType[key],
-) => VNode;
-
-type UIFormFieldMap = {
- [key in keyof FieldType]: FieldComponentFunction<key>;
-};
-
-/**
- * Maps input type with component implementation
- */
-const UIFormConfiguration: UIFormFieldMap = {
- group: Group,
- "download-link": DownloadLink,
- caption: Caption,
- htmlIframe: HtmlIframe,
- //@ts-ignore
- array: InputArray,
- text: InputText,
- //@ts-ignore
- file: InputFile,
- textArea: InputTextArea,
- //@ts-ignore
- absoluteTimeText: InputAbsoluteTime,
- //@ts-ignore
- choiceStacked: InputChoiceStacked,
- //@ts-ignore
- choiceHorizontal: InputChoiceHorizontal,
- integer: InputInteger,
- //@ts-ignore
- selectOne: InputSelectOne,
- //@ts-ignore
- selectMultiple: InputSelectMultiple,
- //@ts-ignore
- toggle: InputToggle,
- //@ts-ignore
- amount: InputAmount,
-};
-
-export function RenderAllFieldsByUiConfig({
- fields,
-}: {
- fields: UIFormField[];
-}): VNode {
- return create(
- Fragment,
- {},
- fields.map((field, i) => {
- const Component = UIFormConfiguration[
- field.type
- ] as FieldComponentFunction<any>;
- return Component(field.properties);
- }),
- );
-}
-
-// type FormSet<T extends object> = {
-// Provider: typeof FormProvider<T>;
-// InputLine: <K extends keyof T>() => typeof InputLine<T, K>;
-// InputChoiceHorizontal: <K extends keyof T>() => typeof
InputChoiceHorizontal<T, K>;
-// };
-
-/**
- * Helper function that created a typed object.
- *
- * @returns
- */
-// export function createNewForm<T extends object>() {
-// const res: FormSet<T> = {
-// Provider: FormProvider,
-// InputLine: () => InputLine,
-// InputChoiceHorizontal: () => InputChoiceHorizontal,
-// };
-// return {
-// Provider: res.Provider,
-// InputLine: res.InputLine(),
-// InputChoiceHorizontal: res.InputChoiceHorizontal(),
-// };
-// }
-
-/**
- * convert field configuration to render function
- *
- * @param i18n_
- * @param fieldConfig
- * @param form
- * @returns
- */
-export function convertUiField(
- i18n_: InternationalizationAPI,
- fieldConfig: UIFormElementConfig[],
- form: object,
- getConverterById: GetConverterById,
-): UIFormField[] {
- return fieldConfig.map((config) => {
- // NON input fields
- switch (config.type) {
- case "caption": {
- const resp: UIFormField = {
- type: config.type,
- properties: converBaseFieldsProps(i18n_, config),
- };
- return resp;
- }
- case "download-link": {
- const resp: UIFormField = {
- type: config.type,
- properties: {
- ...converBaseFieldsProps(i18n_, config),
- label: i18n_.str`${config.label}`,
- url: config.url,
- media: config.media,
- },
- };
- return resp;
- }
- case "htmlIframe": {
- const resp: UIFormField = {
- type: config.type,
- properties: {
- ...converBaseFieldsProps(i18n_, config),
- url: config.url,
- },
- };
- return resp;
- }
- case "group": {
- const resp: UIFormField = {
- type: config.type,
- properties: {
- ...converBaseFieldsProps(i18n_, config),
- fields: convertUiField(
- i18n_,
- config.fields,
- form,
- getConverterById,
- ),
- },
- };
- return resp;
- }
- }
- // Input Fields
- switch (config.type) {
- case "array": {
- return {
- type: "array",
- properties: {
- ...converBaseFieldsProps(i18n_, config),
- ...converInputFieldsProps(form, config, getConverterById),
- labelField: config.labelFieldId,
- fields: config.fields,
- // convertUiField(
- // i18n_,
- // config.fields,
- // (form as any)[config.id].value ?? {},
- // getConverterById,
- // ),
- },
- } as UIFormField;
- }
- case "absoluteTimeText": {
- return {
- type: "absoluteTimeText",
- properties: {
- ...converBaseFieldsProps(i18n_, config),
- ...converInputFieldsProps(form, config, getConverterById),
- },
- } as UIFormField;
- }
- case "amount": {
- return {
- type: "amount",
- properties: {
- ...converBaseFieldsProps(i18n_, config),
- ...converInputFieldsProps(form, config, getConverterById),
- currency: config.currency,
- },
- } as UIFormField;
- }
- case "choiceHorizontal": {
- return {
- type: "choiceHorizontal",
- properties: {
- ...converBaseFieldsProps(i18n_, config),
- ...converInputFieldsProps(form, config, getConverterById),
- choices: config.choices,
- },
- } as UIFormField;
- }
- case "choiceStacked": {
- return {
- type: "choiceStacked",
- properties: {
- ...converBaseFieldsProps(i18n_, config),
- ...converInputFieldsProps(form, config, getConverterById),
- choices: config.choices,
- },
- } as UIFormField;
- }
- case "file": {
- return {
- type: "file",
- properties: {
- ...converBaseFieldsProps(i18n_, config),
- ...converInputFieldsProps(form, config, getConverterById),
- accept: config.accept,
- maxBites: config.maxBytes,
- },
- } as UIFormField;
- }
- case "integer": {
- return {
- type: "integer",
- properties: {
- ...converBaseFieldsProps(i18n_, config),
- ...converInputFieldsProps(form, config, getConverterById),
- },
- } as UIFormField;
- }
- case "selectMultiple": {
- return {
- type: "selectMultiple",
- properties: {
- ...converBaseFieldsProps(i18n_, config),
- ...converInputFieldsProps(form, config, getConverterById),
- choices: config.choices,
- },
- } as UIFormField;
- }
- case "selectOne": {
- return {
- type: "selectOne",
- properties: {
- ...converBaseFieldsProps(i18n_, config),
- ...converInputFieldsProps(form, config, getConverterById),
- choices: config.choices,
- },
- } as UIFormField;
- }
- case "text": {
- return {
- type: "text",
- properties: {
- ...converBaseFieldsProps(i18n_, config),
- ...converInputFieldsProps(form, config, getConverterById),
- },
- } as UIFormField;
- }
- case "textArea": {
- return {
- type: "textArea",
- properties: {
- ...converBaseFieldsProps(i18n_, config),
- ...converInputFieldsProps(form, config, getConverterById),
- },
- } as UIFormField;
- }
- case "toggle": {
- return {
- type: "toggle",
- properties: {
- ...converBaseFieldsProps(i18n_, config),
- ...converInputFieldsProps(form, config, getConverterById),
- threeState: config.threeState,
- },
- } as UIFormField;
- }
- default: {
- assertUnreachable(config);
- }
- }
- });
-}
-
-function getAddonById(_id: string | undefined): Addon {
- return undefined!;
-}
-
-type GetConverterById = (
- id: string | undefined,
- config: unknown,
-) => StringConverter<unknown>;
-
-function converInputFieldsProps(
- form: object,
- p: UIFormFieldBaseConfig,
- getConverterById: GetConverterById,
-) {
- const names = p.id.split(".");
- // console.log("NAMES", names, getValueDeeper2(form, names), form)
- return {
- converter: getConverterById(p.converterId, p),
- handler: getValueDeeper2(form, names),
- required: p.required,
- disabled: p.disabled,
- name: names[names.length - 1],
- help: p.help,
- placeholder: p.placeholder,
- tooltip: p.tooltip,
- label: p.label as TranslatedString,
- };
-}
-
-function converBaseFieldsProps(
- i18n_: InternationalizationAPI,
- p: UIFieldElementDescription,
-) {
- return {
- after: getAddonById(p.addonAfterId),
- before: getAddonById(p.addonBeforeId),
- hidden: p.hidden,
- help: i18n_.str`${p.help}`,
- label: i18n_.str`${p.label}`,
- tooltip: i18n_.str`${p.tooltip}`,
- };
-}
-
-export function getValueDeeper2(
- object: Record<string, any>,
- names: string[],
-): UIFieldHandler {
- if (names.length === 0) return object as UIFieldHandler;
- const [head, ...rest] = names;
- if (!head) {
- return getValueDeeper2(object, rest);
- }
- if (object === undefined) {
- throw Error("handler not found");
- }
- return getValueDeeper2(object[head], rest);
-}
diff --git a/packages/web-util/src/forms/index.stories.ts
b/packages/web-util/src/forms/index.stories.ts
index 55878cb02..b08c727a4 100644
--- a/packages/web-util/src/forms/index.stories.ts
+++ b/packages/web-util/src/forms/index.stories.ts
@@ -5,7 +5,6 @@ export * as a4 from "./InputChoiceStacked.stories.js";
export * as a5 from "./InputAbsoluteTime.stories.js";
export * as a6 from "./InputFile.stories.js";
export * as a7 from "./InputInteger.stories.js";
-export * as a8 from "./InputLine.stories.js";
export * as a9 from "./InputSelectMultiple.stories.js";
export * as a10 from "./InputSelectOne.stories.js";
export * as a11 from "./InputText.stories.js";
diff --git a/packages/web-util/src/forms/index.ts
b/packages/web-util/src/forms/index.ts
index 5e3f4e1d3..187fffef3 100644
--- a/packages/web-util/src/forms/index.ts
+++ b/packages/web-util/src/forms/index.ts
@@ -1,26 +1,23 @@
-export * from "./Calendar.js"
-export * from "./Caption.js"
-export * from "./HtmlIframe.js"
-export * from "./DefaultForm.js"
-export * from "./Dialog.js"
-export * from "./FormProvider.js"
-export * from "./Group.js"
-export * from "./InputAbsoluteTime.js"
-export * from "./InputAmount.js"
-export * from "./InputArray.js"
-export * from "./InputChoiceHorizontal.js"
-export * from "./InputChoiceStacked.js"
-export * from "./InputFile.js"
-export * from "./InputInteger.js"
-export * from "./InputLine.js"
-export * from "./InputSelectMultiple.js"
-export * from "./InputSelectOne.js"
-export * from "./InputText.js"
-export * from "./InputTextArea.js"
-export * from "./InputToggle.js"
-export * from "./TimePicker.js"
-export * from "./forms.js"
-export * from "./ui-form.js"
-export * from "./converter.js"
-export * from "./useField.js"
-
+export * from "./Calendar.js";
+export * from "./Caption.js";
+export * from "./HtmlIframe.js";
+export * from "./Dialog.js";
+export * from "./FormProvider.js";
+export * from "./Group.js";
+export * from "./InputAbsoluteTime.js";
+export * from "./InputAmount.js";
+export * from "./InputArray.js";
+export * from "./InputChoiceHorizontal.js";
+export * from "./InputChoiceStacked.js";
+export * from "./InputFile.js";
+export * from "./InputInteger.js";
+export * from "./InputLine.js";
+export * from "./InputSelectMultiple.js";
+export * from "./InputSelectOne.js";
+export * from "./InputText.js";
+export * from "./InputTextArea.js";
+export * from "./InputToggle.js";
+export * from "./TimePicker.js";
+export * from "./field-types.js";
+export * from "./forms-types.js";
+export * from "./forms-ui.js";
diff --git a/packages/web-util/src/forms/useField.ts
b/packages/web-util/src/forms/useField.ts
index a250d3100..1b4bce02a 100644
--- a/packages/web-util/src/forms/useField.ts
+++ b/packages/web-util/src/forms/useField.ts
@@ -11,10 +11,10 @@ export interface InputFieldHandler<Type> {
/**
* @deprecated removing this so we don't depend on context to create a form
- * @param name
- * @returns
+ * @param name
+ * @returns
*/
-export function useField<T extends object, K extends keyof T>(
+export function useField_deprecated<T extends object, K extends keyof T>(
name: K,
): InputFieldHandler<T[K]> | undefined {
const ctx = useContext(FormContext);
@@ -27,7 +27,7 @@ export function useField<T extends object, K extends keyof T>(
computeFormState,
onUpdate: notifyUpdate,
readOnly: readOnlyForm,
- } = ctx
+ } = ctx;
type P = typeof name;
type V = T[P];
@@ -40,7 +40,7 @@ export function useField<T extends object, K extends keyof T>(
//compute default state
const state = {
- disabled: readOnlyForm ? true : (fieldState.disabled ?? false),
+ disabled: readOnlyForm ? true : fieldState.disabled ?? false,
hidden: fieldState.hidden ?? false,
help: fieldState.help,
elements: "elements" in fieldState ? fieldState.elements ?? [] : [],
@@ -48,7 +48,7 @@ export function useField<T extends object, K extends keyof T>(
function onChange(value: V): void {
// setCurrentValue(value);
- formValue.current = setValueDeeper(
+ formValue.current = setValueDeeper_toberemoved(
formValue.current,
String(name).split("."),
value,
@@ -72,20 +72,24 @@ export function useField<T extends object, K extends keyof
T>(
* @param name
* @returns
*/
-function readField<T>(
- object: any,
- name: string,
-): T | undefined {
+function readField<T>(object: any, name: string): T | undefined {
return name.split(".").reduce((prev, current) => {
return prev ? prev[current] : undefined;
}, object);
}
-function setValueDeeper(object: any, names: string[], value: any): any {
+function setValueDeeper_toberemoved(
+ object: any,
+ names: string[],
+ value: any,
+): any {
if (names.length === 0) return value;
const [head, ...rest] = names;
if (object === undefined) {
- return { [head]: setValueDeeper({}, rest, value) };
+ return { [head]: setValueDeeper_toberemoved({}, rest, value) };
}
- return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value)
};
+ return {
+ ...object,
+ [head]: setValueDeeper_toberemoved(object[head] ?? {}, rest, value),
+ };
}
diff --git a/packages/web-util/src/hooks/index.ts
b/packages/web-util/src/hooks/index.ts
index ba1b6e222..35c1e3186 100644
--- a/packages/web-util/src/hooks/index.ts
+++ b/packages/web-util/src/hooks/index.ts
@@ -1,7 +1,13 @@
export { useLang } from "./useLang.js";
-export { useLocalStorage, buildStorageKey, StorageKey, StorageState } from
"./useLocalStorage.js";
+export {
+ useLocalStorage,
+ buildStorageKey,
+ StorageKey,
+ StorageState,
+} from "./useLocalStorage.js";
export { useMemoryStorage } from "./useMemoryStorage.js";
export * from "./useNotifications.js";
+export { useForm } from "./useForm.js";
export {
useAsyncAsHook,
HookError,
diff --git a/packages/web-util/src/hooks/useForm.ts
b/packages/web-util/src/hooks/useForm.ts
index a8ce98d11..a669f3007 100644
--- a/packages/web-util/src/hooks/useForm.ts
+++ b/packages/web-util/src/hooks/useForm.ts
@@ -17,10 +17,12 @@
import {
AbsoluteTime,
AmountJson,
+ assertUnreachable,
TalerExchangeApi,
TranslatedString,
} from "@gnu-taler/taler-util";
import {
+ FormDesign,
UIFieldHandler,
UIFormElementConfig,
UIHandlerId,
@@ -75,139 +77,113 @@ 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(".");
+export type FormState<T> = {
+ handler: FormHandler<T>;
+ status: FormStatus<T>;
+};
- function updater(newValue: unknown) {
- updateForm(setValueDeeper(form, path, newValue));
- }
+function checkAllRequirements<T>(
+ st: RecursivePartial<FormValues<T>>,
+ check: (f: RecursivePartial<FormValues<T>>) => FormErrors<T> | undefined,
+): FormStatus<T> {
+ const errors = undefinedIfEmpty<FormErrors<T> | undefined>(
+ // validateRequiredFields(st, config),
+ check(st),
+ );
- 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, )
+ if (errors !== undefined) {
+ return {
+ status: "fail" as const,
+ result: st as any,
+ errors,
};
+ }
- return setValueDeeper(handleForm, path, field);
- }, {} as FormHandler<T>);
-
- return handler;
+ return { status: "ok" as const, result: st as any, errors: undefined };
}
-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),
- );
+/**
+ *
+ * @param fields form fields
+ * @param initialValue initial value
+ * @param check validation chain
+ * @returns
+ */
+export function useForm<T>(
+ design: FormDesign,
+ initialValue: RecursivePartial<FormValues<T>>,
+ check?: (f: RecursivePartial<FormValues<T>>) => FormErrors<T> | undefined,
+): FormState<T> {
+ const [formValue, formUpdateHandler] =
+ useState<RecursivePartial<FormValues<T>>>(initialValue);
- if (errors !== undefined) {
- return {
- status: "fail" as const,
- result: st as any,
- errors,
- };
+ const status = checkAllRequirements<T>(formValue, (v) => {
+ // FIXME: checks should be by fields
+ // FIXME: iterate only once and satify all checks, here we are potentially
iterating more than once
+ const required = validateRequiredFields(v, design);
+ if (!required && check) {
+ return check(v);
}
+ return required;
+ });
- 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,
+ design,
+ formValue,
+ formUpdateHandler,
+ status?.errors,
);
- return [handler, status];
+ return { handler, status };
}
+interface Tree<T> extends Record<string, Tree<T> | T> {}
+
/**
- * @deprecated use `useFormStateFromConfig`
+ * Use $path to get the value of $object
+ * return $noFoundValue if the target property is undefined
*
- * @param defaultValue
- * @param check
+ * @param object
+ * @param path
+ * @param notFoundValue
* @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>(
+export function getValueFromPath<T>(
object: Tree<T> | undefined,
- names: string[],
+ path: string[],
notFoundValue?: T,
): T | undefined {
- if (names.length === 0) return object as T;
- const [head, ...rest] = names;
+ if (path.length === 0) return object as T;
+ const [head, ...rest] = path;
if (!head) {
- return getValueDeeper(object, rest, notFoundValue);
+ return getValueFromPath(object, rest, notFoundValue);
}
if (object === undefined) {
return notFoundValue;
}
- return getValueDeeper(object[head] as Tree<T>, rest, notFoundValue);
+ return getValueFromPath(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;
+/**
+ * Use $path to set the value $value into $object
+ * Don't modify $object, returns a new value
+ * @param object
+ * @param path
+ * @param value
+ * @returns
+ */
+function setValueIntoPath(object: any, path: string[], value: any): any {
+ if (path.length === 0) return value;
+ const [head, ...rest] = path;
if (!head) {
- return setValueDeeper(object, rest, value);
+ return setValueIntoPath(object, rest, value);
}
if (object === undefined) {
- return undefinedIfEmpty({ [head]: setValueDeeper({}, rest, value) });
+ return undefinedIfEmpty({ [head]: setValueIntoPath({}, rest, value) });
}
return undefinedIfEmpty({
...object,
- [head]: setValueDeeper(object[head] ?? {}, rest, value),
+ [head]: setValueIntoPath(object[head] ?? {}, rest, value),
});
}
@@ -222,60 +198,105 @@ export function undefinedIfEmpty<T extends object |
undefined>(
: 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>,
+ config: FormDesign,
): 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(
+ let result: FormErrors<FormType> | undefined = undefined;
+
+ function checkIfRequiredFieldHasValue(formElement: UIFormElementConfig) {
+ if ("fields" in formElement) {
+ formElement.fields.forEach(checkIfRequiredFieldHasValue);
+ }
+ if (!("id" in formElement)) {
+ return;
+ }
+ if (!formElement.required) return;
+ const path = formElement.id.split(".");
+ const v = getValueFromPath(form as any, path);
+ result = setValueIntoPath(
result,
path,
v === undefined ? "required" : undefined,
);
- });
+ }
+
+ switch (config.type) {
+ case "double-column": {
+ config.sections.forEach((sec) => {
+ sec.fields.forEach(checkIfRequiredFieldHasValue);
+ });
+ break;
+ }
+ case "single-column": {
+ config.fields.forEach(checkIfRequiredFieldHasValue);
+ break;
+ }
+ default: {
+ assertUnreachable(config);
+ }
+ }
+
return result;
}
+
+function constructFormHandler<T>(
+ design: FormDesign,
+ value: RecursivePartial<FormValues<T>>,
+ onValueChange: (d: RecursivePartial<FormValues<T>>) => void,
+ errors: FormErrors<T> | undefined,
+): FormHandler<T> {
+ let formHandler: FormHandler<T> = {};
+
+ function notifyUpdateOnFieldChange(formElement: UIFormElementConfig): void {
+ if ("fields" in formElement) {
+ formElement.fields.forEach(notifyUpdateOnFieldChange);
+ }
+ if (!("id" in formElement)) {
+ return;
+ }
+ const path = formElement.id.split(".");
+
+ function updater(newValue: unknown) {
+ const updated = setValueIntoPath(value, path, newValue);
+ onValueChange(updated);
+ }
+
+ const currentValue = getValueFromPath<string>(
+ value as any,
+ path,
+ undefined,
+ );
+ const currentError = getValueFromPath<TranslatedString>(
+ errors as any,
+ path,
+ undefined,
+ );
+ const field: UIFieldHandler = {
+ error: currentError,
+ value: currentValue,
+ onChange: updater,
+ state: {}, //FIXME: add the state of the field (hidden, )
+ };
+
+ formHandler = setValueIntoPath(formHandler, path, field);
+ }
+
+ switch (design.type) {
+ case "double-column": {
+ design.sections.forEach((sec) => {
+ sec.fields.forEach(notifyUpdateOnFieldChange);
+ });
+ break;
+ }
+ case "single-column": {
+ design.fields.forEach(notifyUpdateOnFieldChange);
+ break;
+ }
+ default: {
+ assertUnreachable(design);
+ }
+ }
+
+ return formHandler;
+}
diff --git a/packages/web-util/src/stories.html
b/packages/web-util/src/stories.html
index b4a36fc19..1ce5e5c1e 100644
--- a/packages/web-util/src/stories.html
+++ b/packages/web-util/src/stories.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html>
+<!doctype html>
<html>
<head>
<title>WebUtils: Stories</title>
@@ -13,6 +13,10 @@
href="__EXAMPLES_CSS_FILE_LOCATION__"
/>
<script type="module" src="__EXAMPLES_JS_FILE_LOCATION__"></script>
+ <!-- FIXME: remove this -->
+ <!-- this is an easy setup of tailwind to test out the form fields -->
+ <!-- the css must be build locally and prevent the requirement of internet
access for development -->
+ <script
src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp,container-queries"></script>
</head>
<body>
<taler-stories id="container"></taler-stories>
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [taler-wallet-core] branch master updated (e78d0f8e4 -> 9c6146309),
gnunet <=