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