/* 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, TalerExchangeApi, TranslatedString, } from "@gnu-taler/taler-util"; import { UIFieldHandler, UIFormElementConfig, UIHandlerId, } from "@gnu-taler/web-util/browser"; import { useState } from "preact/hooks"; import { undefinedIfEmpty } from "../pages/Start.js"; // export type UIField = { // value: string | undefined; // onUpdate: (s: string) => void; // error: TranslatedString | undefined; // }; export type FormHandler<T> = { [k in keyof T]?: T[k] extends string ? UIFieldHandler : T[k] extends AmountJson ? UIFieldHandler : T[k] extends TalerExchangeApi.AmlState ? UIFieldHandler : FormHandler<T[k]>; }; export type FormValues<T> = { [k in keyof T]: T[k] extends string ? string | undefined : FormValues<T[k]>; }; export type RecursivePartial<T> = { [k in keyof T]?: T[k] extends string ? string : T[k] extends AmountJson ? AmountJson : T[k] extends TalerExchangeApi.AmlState ? TalerExchangeApi.AmlState : RecursivePartial<T[k]>; }; export type FormErrors<T> = { [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<T[k]>; }; export type FormStatus<T> = | { status: "ok"; result: T; errors: undefined; } | { status: "fail"; result: RecursivePartial<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>, ): [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>( object: Tree<T> | 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<T>, rest, notFoundValue); } export 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 undefinedIfEmpty({ [head]: setValueDeeper({}, rest, value) }); } return undefinedIfEmpty({ ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) }); } 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; }