/*
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 {
buildCodecForObject,
buildCodecForUnion,
Codec,
codecForBoolean,
codecForConstString,
codecForList,
codecForNumber,
codecForString,
codecForTimestamp,
codecOptional,
Integer,
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext } from "preact/hooks";
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
export type Type = UiForms;
const defaultForms: UiForms = {
forms: []
};
const Context = createContext(defaultForms);
export type BaseForm = Record;
export const useUiFormsContext = (): Type => useContext(Context);
export const UiFormsProvider = ({
children,
value,
}: {
value: UiForms;
children: ComponentChildren;
}): VNode => {
return h(Context.Provider, {
value,
children,
});
};
export type FormMetadata = {
label: string;
id: string;
version: number;
config: FlexibleForm;
};
type FlexibleForm = DoubleColumnForm;
export interface DoubleColumnForm {
type: "double-column";
design: Array;
// behavior?: (form: Partial) => FormState;
}
export type DoubleColumnFormSection = {
title: string;
description?: string;
fields: UIFormFieldConfig[];
};
// export interface BaseForm {
// state: TalerExchangeApi.AmlState;
// threshold: AmountJson;
// }
export interface UiForms {
// Where libeufin backend is localted
// default: window.origin without "webui/"
forms: Array;
}
export type UIFormFieldConfig =
| UIFormFieldConfigAbsoluteTime
| UIFormFieldConfigAmount
| UIFormFieldConfigArray
| UIFormFieldConfigCaption
| UIFormFieldConfigChoiseHorizontal
| UIFormFieldConfigChoiseStacked
| UIFormFieldConfigFile
| UIFormFieldConfigGroup
| UIFormFieldConfigInteger
| UIFormFieldConfigSelectMultiple
| UIFormFieldConfigSelectOne
| UIFormFieldConfigText
| UIFormFieldConfigTextArea
| UIFormFieldConfigToggle;
type UIFormFieldConfigAbsoluteTime = {
type: "absoluteTime";
properties: UIFormFieldBaseConfig & {
max?: TalerProtocolTimestamp;
min?: TalerProtocolTimestamp;
pattern: string;
};
};
type UIFormFieldConfigAmount = {
type: "amount";
properties: UIFormFieldBaseConfig & {
max?: Integer;
min?: Integer;
currency: string;
};
};
type UIFormFieldConfigArray = {
type: "array";
properties: UIFormFieldBaseConfig & {
// id of the field shown when the array is collapsed
labelFieldId: UIHandlerId;
fields: UIFormFieldConfig[];
};
};
type UIFormFieldConfigCaption = {
type: "caption";
properties: UIFieldBaseDescription;
};
type UIFormFieldConfigGroup = {
type: "group";
properties: UIFormFieldBaseConfig & {
fields: UIFormFieldConfig[];
};
};
type UIFormFieldConfigChoiseHorizontal = {
type: "choiceHorizontal";
properties: UIFormFieldBaseConfig & {
choices: Array;
};
};
type UIFormFieldConfigChoiseStacked = {
type: "choiceStacked";
properties: UIFormFieldBaseConfig & {
choices: Array;
};
};
type UIFormFieldConfigFile = {
type: "file";
properties: UIFormFieldBaseConfig & {
maxBytes?: Integer;
minBytes?: Integer;
// comma-separated list of one or more file types
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers
accept?: string;
};
};
type UIFormFieldConfigInteger = {
type: "integer";
properties: UIFormFieldBaseConfig & {
max?: Integer;
min?: Integer;
};
};
interface SelectUiChoice {
label: string;
description?: string;
value: string;
}
type UIFormFieldConfigSelectMultiple = {
type: "selectMultiple";
properties: UIFormFieldBaseConfig & {
max?: Integer;
min?: Integer;
unique?: boolean;
choices: Array;
};
};
type UIFormFieldConfigSelectOne = {
type: "selectOne";
properties: UIFormFieldBaseConfig & {
choices: Array;
};
};
type UIFormFieldConfigText = {
type: "text";
properties: UIFormFieldBaseConfig;
};
type UIFormFieldConfigTextArea = {
type: "textArea";
properties: UIFormFieldBaseConfig;
};
type UIFormFieldConfigToggle = {
type: "toggle";
properties: UIFormFieldBaseConfig;
};
type UIFieldBaseDescription = {
/* label if the field, visible for the user */
label: string;
/* long text to be shown on user demand */
tooltip?: string;
/* short text to be shown close to the field */
help?: string;
/* if the field should be initialy hidden */
hidden?: boolean;
/* ui element to show before */
addonBeforeId?: string;
/* ui element to show after */
addonAfterId?: string;
};
type UIFormFieldBaseConfig = UIFieldBaseDescription & {
/* example to be shown inside the field */
placeholder?: string;
/* show a mark as required */
required?: boolean;
/* readonly and dim */
disabled?: boolean;
/* name of the field, useful for a11y */
name: string;
/* conversion id to conver the string into the value type
the id should be known to the ui impl
*/
converterId?: string;
/* property id of the form */
id: UIHandlerId;
};
declare const __handlerId: unique symbol;
export type UIHandlerId = string & { [__handlerId]: true };
// FIXME: validate well formed ui field id
const codecForUiFieldId = codecForString as () => Codec;
const codecForUIFormFieldBaseConfigTemplate = <
T extends UIFormFieldBaseConfig,
>() =>
buildCodecForObject()
.property("id", codecForUiFieldId())
.property("addonAfterId", codecOptional(codecForString()))
.property("addonBeforeId", codecOptional(codecForString()))
.property("converterId", codecOptional(codecForString()))
.property("disabled", codecOptional(codecForBoolean()))
.property("hidden", codecOptional(codecForBoolean()))
.property("required", codecOptional(codecForBoolean()))
.property("help", codecOptional(codecForString()))
.property("label", codecForString())
.property("name", codecForString())
.property("placeholder", codecOptional(codecForString()))
.property("tooltip", codecOptional(codecForString()));
const codecForUIFormFieldBaseConfig = (): Codec =>
codecForUIFormFieldBaseConfigTemplate().build("UIFieldToggleProperties");
const codecForUIFormFieldAbsoluteTimeConfig = (): Codec<
UIFormFieldConfigAbsoluteTime["properties"]
> =>
codecForUIFormFieldBaseConfigTemplate<
UIFormFieldConfigAbsoluteTime["properties"]
>()
.property("pattern", codecForString())
.property("max", codecOptional(codecForTimestamp))
.property("min", codecOptional(codecForTimestamp))
.build("UIFormFieldConfigAbsoluteTime.properties");
const codecForUiFormFieldAbsoluteTime =
(): Codec =>
buildCodecForObject()
.property("type", codecForConstString("absoluteTime"))
.property("properties", codecForUIFormFieldAbsoluteTimeConfig())
.build("UIFormFieldConfigAbsoluteTime");
const codecForUIFormFieldAmountConfig = (): Codec<
UIFormFieldConfigAmount["properties"]
> =>
codecForUIFormFieldBaseConfigTemplate()
.property("currency", codecForString())
.property("max", codecOptional(codecForNumber()))
.property("min", codecOptional(codecForNumber()))
.build("UIFormFieldConfigAmount.properties");
const codecForUiFormFieldAmount = (): Codec =>
buildCodecForObject()
.property("type", codecForConstString("amount"))
.property("properties", codecForUIFormFieldAmountConfig())
.build("UIFormFieldConfigAmount");
const codecForUIFormFieldArrayConfig = (): Codec<
UIFormFieldConfigArray["properties"]
> =>
codecForUIFormFieldBaseConfigTemplate()
.property("labelFieldId", codecForUiFieldId())
.property("fields", codecForList(codecForUiFormField()))
.build("UIFormFieldConfigArray.properties");
const codecForUiFormFieldArray = (): Codec =>
buildCodecForObject()
.property("type", codecForConstString("array"))
.property("properties", codecForUIFormFieldArrayConfig())
.build("UIFormFieldConfigArray");
const codecForUiFormFieldCaption = (): Codec =>
buildCodecForObject()
.property("type", codecForConstString("caption"))
.property("properties", codecForUIFormFieldBaseConfig())
.build("UIFormFieldConfigCaption");
const codecForUiFormSelectUiChoice = (): Codec =>
buildCodecForObject()
.property("description", codecForString())
.property("label", codecForString())
.property("value", codecForString())
.build("SelectUiChoice");
const codecForUIFormFieldWithChoiseConfig = (): Codec<
UIFormFieldConfigChoiseHorizontal["properties"]
> =>
codecForUIFormFieldBaseConfigTemplate<
UIFormFieldConfigChoiseHorizontal["properties"]
>()
.property("choices", codecForList(codecForUiFormSelectUiChoice()))
.build("UIFormFieldConfigChoiseHorizontal.properties");
const codecForUiFormFieldChoiceHorizontal =
(): Codec =>
buildCodecForObject()
.property("type", codecForConstString("choiceHorizontal"))
.property("properties", codecForUIFormFieldWithChoiseConfig())
.build("UIFormFieldConfigChoiseHorizontal");
const codecForUiFormFieldChoiceStacked =
(): Codec =>
buildCodecForObject()
.property("type", codecForConstString("choiceStacked"))
.property("properties", codecForUIFormFieldWithChoiseConfig())
.build("UIFormFieldConfigChoiseStacked");
const codecForUiFormFieldFile = (): Codec =>
buildCodecForObject()
.property("type", codecForConstString("file"))
.property("properties", codecForUIFormFieldBaseConfig())
.build("UIFormFieldConfigFile");
const codecForUIFormFieldWithFieldsConfig = (): Codec<
UIFormFieldConfigGroup["properties"]
> =>
codecForUIFormFieldBaseConfigTemplate()
.property("fields", codecForList(codecForUiFormField()))
.build("UIFormFieldConfigGroup.properties");
const codecForUiFormFieldGroup = (): Codec =>
buildCodecForObject()
.property("type", codecForConstString("group"))
.property("properties", codecForUIFormFieldWithFieldsConfig())
.build("UiFormFieldGroup");
const codecForUiFormFieldInteger = (): Codec =>
buildCodecForObject()
.property("type", codecForConstString("integer"))
.property("properties", codecForUIFormFieldBaseConfig())
.build("UIFormFieldConfigInteger");
const codecForUIFormFieldSelectMultipleConfig = (): Codec<
UIFormFieldConfigSelectMultiple["properties"]
> =>
codecForUIFormFieldBaseConfigTemplate<
UIFormFieldConfigSelectMultiple["properties"]
>()
.property("max", codecOptional(codecForNumber()))
.property("min", codecOptional(codecForNumber()))
.property("unique", codecOptional(codecForBoolean()))
.property("choices", codecForList(codecForUiFormSelectUiChoice()))
.build("UIFormFieldConfigSelectMultiple.properties");
const codecForUiFormFieldSelectMultiple =
(): Codec =>
buildCodecForObject()
.property("type", codecForConstString("selectMultiple"))
.property("properties", codecForUIFormFieldSelectMultipleConfig())
.build("UiFormFieldSelectMultiple");
const codecForUiFormFieldSelectOne = (): Codec =>
buildCodecForObject()
.property("type", codecForConstString("selectOne"))
.property("properties", codecForUIFormFieldWithChoiseConfig())
.build("UIFormFieldConfigSelectOne");
const codecForUiFormFieldText = (): Codec =>
buildCodecForObject()
.property("type", codecForConstString("text"))
.property("properties", codecForUIFormFieldBaseConfig())
.build("UIFormFieldConfigText");
const codecForUiFormFieldTextArea = (): Codec =>
buildCodecForObject()
.property("type", codecForConstString("textArea"))
.property("properties", codecForUIFormFieldBaseConfig())
.build("UIFormFieldConfigTextArea");
const codecForUiFormFieldToggle = (): Codec =>
buildCodecForObject()
.property("type", codecForConstString("toggle"))
.property("properties", codecForUIFormFieldBaseConfig())
.build("UIFormFieldConfigToggle");
const codecForUiFormField = (): Codec =>
buildCodecForUnion()
.discriminateOn("type")
.alternative("absoluteTime", codecForUiFormFieldAbsoluteTime())
.alternative("amount", codecForUiFormFieldAmount())
.alternative("array", codecForUiFormFieldArray())
.alternative("caption", codecForUiFormFieldCaption())
.alternative("choiceHorizontal", codecForUiFormFieldChoiceHorizontal())
.alternative("choiceStacked", codecForUiFormFieldChoiceStacked())
.alternative("file", codecForUiFormFieldFile())
.alternative("group", codecForUiFormFieldGroup())
.alternative("integer", codecForUiFormFieldInteger())
.alternative("selectMultiple", codecForUiFormFieldSelectMultiple())
.alternative("selectOne", codecForUiFormFieldSelectOne())
.alternative("text", codecForUiFormFieldText())
.alternative("textArea", codecForUiFormFieldTextArea())
.alternative("toggle", codecForUiFormFieldToggle())
.build("UIFormField");
const codecForDoubleColumnFormSection = (): Codec =>
buildCodecForObject()
.property("title", codecForString())
.property("description", codecForString())
.property("fields", codecForList(codecForUiFormField()))
.build("DoubleColumnFormSection");
const codecForDoubleColumnForm = (): Codec =>
buildCodecForObject()
.property("type", codecForConstString("double-column"))
.property("design", codecForList(codecForDoubleColumnFormSection()))
.build("DoubleColumnForm");
const codecForFlexibleForm = (): Codec =>
buildCodecForUnion()
.discriminateOn("type")
.alternative("double-column", codecForDoubleColumnForm())
.build("FlexibleForm");
const codecForFormMetadata = (): Codec =>
buildCodecForObject()
.property("label", codecForString())
.property("id", codecForString())
.property("version", codecForNumber())
.property("config", codecForFlexibleForm())
.build("FormMetadata");
const codecForUIForms = (): Codec =>
buildCodecForObject()
.property("forms", codecForList(codecForFormMetadata()))
.build("UiForms");
function removeUndefineField(obj: T): T {
const keys = Object.keys(obj) as Array;
return keys.reduce((prev, cur) => {
if (typeof prev[cur] === "undefined") {
delete prev[cur];
}
return prev;
}, obj);
}
export function fetchUiForms(listener: (s: UiForms) => void): void {
fetch("./forms.json")
.then((resp) => resp.json())
.then((json) => codecForUIForms().decode(json))
.then((result) =>
listener({
...defaultForms,
...removeUndefineField(result),
}),
)
.catch((e) => {
console.log("failed to fetch forms", e);
listener(defaultForms);
});
}