From bf03157b6570af6804032e206a3c60a3c7e030f3 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 6 May 2024 12:47:45 -0300 Subject: add required validation --- packages/web-util/src/forms/Group.tsx | 11 +- packages/web-util/src/forms/converter.ts | 119 ++++++++++++++++ packages/web-util/src/forms/forms.ts | 229 ++++++++++++++++++++++++++++++- packages/web-util/src/forms/index.ts | 1 + packages/web-util/src/forms/ui-form.ts | 2 + 5 files changed, 358 insertions(+), 4 deletions(-) create mode 100644 packages/web-util/src/forms/converter.ts (limited to 'packages/web-util/src') diff --git a/packages/web-util/src/forms/Group.tsx b/packages/web-util/src/forms/Group.tsx index d5626be1d..f63fa4a9b 100644 --- a/packages/web-util/src/forms/Group.tsx +++ b/packages/web-util/src/forms/Group.tsx @@ -1,8 +1,11 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { VNode, h } from "preact"; import { LabelWithTooltipMaybeRequired, RenderAddon } from "./InputLine.js"; -import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js"; -import { Addon } from "./FormProvider.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"; interface Props { label: TranslatedString; @@ -32,7 +35,9 @@ export function Group({

)}
- +
); diff --git a/packages/web-util/src/forms/converter.ts b/packages/web-util/src/forms/converter.ts new file mode 100644 index 000000000..3a522bf7e --- /dev/null +++ b/packages/web-util/src/forms/converter.ts @@ -0,0 +1,119 @@ +/* + 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 + */ + +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}`); + } +} + +function amountConverter(config: any): StringConverter { + 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 { + 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 { + 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 undefined!; +} diff --git a/packages/web-util/src/forms/forms.ts b/packages/web-util/src/forms/forms.ts index 6bda2f674..4bd6b4924 100644 --- a/packages/web-util/src/forms/forms.ts +++ b/packages/web-util/src/forms/forms.ts @@ -13,7 +13,10 @@ 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, UIFieldBaseDescription } from "../index.browser.js"; +import { assertUnreachable, TranslatedString } from "@gnu-taler/taler-util"; +import {UIFormFieldBaseConfig, UIFormFieldConfig} from "./ui-form.js"; /** * Constrain the type with the ui props */ @@ -142,3 +145,227 @@ export function RenderAllFieldsByUiConfig({ // InputChoiceHorizontal: res.InputChoiceHorizontal(), // }; // } + +/** + * convert field configuration to render function + * + * @param i18n_ + * @param fieldConfig + * @param form + * @returns + */ +export function convertUiField( + i18n_: InternationalizationAPI, + fieldConfig: UIFormFieldConfig[], + 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.properties), + }; + return resp; + } + case "group": { + const resp: UIFormField = { + type: config.type, + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + fields: convertUiField(i18n_, config.properties.fields, form, getConverterById), + }, + }; + return resp; + } + } + // Input Fields + switch (config.type) { + case "array": { + return { + type: "array", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + labelField: config.properties.labelFieldId, + fields: convertUiField(i18n_, config.properties.fields, form, getConverterById), + }, + } as UIFormField; + } + case "absoluteTime": { + return { + type: "absoluteTime", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + }, + } as UIFormField; + } + case "amount": { + return { + type: "amount", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + }, + } as UIFormField; + } + case "choiceHorizontal": { + return { + type: "choiceHorizontal", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + choices: config.properties.choices, + }, + } as UIFormField; + } + case "choiceStacked": { + return { + type: "choiceStacked", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + choices: config.properties.choices, + + }, + }as UIFormField; + } + case "file":{ + return { + type: "file", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + accept: config.properties.accept, + maxBites: config.properties.maxBytes, + }, + } as UIFormField; + } + case "integer":{ + return { + type: "integer", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + }, + } as UIFormField; + } + case "selectMultiple":{ + return { + type: "selectMultiple", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + choices: config.properties.choices, + }, + } as UIFormField; + } + case "selectOne": { + return { + type: "selectOne", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + choices: config.properties.choices, + }, + } as UIFormField; + } + case "text": { + return { + type: "text", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + }, + } as UIFormField; + } + case "textArea": { + return { + type: "text", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + }, + } as UIFormField; + } + case "toggle": { + return { + type: "toggle", + properties: { + ...converBaseFieldsProps(i18n_, config.properties), + ...converInputFieldsProps(form, config.properties, getConverterById), + }, + } as UIFormField; + } + default: { + assertUnreachable(config); + } + } + }); +} + + + +function getAddonById(_id: string | undefined): Addon { + return undefined!; +} + + +type GetConverterById = ( + id: string | undefined, + config: unknown, +) => StringConverter; + + +function converInputFieldsProps( + form: object, + p: UIFormFieldBaseConfig, + getConverterById: GetConverterById, +) { + return { + converter: getConverterById(p.converterId, p), + handler: getValueDeeper2(form, p.id.split(".")), + name: p.name, + required: p.required, + disabled: p.disabled, + help: p.help, + placeholder: p.placeholder, + tooltip: p.tooltip, + label: p.label as TranslatedString, + }; +} + +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 getValueDeeper2( + object: Record, + 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.ts b/packages/web-util/src/forms/index.ts index 8c6c23ec5..7320c70d0 100644 --- a/packages/web-util/src/forms/index.ts +++ b/packages/web-util/src/forms/index.ts @@ -20,5 +20,6 @@ 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" diff --git a/packages/web-util/src/forms/ui-form.ts b/packages/web-util/src/forms/ui-form.ts index 9bbc2e96c..212efd506 100644 --- a/packages/web-util/src/forms/ui-form.ts +++ b/packages/web-util/src/forms/ui-form.ts @@ -262,6 +262,7 @@ const codecForUIFormFieldArrayConfig = (): Codec< > => codecForUIFormFieldBaseConfigTemplate() .property("labelFieldId", codecForUiFieldId()) + // eslint-disable-next-line @typescript-eslint/no-use-before-define .property("fields", codecForList(codecForUiFormField())) .build("UIFormFieldConfigArray.properties"); @@ -328,6 +329,7 @@ const codecForUIFormFieldWithFieldsConfig = (): Codec< codecForUIFormFieldBaseDescriptionTemplate< UIFormFieldConfigGroup["properties"] >() + // eslint-disable-next-line @typescript-eslint/no-use-before-define .property("fields", codecForList(codecForUiFormField())) .build("UIFormFieldConfigGroup.properties"); -- cgit v1.2.3