/* 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, TalerExchangeApi, TranslatedString, assertUnreachable, } from "@gnu-taler/taler-util"; import { Addon, InternationalizationAPI, UIFieldBaseDescription, UIFieldHandler, UIFormField, UIFormFieldBaseConfig, UIFormFieldConfig, UIHandlerId } from "@gnu-taler/web-util/browser"; import { useState } from "preact/hooks"; import { getConverterById } from "../utils/converter.js"; // export type UIField = { // value: string | undefined; // onUpdate: (s: string) => void; // error: TranslatedString | undefined; // }; export type FormHandler = { [k in keyof T]?: T[k] extends string ? UIFieldHandler : T[k] extends AmountJson ? UIFieldHandler : T[k] extends TalerExchangeApi.AmlState ? UIFieldHandler : FormHandler; }; export type FormValues = { [k in keyof T]: T[k] extends string ? string | undefined : FormValues; }; export type RecursivePartial = { [k in keyof T]?: T[k] extends string ? string : T[k] extends AmountJson ? AmountJson : T[k] extends TalerExchangeApi.AmlState ? TalerExchangeApi.AmlState : RecursivePartial; }; export type FormErrors = { [k in keyof T]?: T[k] extends string ? TranslatedString : T[k] extends AmountJson ? TranslatedString : T[k] extends AbsoluteTime ? TranslatedString : T[k] extends TalerExchangeApi.AmlState ? TranslatedString : FormErrors; }; export type FormStatus = | { status: "ok"; result: T; errors: undefined; } | { status: "fail"; result: RecursivePartial; errors: FormErrors; }; function constructFormHandler( shape: Array, form: RecursivePartial>, updateForm: (d: RecursivePartial>) => void, errors: FormErrors | undefined, ): FormHandler { const handler = shape.reduce((handleForm, fieldId) => { const path = fieldId.split(".") function updater(newValue: unknown) { updateForm(setValueDeeper(form, path, newValue)); } const currentValue = getValueDeeper(form as any, path, undefined) const currentError = getValueDeeper(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); return handler; } /** * FIXME: Consider sending this to web-utils * * * @param defaultValue * @param check * @returns */ export function useFormState( shape: Array, defaultValue: RecursivePartial>, check: (f: RecursivePartial>) => FormStatus, ): [FormHandler, FormStatus] { const [form, updateForm] = useState>>(defaultValue); const status = check(form); const handler = constructFormHandler(shape, form, updateForm, status.errors); return [handler, status]; } interface Tree extends Record | T> {} function getValueDeeper( object: Tree | undefined, names: string[], notFoundValue?: T, ): T | undefined { if (names.length === 0) return object as T; const [head, ...rest] = names; if (!head) { return getValueDeeper(object, rest, notFoundValue); } if (object === undefined) { return notFoundValue } return getValueDeeper(object[head] as Tree, rest, notFoundValue); } 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); } 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) }; } function getAddonById(_id: string | undefined): Addon { return undefined!; } function converInputFieldsProps( form: FormHandler, p: UIFormFieldBaseConfig, ) { 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}`, }; } export function convertUiField( i18n_: InternationalizationAPI, fieldConfig: UIFormFieldConfig[], form: FormHandler, ): 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 "array": { return { type: "array", properties: { ...converBaseFieldsProps(i18n_, config.properties), ...converInputFieldsProps(form, config.properties), labelField: config.properties.labelFieldId, fields: convertUiField(i18n_, config.properties.fields, form), }, } as UIFormField; } case "absoluteTime": { return { type: "absoluteTime", properties: { ...converBaseFieldsProps(i18n_, config.properties), ...converInputFieldsProps(form, config.properties), }, } as UIFormField; } case "amount": { return { type: "amount", properties: { ...converBaseFieldsProps(i18n_, config.properties), ...converInputFieldsProps(form, config.properties), }, } as UIFormField; } case "choiceHorizontal": { return { type: "choiceHorizontal", properties: { ...converBaseFieldsProps(i18n_, config.properties), ...converInputFieldsProps(form, config.properties), choices: config.properties.choices, }, } as UIFormField; } case "choiceStacked": { return { type: "choiceStacked", properties: { ...converBaseFieldsProps(i18n_, config.properties), ...converInputFieldsProps(form, config.properties), choices: config.properties.choices, }, }as UIFormField; } case "file":{ console.log("ASDASD", config.properties.accept) return { type: "file", properties: { ...converBaseFieldsProps(i18n_, config.properties), ...converInputFieldsProps(form, config.properties), accept: config.properties.accept, maxBites: config.properties.maxBytes, }, } as UIFormField; } case "integer":{ return { type: "integer", properties: { ...converBaseFieldsProps(i18n_, config.properties), ...converInputFieldsProps(form, config.properties), }, } as UIFormField; } case "selectMultiple":{ return { type: "selectMultiple", properties: { ...converBaseFieldsProps(i18n_, config.properties), ...converInputFieldsProps(form, config.properties), choices: config.properties.choices, }, } as UIFormField; } case "selectOne": { return { type: "selectOne", properties: { ...converBaseFieldsProps(i18n_, config.properties), ...converInputFieldsProps(form, config.properties), choices: config.properties.choices, }, } as UIFormField; } 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 { type: "toggle", properties: { ...converBaseFieldsProps(i18n_, config.properties), ...converInputFieldsProps(form, config.properties), }, } as UIFormField; } default: { assertUnreachable(config); } } }); }