summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-05-03 08:43:53 -0300
committerSebastian <sebasjm@gmail.com>2024-05-03 08:44:07 -0300
commit20353eda268efa962959bead466b59823bfb9b29 (patch)
tree868d016693f09b40e2c55893d3aed72eca505ecb
parentfa4c7039f4ebeb6ad3cf19237ad7b138519ac142 (diff)
downloadwallet-core-20353eda268efa962959bead466b59823bfb9b29.tar.gz
wallet-core-20353eda268efa962959bead466b59823bfb9b29.tar.bz2
wallet-core-20353eda268efa962959bead466b59823bfb9b29.zip
form hook now takes the shape of the form (do not rely on initial value)
-rw-r--r--packages/aml-backoffice-ui/src/context/ui-forms.ts36
-rw-r--r--packages/aml-backoffice-ui/src/forms/simplest.ts1
-rw-r--r--packages/aml-backoffice-ui/src/hooks/form.ts89
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseDetails.tsx4
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx239
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx14
-rw-r--r--packages/aml-backoffice-ui/src/pages/CreateAccount.tsx2
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx7
-rw-r--r--packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx2
-rw-r--r--packages/aml-backoffice-ui/src/utils/converter.ts25
-rw-r--r--packages/web-util/src/forms/Caption.tsx17
-rw-r--r--packages/web-util/src/forms/Group.tsx38
-rw-r--r--packages/web-util/src/forms/InputAmount.tsx6
-rw-r--r--packages/web-util/src/forms/InputChoiceHorizontal.tsx8
-rw-r--r--packages/web-util/src/forms/InputLine.tsx83
15 files changed, 360 insertions, 211 deletions
diff --git a/packages/aml-backoffice-ui/src/context/ui-forms.ts b/packages/aml-backoffice-ui/src/context/ui-forms.ts
index 2e0b8a76d..9cf6125c9 100644
--- a/packages/aml-backoffice-ui/src/context/ui-forms.ts
+++ b/packages/aml-backoffice-ui/src/context/ui-forms.ts
@@ -39,7 +39,7 @@ import { useContext } from "preact/hooks";
export type Type = UiForms;
const defaultForms: UiForms = {
- forms: []
+ forms: [],
};
const Context = createContext<Type>(defaultForms);
@@ -142,7 +142,7 @@ type UIFormFieldConfigCaption = {
type UIFormFieldConfigGroup = {
type: "group";
- properties: UIFormFieldBaseConfig & {
+ properties: UIFieldBaseDescription & {
fields: UIFormFieldConfig[];
};
};
@@ -213,7 +213,7 @@ type UIFormFieldConfigToggle = {
properties: UIFormFieldBaseConfig;
};
-type UIFieldBaseDescription = {
+export type UIFieldBaseDescription = {
/* label if the field, visible for the user */
label: string;
/* long text to be shown on user demand */
@@ -222,6 +222,9 @@ type UIFieldBaseDescription = {
/* short text to be shown close to the field */
help?: string;
+ /* name of the field, useful for a11y */
+ name: string;
+
/* if the field should be initialy hidden */
hidden?: boolean;
/* ui element to show before */
@@ -230,7 +233,7 @@ type UIFieldBaseDescription = {
addonAfterId?: string;
};
-type UIFormFieldBaseConfig = UIFieldBaseDescription & {
+export type UIFormFieldBaseConfig = UIFieldBaseDescription & {
/* example to be shown inside the field */
placeholder?: string;
@@ -240,9 +243,6 @@ type UIFormFieldBaseConfig = UIFieldBaseDescription & {
/* readonly and dim */
disabled?: boolean;
- /* name of the field, useful for a11y */
- name: string;
-
/* conversion id to conver the string into the value type
the id should be known to the ui impl
*/
@@ -258,23 +258,27 @@ export type UIHandlerId = string & { [__handlerId]: true };
// FIXME: validate well formed ui field id
const codecForUiFieldId = codecForString as () => Codec<UIHandlerId>;
-const codecForUIFormFieldBaseConfigTemplate = <
- T extends UIFormFieldBaseConfig,
+const codecForUIFormFieldBaseDescriptionTemplate = <
+ T extends UIFieldBaseDescription,
>() =>
buildCodecForObject<T>()
- .property("id", codecForUiFieldId())
.property("addonAfterId", codecOptional(codecForString()))
.property("addonBeforeId", codecOptional(codecForString()))
- .property("converterId", codecOptional(codecForString()))
- .property("disabled", codecOptional(codecForBoolean()))
.property("hidden", codecOptional(codecForBoolean()))
- .property("required", codecOptional(codecForBoolean()))
.property("help", codecOptional(codecForString()))
.property("label", codecForString())
.property("name", codecForString())
- .property("placeholder", codecOptional(codecForString()))
.property("tooltip", codecOptional(codecForString()));
+const codecForUIFormFieldBaseConfigTemplate = <
+ T extends UIFormFieldBaseConfig,
+>() =>
+ codecForUIFormFieldBaseDescriptionTemplate<T>()
+ .property("id", codecForUiFieldId())
+ .property("converterId", codecOptional(codecForString()))
+ .property("disabled", codecOptional(codecForBoolean()))
+ .property("required", codecOptional(codecForBoolean()))
+ .property("placeholder", codecOptional(codecForString()));
const codecForUIFormFieldBaseConfig = (): Codec<UIFormFieldBaseConfig> =>
codecForUIFormFieldBaseConfigTemplate().build("UIFieldToggleProperties");
@@ -370,7 +374,9 @@ const codecForUiFormFieldFile = (): Codec<UIFormFieldConfigFile> =>
const codecForUIFormFieldWithFieldsConfig = (): Codec<
UIFormFieldConfigGroup["properties"]
> =>
- codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigGroup["properties"]>()
+ codecForUIFormFieldBaseDescriptionTemplate<
+ UIFormFieldConfigGroup["properties"]
+ >()
.property("fields", codecForList(codecForUiFormField()))
.build("UIFormFieldConfigGroup.properties");
diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts
index c7ba95462..b52f2bf74 100644
--- a/packages/aml-backoffice-ui/src/forms/simplest.ts
+++ b/packages/aml-backoffice-ui/src/forms/simplest.ts
@@ -86,6 +86,7 @@ export function resolutionSection(
id: ".threshold" as UIHandlerId,
currency: "NETZBON",
name: "threshold",
+ converterId: "Taler.Amount",
label: i18n.str`New threshold`,
},
},
diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts
index edeae6085..752444bd2 100644
--- a/packages/aml-backoffice-ui/src/hooks/form.ts
+++ b/packages/aml-backoffice-ui/src/hooks/form.ts
@@ -15,12 +15,14 @@
*/
import {
+ AbsoluteTime,
AmountJson,
TalerExchangeApi,
TranslatedString,
} from "@gnu-taler/taler-util";
import { UIFieldHandler } from "@gnu-taler/web-util/browser";
import { useState } from "preact/hooks";
+import { UIFormFieldConfig, UIHandlerId } from "../context/ui-forms.js";
// export type UIField = {
// value: string | undefined;
@@ -57,6 +59,8 @@ export type FormErrors<T> = {
? TranslatedString
: T[k] extends AmountJson
? TranslatedString
+ : T[k] extends AbsoluteTime
+ ? TranslatedString
: T[k] extends TalerExchangeApi.AmlState
? TranslatedString
: FormErrors<T[k]>;
@@ -75,31 +79,22 @@ export type FormStatus<T> =
};
function constructFormHandler<T>(
+ shape: Array<UIHandlerId>,
form: RecursivePartial<FormValues<T>>,
updateForm: (d: RecursivePartial<FormValues<T>>) => void,
errors: FormErrors<T> | undefined,
): FormHandler<T> {
- const keys = Object.keys(form) as Array<keyof T>;
- const handler = keys.reduce((prev, fieldName) => {
- const currentValue: unknown = form[fieldName];
- const currentError: unknown =
- errors !== undefined ? errors[fieldName] : undefined;
+ const handler = shape.reduce((handleForm, fieldId) => {
+
+ const path = fieldId.split(".")
+
function updater(newValue: unknown) {
- updateForm({ ...form, [fieldName]: newValue });
- }
- /**
- * There is no clear way to know if this object is a custom field
- * or a group of fields
- */
- if (typeof currentValue === "object") {
- // @ts-expect-error FIXME better typing
- const group = constructFormHandler(currentValue, updater, currentError);
- // @ts-expect-error FIXME better typing
- prev[fieldName] = group;
- return prev;
+ updateForm(setValueDeeper(form, path, newValue));
}
+ const currentValue: unknown = getValueDeeper(form, path)
+ const currentError: unknown = errors === undefined ? undefined : getValueDeeper(errors, path)
const field: UIFieldHandler = {
// @ts-expect-error FIXME better typing
error: currentError,
@@ -108,14 +103,37 @@ function constructFormHandler<T>(
onChange: updater,
state: {},
};
- // @ts-expect-error FIXME better typing
- prev[fieldName] = field;
- return prev;
+
+ return setValueDeeper(handleForm, path, field)
+
+ /**
+ * There is no clear way to know if this object is a custom field
+ * or a group of fields
+ */
+ // if (typeof currentValue === "object") {
+ // // @ts-expect-error FIXME better typing
+ // const group = constructFormHandler(currentValue, updater, currentError);
+ // // @ts-expect-error FIXME better typing
+ // prev[fieldName] = group;
+ // return prev;
+ // }
+
+ // const field: UIFieldHandler = {
+ // // @ts-expect-error FIXME better typing
+ // error: currentError,
+ // // @ts-expect-error FIXME better typing
+ // value: currentValue,
+ // onChange: updater,
+ // state: {},
+ // };
+ // handleForm[fieldName] = field;
+ // return handleForm;
}, {} as FormHandler<T>);
return handler;
}
+
/**
* FIXME: Consider sending this to web-utils
*
@@ -125,6 +143,7 @@ function constructFormHandler<T>(
* @returns
*/
export function useFormState<T>(
+ shape: Array<UIHandlerId>,
defaultValue: RecursivePartial<FormValues<T>>,
check: (f: RecursivePartial<FormValues<T>>) => FormStatus<T>,
): [FormHandler<T>, FormStatus<T>] {
@@ -132,7 +151,35 @@ export function useFormState<T>(
useState<RecursivePartial<FormValues<T>>>(defaultValue);
const status = check(form);
- const handler = constructFormHandler(form, updateForm, status.errors);
+ const handler = constructFormHandler(shape, form, updateForm, status.errors);
return [handler, status];
}
+
+
+function getValueDeeper(
+ object: Record<string, any>,
+ names: string[],
+): UIFieldHandler {
+ if (names.length === 0) return object as UIFieldHandler;
+ const [head, ...rest] = names;
+ if (!head) {
+ return getValueDeeper(object, rest);
+ }
+ if (object === undefined) {
+ throw Error("handler not found");
+ }
+ return getValueDeeper(object[head], rest);
+}
+
+function setValueDeeper(object: any, names: string[], value: any): any {
+ if (names.length === 0) return value;
+ const [head, ...rest] = names;
+ if (!head) {
+ return setValueDeeper(object, rest, value);
+ }
+ if (object === undefined) {
+ return { [head]: setValueDeeper({}, rest, value) };
+ }
+ return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) };
+}
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index 62c54d222..11b6d053e 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -45,6 +45,7 @@ import { privatePages } from "../Routing.js";
import { FormMetadata, useUiFormsContext } from "../context/ui-forms.js";
import { useCaseDetails } from "../hooks/useCaseDetails.js";
import { ShowConsolidated } from "./ShowConsolidated.js";
+import { preloadedForms } from "../forms/index.js";
export type AmlEvent =
| AmlFormEvent
@@ -164,6 +165,7 @@ export function CaseDetails({ account }: { account: string }) {
const details = useCaseDetails(account);
const {forms} = useUiFormsContext()
+ const allForms = [...forms, ...preloadedForms(i18n)]
if (!details) {
return <Loading />;
}
@@ -183,7 +185,7 @@ export function CaseDetails({ account }: { account: string }) {
}
const { aml_history, kyc_attributes } = details.body;
- const events = getEventsFromAmlHistory(aml_history, kyc_attributes, i18n, forms);
+ const events = getEventsFromAmlHistory(aml_history, kyc_attributes, i18n, allForms);
if (showForm !== undefined) {
return (
diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
index f04d404d0..bbfa65ca7 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
@@ -20,13 +20,15 @@ import {
HttpStatusCode,
TalerExchangeApi,
TalerProtocolTimestamp,
- assertUnreachable
+ assertUnreachable,
} from "@gnu-taler/taler-util";
import {
+ Addon,
Button,
InternationalizationAPI,
LocalNotificationBanner,
RenderAllFieldsByUiConfig,
+ StringConverter,
UIFieldHandler,
UIFormField,
useExchangeApiContext,
@@ -37,14 +39,19 @@ import { Fragment, VNode, h } from "preact";
import { privatePages } from "../Routing.js";
import {
FormMetadata,
+ UIFieldBaseDescription,
+ UIFormFieldBaseConfig,
UIFormFieldConfig,
+ UIHandlerId,
useUiFormsContext,
} from "../context/ui-forms.js";
import { preloadedForms } from "../forms/index.js";
-import { FormHandler, useFormState } from "../hooks/form.js";
+import { FormErrors, FormHandler, useFormState } from "../hooks/form.js";
import { useOfficer } from "../hooks/officer.js";
import { Justification } from "./CaseDetails.js";
import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
+import { undefinedIfEmpty } from "./CreateAccount.js";
+import { getConverterById } from "../utils/converter.js";
function searchForm(
i18n: InternationalizationAPI,
@@ -67,6 +74,7 @@ type FormType = {
when: AbsoluteTime;
state: TalerExchangeApi.AmlState;
threshold: AmountJson;
+ comment: string;
};
export function CaseUpdate({
@@ -89,6 +97,7 @@ export function CaseUpdate({
when: AbsoluteTime.now(),
state: TalerExchangeApi.AmlState.pending,
threshold: Amounts.zeroOfCurrency(config.currency),
+ comment: "",
};
if (officer.state !== "ready") {
@@ -99,27 +108,41 @@ export function CaseUpdate({
return <div>form with id {formId} not found</div>;
}
- let defaultValue = initial
+ const shape: Array<UIHandlerId> = [];
theForm.config.design.forEach((section) => {
section.fields.forEach((field) => {
if ("id" in field.properties) {
- const path = field.properties.id.split(".");
- defaultValue = setValueDeeper(defaultValue, path, undefined);
+ shape.push(field.properties.id);
+ // const path = field.properties.id.split(".");
+ // defaultValue = setValueDeeper(defaultValue, path, undefined);
}
});
});
-
- const [form, state] = useFormState<FormType>(defaultValue, (st) => {
+ const [form, state] = useFormState<FormType>(shape, initial, (st) => {
+ const errors = undefinedIfEmpty<FormErrors<FormType>>({
+ state: !st.state ? i18n.str`required` : undefined,
+ threshold: !st.threshold ? i18n.str`required` : undefined,
+ when: !st.when ? i18n.str`required` : undefined,
+ comment: !st.comment ? i18n.str`required` : undefined,
+ });
+ if (errors === undefined) {
+ return {
+ status: "ok",
+ result: st as any,
+ errors: undefined,
+ };
+ }
return {
- status: "ok",
+ status: "fail",
result: st as any,
- errors: undefined,
+ errors,
};
});
-
- console.log("FORM", form)
- const validatedForm = state.status === "fail" ? undefined : state.result;
+
+ console.log("NOW FORM", form);
+
+ const validatedForm = state.status !== "ok" ? undefined : state.result;
const submitHandler =
validatedForm === undefined
@@ -166,75 +189,6 @@ export function CaseUpdate({
},
);
- function convertUiField(
- fieldConfig: UIFormFieldConfig[],
- form: FormHandler<unknown>,
- ): UIFormField[] {
- return fieldConfig.map((config) => {
- switch (config.type) {
- case "absoluteTime": {
- return undefined!;
- }
- case "amount": {
- return {
- type: "amount",
- properties: {
- ...(config.properties as any),
- handler: getValueDeeper(form, config.properties.id.split(".")),
- },
- } as UIFormField;
- }
- case "array": {
- return undefined!;
- }
- case "caption": {
- return undefined!;
- }
- case "choiceHorizontal": {
- return {
- type: "choiceHorizontal",
- properties: {
- handler: getValueDeeper(form, config.properties.id.split(".")),
- choices: config.properties.choices,
- },
- } as UIFormField;
- }
- case "choiceStacked":
- case "file":
- case "group":
- case "integer":
- case "selectMultiple":
- case "selectOne": {
- return undefined!;
- }
- case "text": {
- return {
- type: "text",
- properties: {
- ...(config.properties as any),
- handler: getValueDeeper(form, config.properties.id.split(".")),
- },
- } as UIFormField;
- }
- case "textArea": {
- return {
- type: "text",
- properties: {
- ...(config.properties as any),
- handler: getValueDeeper(form, config.properties.id.split(".")),
- },
- } as UIFormField;
- }
- case "toggle": {
- return undefined!;
- }
- default: {
- assertUnreachable(config);
- }
- }
- });
- }
-
return (
<Fragment>
<LocalNotificationBanner notification={notification} />
@@ -261,7 +215,7 @@ export function CaseUpdate({
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<RenderAllFieldsByUiConfig
key={i}
- fields={convertUiField(section.fields, form)}
+ fields={convertUiField(i18n, section.fields, form)}
/>
</div>
</div>
@@ -281,7 +235,8 @@ export function CaseUpdate({
<Button
type="submit"
handler={submitHandler}
- class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 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"
+ disabled={!submitHandler}
+ class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 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"
>
<i18n.Translate>Confirm</i18n.Translate>
</Button>
@@ -350,3 +305,121 @@ function setValueDeeper(object: any, names: string[], value: any): any {
}
return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) };
}
+
+function getAddonById(_id: string | undefined): Addon {
+ return undefined!;
+}
+
+
+function converInputFieldsProps(
+ form: FormHandler<unknown>,
+ p: UIFormFieldBaseConfig,
+) {
+ return {
+ converter: getConverterById(p.converterId),
+ handler: getValueDeeper(form, p.id.split(".")),
+ };
+}
+
+function converBaseFieldsProps(
+ i18n_: InternationalizationAPI,
+ p: UIFieldBaseDescription,
+) {
+ return {
+ after: getAddonById(p.addonAfterId),
+ before: getAddonById(p.addonBeforeId),
+ hidden: p.hidden,
+ name: p.name,
+ help: i18n_.str`${p.help}`,
+ label: i18n_.str`${p.label}`,
+ tooltip: i18n_.str`${p.tooltip}`,
+ };
+}
+
+function convertUiField(
+ i18n_: InternationalizationAPI,
+ fieldConfig: UIFormFieldConfig[],
+ form: FormHandler<unknown>,
+): UIFormField[] {
+ return fieldConfig.map((config) => {
+ // NON input fields
+ switch (config.type) {
+ case "caption": {
+ const resp: UIFormField = {
+ type: config.type,
+ properties: converBaseFieldsProps(i18n_, config.properties),
+ };
+ return resp;
+ }
+ case "group": {
+ const resp: UIFormField = {
+ type: config.type,
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ fields: convertUiField(i18n_, config.properties.fields, form),
+ },
+ };
+ return resp;
+ }
+ }
+ // Input Fields
+ switch (config.type) {
+ case "absoluteTime": {
+ return undefined!;
+ }
+ case "amount": {
+ return {
+ type: "amount",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ },
+ } as UIFormField;
+ }
+ case "array": {
+ return undefined!;
+ }
+ case "choiceHorizontal": {
+ return {
+ type: "choiceHorizontal",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ choices: config.properties.choices,
+ },
+ } as UIFormField;
+ }
+ case "choiceStacked":
+ case "file":
+ case "integer":
+ case "selectMultiple":
+ case "selectOne": {
+ return undefined!;
+ }
+ case "text": {
+ return {
+ type: "text",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ },
+ } as UIFormField;
+ }
+ case "textArea": {
+ return {
+ type: "text",
+ properties: {
+ ...converBaseFieldsProps(i18n_, config.properties),
+ ...converInputFieldsProps(form, config.properties),
+ },
+ } as UIFormField;
+ }
+ case "toggle": {
+ return undefined!;
+ }
+ default: {
+ assertUnreachable(config);
+ }
+ }
+ });
+}
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx
index 2e92c111e..3860bcd98 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx
@@ -24,7 +24,7 @@ import {
ErrorLoading,
InputChoiceHorizontal,
Loading,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
@@ -34,6 +34,8 @@ import { privatePages } from "../Routing.js";
import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js";
import { undefinedIfEmpty } from "./CreateAccount.js";
import { Officer } from "./Officer.js";
+import { UIHandlerId } from "../context/ui-forms.js";
+import { amlStateConverter } from "../utils/converter.js";
type FormType = {
state: TalerExchangeApi.AmlState;
@@ -55,6 +57,7 @@ export function CasesUI({
const { i18n } = useTranslationContext();
const [form, status] = useFormState<FormType>(
+ [".state"] as Array<UIHandlerId>,
{
state: filter,
},
@@ -106,18 +109,19 @@ export function CasesUI({
name="state"
label={i18n.str`Filter`}
handler={form.state}
+ converter={amlStateConverter}
choices={[
{
label: i18n.str`Pending`,
- value: TalerExchangeApi.AmlState.pending,
+ value: "pending",
},
{
label: i18n.str`Frozen`,
- value: TalerExchangeApi.AmlState.frozen,
+ value: "frozen",
},
{
label: i18n.str`Normal`,
- value: TalerExchangeApi.AmlState.normal,
+ value: "normal",
},
]}
/>
@@ -269,7 +273,7 @@ export function Cases() {
onNext={list.isLastPage ? undefined : list.loadNext}
filter={stateFilter}
onChangeFilter={(d) => {
- setStateFilter(d)
+ setStateFilter(d);
}}
/>
);
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
index abcaaa2a6..98160fb3a 100644
--- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -31,6 +31,7 @@ import {
} from "../hooks/form.js";
import { useOfficer } from "../hooks/officer.js";
import { usePreferences } from "../hooks/preferences.js";
+import { UIHandlerId } from "../context/ui-forms.js";
type FormType = {
password: string;
@@ -104,6 +105,7 @@ export function CreateAccount(): VNode {
const [notification, withErrorHandler] = useLocalNotificationHandler();
const [form, status] = useFormState<FormType>(
+ [".password", ".repeat"] as Array<UIHandlerId>,
{
password: undefined,
repeat: undefined,
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
index 0169572bf..0978d8bcc 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
@@ -72,18 +72,19 @@ export function ShowConsolidated({
properties: {
label: i18n.str`State`,
name: "aml.state",
+
choices: [
{
label: i18n.str`Frozen`,
- value: TalerExchangeApi.AmlState.frozen,
+ value: "frozen",
},
{
label: i18n.str`Pending`,
- value: TalerExchangeApi.AmlState.pending,
+ value: "pending",
},
{
label: i18n.str`Normal`,
- value: TalerExchangeApi.AmlState.normal,
+ value: "normal",
},
],
},
diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
index 9552f2b0c..b81e66468 100644
--- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -24,6 +24,7 @@ import { VNode, h } from "preact";
import { FormErrors, useFormState } from "../hooks/form.js";
import { useOfficer } from "../hooks/officer.js";
import { undefinedIfEmpty } from "./CreateAccount.js";
+import { UIHandlerId } from "../context/ui-forms.js";
type FormType = {
password: string;
@@ -36,6 +37,7 @@ export function UnlockAccount(): VNode {
const [notification, withErrorHandler] = useLocalNotificationHandler();
const [form, status] = useFormState<FormType>(
+ [".password"] as Array<UIHandlerId>,
{
password: undefined,
},
diff --git a/packages/aml-backoffice-ui/src/utils/converter.ts b/packages/aml-backoffice-ui/src/utils/converter.ts
index cca764a81..25a824697 100644
--- a/packages/aml-backoffice-ui/src/utils/converter.ts
+++ b/packages/aml-backoffice-ui/src/utils/converter.ts
@@ -14,7 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { TalerExchangeApi } from "@gnu-taler/taler-util";
+import { AmountJson, Amounts, TalerExchangeApi } from "@gnu-taler/taler-util";
+import { StringConverter } from "@gnu-taler/web-util/browser";
export const amlStateConverter = {
toStringUI: stringifyAmlState,
@@ -45,3 +46,25 @@ function parseAmlState(s: string | undefined): TalerExchangeApi.AmlState {
throw Error(`unknown AML state: ${s}`);
}
}
+
+const amountConverter: StringConverter<AmountJson> = {
+ fromStringUI(v: string | undefined): AmountJson {
+ // FIXME: requires currency
+ return Amounts.parse(`NETZBON:${v}`) ?? Amounts.zeroOfCurrency("NETZBON");
+ },
+ toStringUI(v: unknown): string {
+ return v === undefined ? "" : Amounts.stringifyValue(v as AmountJson);
+ },
+};
+
+export function getConverterById(id: string | undefined): StringConverter<unknown> {
+ if (id === "Taler.Amount") {
+ // @ts-expect-error check this
+ return amountConverter;
+ }
+ if (id === "TalerExchangeApi.AmlState") {
+ // @ts-expect-error check this
+ return amlStateConverter;
+ }
+ return undefined!;
+}
diff --git a/packages/web-util/src/forms/Caption.tsx b/packages/web-util/src/forms/Caption.tsx
index 8facddec3..be4725ffa 100644
--- a/packages/web-util/src/forms/Caption.tsx
+++ b/packages/web-util/src/forms/Caption.tsx
@@ -1,27 +1,22 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import { VNode, h } from "preact";
-import {
- LabelWithTooltipMaybeRequired
-} from "./InputLine.js";
+import { LabelWithTooltipMaybeRequired, RenderAddon } from "./InputLine.js";
+import { Addon } from "./FormProvider.js";
interface Props {
label: TranslatedString;
tooltip?: TranslatedString;
help?: TranslatedString;
- before?: VNode;
- after?: VNode;
+ before?: Addon;
+ after?: Addon;
}
export function Caption({ before, after, label, tooltip, help }: Props): VNode {
return (
<div class="sm:col-span-6 flex">
- {before !== undefined && (
- <span class="pointer-events-none flex items-center pr-2">{before}</span>
- )}
+ {before !== undefined && <RenderAddon addon={before} />}
<LabelWithTooltipMaybeRequired label={label} tooltip={tooltip} />
- {after !== undefined && (
- <span class="pointer-events-none flex items-center pl-2">{after}</span>
- )}
+ {after !== undefined && <RenderAddon addon={after} />}
{help && (
<p class="mt-2 text-sm text-gray-500" id="email-description">
{help}
diff --git a/packages/web-util/src/forms/Group.tsx b/packages/web-util/src/forms/Group.tsx
index 0645f6d97..d5626be1d 100644
--- a/packages/web-util/src/forms/Group.tsx
+++ b/packages/web-util/src/forms/Group.tsx
@@ -1,41 +1,39 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import { VNode, h } from "preact";
-import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
+import { LabelWithTooltipMaybeRequired, RenderAddon } from "./InputLine.js";
import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
+import { Addon } from "./FormProvider.js";
interface Props {
- before?: TranslatedString;
- after?: TranslatedString;
- tooltipBefore?: TranslatedString;
- tooltipAfter?: TranslatedString;
+ label: TranslatedString;
+ tooltip?: TranslatedString;
+ help?: TranslatedString;
+ before?: Addon;
+ after?: Addon;
fields: UIFormField[];
}
export function Group({
before,
after,
- tooltipAfter,
- tooltipBefore,
+ label,
+ tooltip,
+ help,
fields,
}: Props): VNode {
return (
<div class="sm:col-span-6 p-4 rounded-lg border-r-2 border-2 bg-gray-50">
- <div class="pb-4">
- {before && (
- <LabelWithTooltipMaybeRequired
- label={before}
- tooltip={tooltipBefore}
- />
- )}
- </div>
+ {before !== undefined && <RenderAddon addon={before} />}
+ <LabelWithTooltipMaybeRequired label={label} tooltip={tooltip} />
+ {after !== undefined && <RenderAddon addon={after} />}
+ {help && (
+ <p class="mt-2 text-sm text-gray-500" id="email-description">
+ {help}
+ </p>
+ )}
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-2 sm:grid-cols-6">
<RenderAllFieldsByUiConfig fields={fields} />
</div>
- <div class="pt-4">
- {after && (
- <LabelWithTooltipMaybeRequired label={after} tooltip={tooltipAfter} />
- )}
- </div>
</div>
);
}
diff --git a/packages/web-util/src/forms/InputAmount.tsx b/packages/web-util/src/forms/InputAmount.tsx
index 31e83350e..e8683468e 100644
--- a/packages/web-util/src/forms/InputAmount.tsx
+++ b/packages/web-util/src/forms/InputAmount.tsx
@@ -23,15 +23,15 @@ export function InputAmount<T extends object, K extends keyof T>(
type: "text",
text: currency as TranslatedString,
}}
- converter={{
- //@ts-ignore
+ //@ts-ignore
+ converter={ props.converter ?? {
+
fromStringUI: (v): AmountJson => {
return (
Amounts.parse(`${currency}:${v}`) ??
Amounts.zeroOfCurrency(currency)
);
},
- //@ts-ignore
toStringUI: (v: AmountJson) => {
return v === undefined ? "" : Amounts.stringifyValue(v);
},
diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.tsx b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
index 82a7c3115..d8361718d 100644
--- a/packages/web-util/src/forms/InputChoiceHorizontal.tsx
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
@@ -12,10 +12,10 @@ export interface ChoiceH<V> {
export function InputChoiceHorizontal<T extends object, K extends keyof T>(
props: {
- choices: ChoiceH<T[K]>[];
+ choices: ChoiceH<string>[];
} & UIFormProps<T, K>,
): VNode {
- const { choices, label, tooltip, help, required } = props;
+ const { choices, label, tooltip, help, required, converter } = props;
//FIXME: remove deprecated
const fieldCtx = useField<T, K>(props.name);
const { value, onChange, state } =
@@ -38,7 +38,7 @@ export function InputChoiceHorizontal<T extends object, K extends keyof T>(
const isLast = idx === choices.length - 1;
let clazz =
"relative inline-flex items-center px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 focus:z-10";
- if (choice.value === value) {
+ if (converter?.fromStringUI(choice.value as any) === value) {
clazz +=
" text-white bg-indigo-600 hover:bg-indigo-500 ring-2 ring-indigo-600 hover:ring-indigo-500";
} else {
@@ -61,7 +61,7 @@ export function InputChoiceHorizontal<T extends object, K extends keyof T>(
class={clazz}
onClick={(e) => {
onChange(
- (value === choice.value ? undefined : choice.value) as any,
+ (value === choice.value ? undefined : converter?.fromStringUI(choice.value as any)) as any,
);
}}
>
diff --git a/packages/web-util/src/forms/InputLine.tsx b/packages/web-util/src/forms/InputLine.tsx
index ee9150492..eb3238ef9 100644
--- a/packages/web-util/src/forms/InputLine.tsx
+++ b/packages/web-util/src/forms/InputLine.tsx
@@ -1,6 +1,6 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import { ComponentChildren, Fragment, VNode, h } from "preact";
-import { UIFormProps } from "./FormProvider.js";
+import { Addon, UIFormProps } from "./FormProvider.js";
import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
import { useField } from "./useField.js";
@@ -68,6 +68,37 @@ export function LabelWithTooltipMaybeRequired({
return WithTooltip;
}
+export function RenderAddon({ disabled, addon }: { disabled?: boolean, addon: Addon }): VNode {
+ switch (addon.type) {
+ case "text": {
+ return (
+ <span class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
+ {addon.text}
+ </span>
+ );
+ }
+ case "icon": {
+ return (
+ <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
+ {addon.icon}
+ </div>
+ );
+ }
+ case "button": {
+ return (
+ <button
+ type="button"
+ disabled={disabled}
+ onClick={addon.onClick}
+ class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-l-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
+ >
+ {addon.children}
+ </button>
+ );
+ }
+ }
+}
+
function InputWrapper<T extends object, K extends keyof T>({
children,
label,
@@ -91,47 +122,11 @@ function InputWrapper<T extends object, K extends keyof T>({
tooltip={tooltip}
/>
<div class="relative mt-2 flex rounded-md shadow-sm">
- {before &&
- (before.type === "text" ? (
- <span class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
- {before.text}
- </span>
- ) : before.type === "icon" ? (
- <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
- {before.icon}
- </div>
- ) : before.type === "button" ? (
- <button
- type="button"
- disabled={disabled}
- onClick={before.onClick}
- class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-l-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
- >
- {before.children}
- </button>
- ) : undefined)}
+ {before && <RenderAddon disabled={disabled} addon={before} />}
{children}
- {after &&
- (after.type === "text" ? (
- <span class="inline-flex items-center rounded-r-md border border-l-0 border-gray-300 px-3 text-gray-500 sm:text-sm">
- {after.text}
- </span>
- ) : after.type === "icon" ? (
- <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
- {after.icon}
- </div>
- ) : after.type === "button" ? (
- <button
- type="button"
- disabled={disabled}
- onClick={after.onClick}
- class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
- >
- {after.children}
- </button>
- ) : undefined)}
+ {after && <RenderAddon disabled={disabled} addon={after} />}
</div>
{error && (
<p class="mt-2 text-sm text-red-600" id="email-error">
@@ -259,13 +254,13 @@ export function InputLine<T extends object, K extends keyof T>(
name={String(name)}
type={type}
onChange={(e) => {
- onChange(e.currentTarget.value as any);
+ onChange(fromString(e.currentTarget.value));
}}
placeholder={placeholder ? placeholder : undefined}
- value={value as string}
- onBlur={() => {
- onChange(fromString(value as any));
- }}
+ value={toString(value) ?? ""}
+ // onBlur={() => {
+ // onChange(fromString(value as any));
+ // }}
// defaultValue={toString(value)}
disabled={state.disabled}
aria-invalid={showError}