/* 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, } from "@gnu-taler/taler-util"; import { UIFieldHandler, UIFormFieldConfig, UIHandlerId, } from "@gnu-taler/web-util/browser"; import { useState } from "preact/hooks"; import { undefinedIfEmpty } from "../pages/CreateAccount.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> {} export 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); } 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: UIFormFieldConfig[], ): Array { const shape: Array = []; fields.forEach((field) => { if ("id" in field.properties) { // FIXME: this should be a validation when loading the form // consistency check if (shape.indexOf(field.properties.id) !== -1) { throw Error(`already present: ${field.properties.id}`); } shape.push(field.properties.id); } else if (field.type === "group") { Array.prototype.push.apply( shape, getShapeFromFields(field.properties.fields), ); } }); return shape; } export function getRequiredFields( fields: UIFormFieldConfig[], ): Array { const shape: Array = []; fields.forEach((field) => { if ("id" in field.properties) { // FIXME: this should be a validation when loading the form // consistency check if (shape.indexOf(field.properties.id) !== -1) { throw Error(`already present: ${field.properties.id}`); } if (!field.properties.required) { return; } shape.push(field.properties.id); } else if (field.type === "group") { Array.prototype.push.apply( shape, getRequiredFields(field.properties.fields), ); } }); return shape; } export function validateRequiredFields( errors: FormErrors | undefined, form: object, fields: Array, ): FormErrors | undefined { let result: FormErrors | 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; }