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')
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