/*
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;
}