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