diff options
Diffstat (limited to 'packages/aml-backoffice-ui/src/hooks/form.ts')
-rw-r--r-- | packages/aml-backoffice-ui/src/hooks/form.ts | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts new file mode 100644 index 000000000..70b2db571 --- /dev/null +++ b/packages/aml-backoffice-ui/src/hooks/form.ts @@ -0,0 +1,227 @@ +/* + 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/CreateAccount.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; +} |