summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-05-03 18:23:01 -0300
committerSebastian <sebasjm@gmail.com>2024-05-03 18:23:01 -0300
commit35fee72ef3d75b7a9681353ab7a1ca5bacff150e (patch)
treeab5fd0588c50d389a24651a18ca8756b39cd7772 /packages
parent5db79542f34477911f14f2e454925368c0d2c33f (diff)
downloadwallet-core-35fee72ef3d75b7a9681353ab7a1ca5bacff150e.tar.gz
wallet-core-35fee72ef3d75b7a9681353ab7a1ca5bacff150e.tar.bz2
wallet-core-35fee72ef3d75b7a9681353ab7a1ca5bacff150e.zip
form implemented, moving functions to web-utils some final testing still pedning
Diffstat (limited to 'packages')
-rwxr-xr-xpackages/aml-backoffice-ui/dev.mjs2
-rw-r--r--packages/aml-backoffice-ui/src/App.tsx7
-rw-r--r--packages/aml-backoffice-ui/src/context/ui-forms.ts439
-rw-r--r--packages/aml-backoffice-ui/src/forms.json539
-rw-r--r--packages/aml-backoffice-ui/src/forms/index.ts3
-rw-r--r--packages/aml-backoffice-ui/src/forms/simplest.ts8
-rw-r--r--packages/aml-backoffice-ui/src/hooks/form.ts249
-rw-r--r--packages/aml-backoffice-ui/src/index.html1
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseDetails.tsx19
-rw-r--r--packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx175
-rw-r--r--packages/aml-backoffice-ui/src/pages/Cases.tsx2
-rw-r--r--packages/aml-backoffice-ui/src/pages/CreateAccount.tsx2
-rw-r--r--packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx31
-rw-r--r--packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx2
-rw-r--r--packages/aml-backoffice-ui/src/settings.json4
-rw-r--r--packages/aml-backoffice-ui/src/utils/converter.ts73
-rw-r--r--packages/taler-util/src/codec.ts13
-rw-r--r--packages/web-util/src/forms/DefaultForm.tsx13
-rw-r--r--packages/web-util/src/forms/InputAbsoluteTime.stories.tsx4
-rw-r--r--packages/web-util/src/forms/InputAmount.stories.tsx4
-rw-r--r--packages/web-util/src/forms/InputArray.stories.tsx4
-rw-r--r--packages/web-util/src/forms/InputArray.tsx3
-rw-r--r--packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx4
-rw-r--r--packages/web-util/src/forms/InputChoiceStacked.stories.tsx4
-rw-r--r--packages/web-util/src/forms/InputFile.stories.tsx4
-rw-r--r--packages/web-util/src/forms/InputFile.tsx54
-rw-r--r--packages/web-util/src/forms/InputInteger.stories.tsx4
-rw-r--r--packages/web-util/src/forms/InputLine.stories.tsx4
-rw-r--r--packages/web-util/src/forms/InputSelectMultiple.stories.tsx4
-rw-r--r--packages/web-util/src/forms/InputSelectMultiple.tsx2
-rw-r--r--packages/web-util/src/forms/InputSelectOne.stories.tsx4
-rw-r--r--packages/web-util/src/forms/InputText.stories.tsx4
-rw-r--r--packages/web-util/src/forms/InputTextArea.stories.tsx4
-rw-r--r--packages/web-util/src/forms/InputToggle.stories.tsx4
-rw-r--r--packages/web-util/src/forms/index.ts1
-rw-r--r--packages/web-util/src/forms/ui-form.ts451
36 files changed, 1413 insertions, 732 deletions
diff --git a/packages/aml-backoffice-ui/dev.mjs b/packages/aml-backoffice-ui/dev.mjs
index 4b58116d1..e91b48f9d 100755
--- a/packages/aml-backoffice-ui/dev.mjs
+++ b/packages/aml-backoffice-ui/dev.mjs
@@ -24,7 +24,7 @@ const build = initializeDev({
type: "development",
source: {
js: devEntryPoints,
- assets: [{ base: "src", files: ["src/index.html"] }],
+ assets: [{ base: "src", files: ["src/index.html","src/forms.json","src/settings.json"] }],
},
destination: "./dist/dev",
css: "postcss",
diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx
index ae8b574b6..e9be84441 100644
--- a/packages/aml-backoffice-ui/src/App.tsx
+++ b/packages/aml-backoffice-ui/src/App.tsx
@@ -19,6 +19,7 @@ import {
ExchangeApiProvider,
Loading,
TranslationProvider,
+ UiForms,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
@@ -29,7 +30,7 @@ import { UiSettingsProvider } from "./context/ui-settings.js";
import { strings } from "./i18n/strings.js";
import "./scss/main.css";
import { UiSettings, fetchUiSettings } from "./context/ui-settings.js";
-import { UiForms, fetchUiForms } from "./context/ui-forms.js";
+import { UiFormsProvider, fetchUiForms } from "./context/ui-forms.js";
const WITH_LOCAL_STORAGE_CACHE = false;
@@ -84,7 +85,9 @@ export function App(): VNode {
}}
>
<BrowserHashNavigationProvider>
- <Routing />
+ <UiFormsProvider value={forms}>
+ <Routing />
+ </UiFormsProvider>
</BrowserHashNavigationProvider>
</SWRConfig>
</ExchangeApiProvider>
diff --git a/packages/aml-backoffice-ui/src/context/ui-forms.ts b/packages/aml-backoffice-ui/src/context/ui-forms.ts
index 9cf6125c9..3a25234d2 100644
--- a/packages/aml-backoffice-ui/src/context/ui-forms.ts
+++ b/packages/aml-backoffice-ui/src/context/ui-forms.ts
@@ -14,20 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import {
- buildCodecForObject,
- buildCodecForUnion,
- Codec,
- codecForBoolean,
- codecForConstString,
- codecForList,
- codecForNumber,
- codecForString,
- codecForTimestamp,
- codecOptional,
- Integer,
- TalerProtocolTimestamp,
-} from "@gnu-taler/taler-util";
+import { codecForUIForms, UiForms } from "@gnu-taler/web-util/browser";
import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext } from "preact/hooks";
@@ -60,431 +47,7 @@ export const UiFormsProvider = ({
});
};
-export type FormMetadata = {
- label: string;
- id: string;
- version: number;
- config: FlexibleForm;
-};
-
-type FlexibleForm = DoubleColumnForm;
-
-export interface DoubleColumnForm {
- type: "double-column";
- design: Array<DoubleColumnFormSection>;
- // behavior?: (form: Partial<T>) => FormState<T>;
-}
-
-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<FormMetadata>;
-}
-
-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: UIFieldBaseDescription & {
- fields: UIFormFieldConfig[];
- };
-};
-
-type UIFormFieldConfigChoiseHorizontal = {
- type: "choiceHorizontal";
- properties: UIFormFieldBaseConfig & {
- choices: Array<SelectUiChoice>;
- };
-};
-
-type UIFormFieldConfigChoiseStacked = {
- type: "choiceStacked";
- properties: UIFormFieldBaseConfig & {
- choices: Array<SelectUiChoice>;
- };
-};
-
-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<SelectUiChoice>;
- };
-};
-type UIFormFieldConfigSelectOne = {
- type: "selectOne";
- properties: UIFormFieldBaseConfig & {
- choices: Array<SelectUiChoice>;
- };
-};
-type UIFormFieldConfigText = {
- type: "text";
- properties: UIFormFieldBaseConfig;
-};
-type UIFormFieldConfigTextArea = {
- type: "textArea";
- properties: UIFormFieldBaseConfig;
-};
-type UIFormFieldConfigToggle = {
- type: "toggle";
- properties: UIFormFieldBaseConfig;
-};
-
-export 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;
-
- /* name of the field, useful for a11y */
- name: string;
-
- /* if the field should be initialy hidden */
- hidden?: boolean;
- /* ui element to show before */
- addonBeforeId?: string;
- /* ui element to show after */
- addonAfterId?: string;
-};
-
-export type UIFormFieldBaseConfig = UIFieldBaseDescription & {
- /* example to be shown inside the field */
- placeholder?: string;
-
- /* show a mark as required */
- required?: boolean;
-
- /* readonly and dim */
- disabled?: boolean;
-
- /* 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<UIHandlerId>;
-
-const codecForUIFormFieldBaseDescriptionTemplate = <
- T extends UIFieldBaseDescription,
->() =>
- buildCodecForObject<T>()
- .property("addonAfterId", codecOptional(codecForString()))
- .property("addonBeforeId", codecOptional(codecForString()))
- .property("hidden", codecOptional(codecForBoolean()))
- .property("help", codecOptional(codecForString()))
- .property("label", codecForString())
- .property("name", codecForString())
- .property("tooltip", codecOptional(codecForString()));
-
-const codecForUIFormFieldBaseConfigTemplate = <
- T extends UIFormFieldBaseConfig,
->() =>
- codecForUIFormFieldBaseDescriptionTemplate<T>()
- .property("id", codecForUiFieldId())
- .property("converterId", codecOptional(codecForString()))
- .property("disabled", codecOptional(codecForBoolean()))
- .property("required", codecOptional(codecForBoolean()))
- .property("placeholder", codecOptional(codecForString()));
-const codecForUIFormFieldBaseConfig = (): Codec<UIFormFieldBaseConfig> =>
- 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<UIFormFieldConfigAbsoluteTime> =>
- buildCodecForObject<UIFormFieldConfigAbsoluteTime>()
- .property("type", codecForConstString("absoluteTime"))
- .property("properties", codecForUIFormFieldAbsoluteTimeConfig())
- .build("UIFormFieldConfigAbsoluteTime");
-
-const codecForUIFormFieldAmountConfig = (): Codec<
- UIFormFieldConfigAmount["properties"]
-> =>
- codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigAmount["properties"]>()
- .property("currency", codecForString())
- .property("max", codecOptional(codecForNumber()))
- .property("min", codecOptional(codecForNumber()))
- .build("UIFormFieldConfigAmount.properties");
-
-const codecForUiFormFieldAmount = (): Codec<UIFormFieldConfigAmount> =>
- buildCodecForObject<UIFormFieldConfigAmount>()
- .property("type", codecForConstString("amount"))
- .property("properties", codecForUIFormFieldAmountConfig())
- .build("UIFormFieldConfigAmount");
-
-const codecForUIFormFieldArrayConfig = (): Codec<
- UIFormFieldConfigArray["properties"]
-> =>
- codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigArray["properties"]>()
- .property("labelFieldId", codecForUiFieldId())
- .property("fields", codecForList(codecForUiFormField()))
- .build("UIFormFieldConfigArray.properties");
-
-const codecForUiFormFieldArray = (): Codec<UIFormFieldConfigArray> =>
- buildCodecForObject<UIFormFieldConfigArray>()
- .property("type", codecForConstString("array"))
- .property("properties", codecForUIFormFieldArrayConfig())
- .build("UIFormFieldConfigArray");
-
-const codecForUiFormFieldCaption = (): Codec<UIFormFieldConfigCaption> =>
- buildCodecForObject<UIFormFieldConfigCaption>()
- .property("type", codecForConstString("caption"))
- .property("properties", codecForUIFormFieldBaseConfig())
- .build("UIFormFieldConfigCaption");
-
-const codecForUiFormSelectUiChoice = (): Codec<SelectUiChoice> =>
- buildCodecForObject<SelectUiChoice>()
- .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<UIFormFieldConfigChoiseHorizontal> =>
- buildCodecForObject<UIFormFieldConfigChoiseHorizontal>()
- .property("type", codecForConstString("choiceHorizontal"))
- .property("properties", codecForUIFormFieldWithChoiseConfig())
- .build("UIFormFieldConfigChoiseHorizontal");
-
-const codecForUiFormFieldChoiceStacked =
- (): Codec<UIFormFieldConfigChoiseStacked> =>
- buildCodecForObject<UIFormFieldConfigChoiseStacked>()
- .property("type", codecForConstString("choiceStacked"))
- .property("properties", codecForUIFormFieldWithChoiseConfig())
- .build("UIFormFieldConfigChoiseStacked");
-
-const codecForUiFormFieldFile = (): Codec<UIFormFieldConfigFile> =>
- buildCodecForObject<UIFormFieldConfigFile>()
- .property("type", codecForConstString("file"))
- .property("properties", codecForUIFormFieldBaseConfig())
- .build("UIFormFieldConfigFile");
-
-const codecForUIFormFieldWithFieldsConfig = (): Codec<
- UIFormFieldConfigGroup["properties"]
-> =>
- codecForUIFormFieldBaseDescriptionTemplate<
- UIFormFieldConfigGroup["properties"]
- >()
- .property("fields", codecForList(codecForUiFormField()))
- .build("UIFormFieldConfigGroup.properties");
-
-const codecForUiFormFieldGroup = (): Codec<UIFormFieldConfigGroup> =>
- buildCodecForObject<UIFormFieldConfigGroup>()
- .property("type", codecForConstString("group"))
- .property("properties", codecForUIFormFieldWithFieldsConfig())
- .build("UiFormFieldGroup");
-
-const codecForUiFormFieldInteger = (): Codec<UIFormFieldConfigInteger> =>
- buildCodecForObject<UIFormFieldConfigInteger>()
- .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<UIFormFieldConfigSelectMultiple> =>
- buildCodecForObject<UIFormFieldConfigSelectMultiple>()
- .property("type", codecForConstString("selectMultiple"))
- .property("properties", codecForUIFormFieldSelectMultipleConfig())
- .build("UiFormFieldSelectMultiple");
-
-const codecForUiFormFieldSelectOne = (): Codec<UIFormFieldConfigSelectOne> =>
- buildCodecForObject<UIFormFieldConfigSelectOne>()
- .property("type", codecForConstString("selectOne"))
- .property("properties", codecForUIFormFieldWithChoiseConfig())
- .build("UIFormFieldConfigSelectOne");
-
-const codecForUiFormFieldText = (): Codec<UIFormFieldConfigText> =>
- buildCodecForObject<UIFormFieldConfigText>()
- .property("type", codecForConstString("text"))
- .property("properties", codecForUIFormFieldBaseConfig())
- .build("UIFormFieldConfigText");
-
-const codecForUiFormFieldTextArea = (): Codec<UIFormFieldConfigTextArea> =>
- buildCodecForObject<UIFormFieldConfigTextArea>()
- .property("type", codecForConstString("textArea"))
- .property("properties", codecForUIFormFieldBaseConfig())
- .build("UIFormFieldConfigTextArea");
-
-const codecForUiFormFieldToggle = (): Codec<UIFormFieldConfigToggle> =>
- buildCodecForObject<UIFormFieldConfigToggle>()
- .property("type", codecForConstString("toggle"))
- .property("properties", codecForUIFormFieldBaseConfig())
- .build("UIFormFieldConfigToggle");
-
-const codecForUiFormField = (): Codec<UIFormFieldConfig> =>
- buildCodecForUnion<UIFormFieldConfig>()
- .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<DoubleColumnFormSection> =>
- buildCodecForObject<DoubleColumnFormSection>()
- .property("title", codecForString())
- .property("description", codecForString())
- .property("fields", codecForList(codecForUiFormField()))
- .build("DoubleColumnFormSection");
-
-const codecForDoubleColumnForm = (): Codec<DoubleColumnForm> =>
- buildCodecForObject<DoubleColumnForm>()
- .property("type", codecForConstString("double-column"))
- .property("design", codecForList(codecForDoubleColumnFormSection()))
- .build("DoubleColumnForm");
-
-const codecForFlexibleForm = (): Codec<FlexibleForm> =>
- buildCodecForUnion<FlexibleForm>()
- .discriminateOn("type")
- .alternative("double-column", codecForDoubleColumnForm())
- .build<FlexibleForm>("FlexibleForm");
-
-const codecForFormMetadata = (): Codec<FormMetadata> =>
- buildCodecForObject<FormMetadata>()
- .property("label", codecForString())
- .property("id", codecForString())
- .property("version", codecForNumber())
- .property("config", codecForFlexibleForm())
- .build("FormMetadata");
-const codecForUIForms = (): Codec<UiForms> =>
- buildCodecForObject<UiForms>()
- .property("forms", codecForList(codecForFormMetadata()))
- .build("UiForms");
function removeUndefineField<T extends object>(obj: T): T {
const keys = Object.keys(obj) as Array<keyof T>;
diff --git a/packages/aml-backoffice-ui/src/forms.json b/packages/aml-backoffice-ui/src/forms.json
new file mode 100644
index 000000000..de0da601c
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/forms.json
@@ -0,0 +1,539 @@
+{
+ "forms": [
+ {
+ "label": "Information on customer",
+ "id": "902_1e",
+ "version": 1,
+ "config": {
+ "type": "double-column",
+ "design": [
+ {
+ "title": "Information on customer",
+ "description": "The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer.",
+ "fields": [
+ {
+ "type": "choiceStacked",
+ "properties": {
+ "name": "customerType",
+ "id": ".customerType",
+ "label": "Type of customer",
+ "required": true,
+ "choices": [
+ {
+ "label": "Natural person",
+ "value": "natural"
+ },
+ {
+ "label": "Legal entity",
+ "value": "legal"
+ }
+ ]
+ }
+ },
+ {
+ "type": "group",
+ "properties": {
+ "label": "Natural customer form",
+ "name": "algo",
+ "id": "algo",
+ "before": "a) Country risk (nationality)",
+ "after": "a) Country risk (nationality)",
+ "fields": [
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.fullName",
+ "id": ".naturalCustomer.fullName",
+ "label": "Full name",
+ "required": true
+ }
+ }
+ ]
+ }
+ },
+
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.address",
+ "id": ".naturalCustomer.address",
+ "label": "Residential address",
+ "required": true
+ }
+ },
+ {
+ "type": "integer",
+ "properties": {
+ "name": "naturalCustomer.telephone",
+ "id": ".naturalCustomer.telephone",
+ "label": "Telephone"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.email",
+ "id": ".naturalCustomer.email",
+ "label": "E-mail"
+ }
+ },
+ {
+ "type": "absoluteTime",
+ "properties": {
+ "pattern": "dd/MM/yyyy",
+ "name": "naturalCustomer.dateOfBirth",
+ "id": ".naturalCustomer.dateOfBirth",
+ "label": "Date of birth",
+ "required": true
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.nationality",
+ "id": ".naturalCustomer.nationality",
+ "label": "Nationality",
+ "required": true
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.document",
+ "id": ".naturalCustomer.document",
+ "label": "Identification document",
+ "required": true
+ }
+ },
+ {
+ "type": "file",
+ "properties": {
+ "name": "naturalCustomer.documentAttachment",
+ "id": ".naturalCustomer.documentAttachment",
+ "label": "Document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".pdf",
+ "help": "PDF file with max size of 2 mega bytes"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.companyName",
+ "id": ".naturalCustomer.companyName",
+ "label": "Company name"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.office",
+ "id": ".naturalCustomer.office",
+ "label": "Registered office"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "naturalCustomer.companyDocument",
+ "id": ".naturalCustomer.companyDocument",
+ "label": "Company identification document"
+ }
+ },
+ {
+ "type": "file",
+ "properties": {
+ "name": "naturalCustomer.companyDocumentAttachment",
+ "id": ".naturalCustomer.companyDocumentAttachment",
+ "label": "Document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".png",
+ "help": "PNG file with max size of 2 mega bytes"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "legalCustomer.companyName",
+ "id": ".legalCustomer.companyName",
+ "label": "Company name",
+ "required": true
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "legalCustomer.domicile",
+ "id": ".legalCustomer.domicile",
+ "label": "Domicile",
+ "required": true
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "legalCustomer.contactPerson",
+ "id": ".legalCustomer.contactPerson",
+ "label": "Contact person"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "legalCustomer.telephone",
+ "id": ".legalCustomer.telephone",
+ "label": "Telephone"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "legalCustomer.email",
+ "id": ".legalCustomer.email",
+ "label": "E-mail"
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "legalCustomer.document",
+ "id": ".legalCustomer.document",
+ "label": "Identification document",
+ "help": "Not older than 12 month"
+ }
+ },
+ {
+ "type": "file",
+ "properties": {
+ "name": "legalCustomer.documentAttachment",
+ "id": ".legalCustomer.documentAttachment",
+ "label": "Document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".png",
+ "help": "PNG file with max size of 2 mega bytes"
+ }
+ }
+ ]
+ },
+ {
+ "title": "Information on the natural persons who establish the business relationship for legal entities and partnerships",
+ "description": "For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified.",
+ "fields": [
+ {
+ "type": "array",
+ "properties": {
+ "name": "businessEstablisher",
+ "id": ".businessEstablisher",
+ "label": "Persons",
+ "required": true,
+ "labelFieldId": "fullName",
+ "placeholder": "this is the placeholder",
+ "fields": [
+ {
+ "type": "text",
+ "properties": {
+ "name": "fullName",
+ "id": ".fullName",
+ "label": "Full name",
+ "required": true
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "address",
+ "id": ".address",
+ "label": "Residential address",
+ "required": true
+ }
+ },
+ {
+ "type": "absoluteTime",
+ "properties": {
+ "pattern": "dd/MM/yyyy",
+ "name": "dateOfBirth",
+ "id": ".dateOfBirth",
+ "label": "Date of birth",
+ "required": true
+ }
+ },
+
+ {
+ "type": "text",
+ "properties": {
+ "name": "nationality",
+ "id": ".nationality",
+ "label": "Nationality",
+ "required": true
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "typeOfAuthorization",
+ "id": ".typeOfAuthorization",
+ "label": "Type of authorization (signatory of representation)",
+ "required": true
+ }
+ },
+ {
+ "type": "file",
+ "properties": {
+ "name": "documentAttachment",
+ "id": ".documentAttachment",
+ "label": "Identification document attachment",
+ "required": true,
+ "maxBites": 2097152,
+ "accept": ".pdf",
+ "help": "PDF file with max size of 2 mega bytes"
+ }
+ },
+ {
+ "type": "choiceStacked",
+ "properties": {
+ "name": "powerOfAttorneyArrangements",
+ "id": ".powerOfAttorneyArrangements",
+ "label": "Power of attorney arrangements",
+ "required": true,
+ "choices": [
+ {
+ "label": "CR extract",
+ "value": "cr"
+ },
+ {
+ "label": "Mandate",
+ "value": "mandate"
+ },
+ {
+ "label": "Other",
+ "value": "other"
+ }
+ ]
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "powerOfAttorneyArrangementsOther",
+ "id": ".powerOfAttorneyArrangementsOther",
+ "label": "Power of attorney arrangements",
+ "required": true
+ }
+ }
+ ],
+ "labelField": "fullName"
+ }
+ }
+ ]
+ },
+ {
+ "title": "Acceptance of business relationship",
+ "fields": [
+ {
+ "type": "absoluteTime",
+ "properties": {
+ "name": "acceptance.when",
+ "id": ".acceptance.when",
+ "pattern": "dd/MM/yyyy",
+ "converterId": "Taler.AbsoluteTime",
+ "label": "Date (conclusion of contract)"
+ }
+ },
+ {
+ "type": "choiceStacked",
+ "properties": {
+ "name": "acceptance.acceptedBy",
+ "id": ".acceptance.acceptedBy",
+ "label": "Accepted by",
+ "required": true,
+ "choices": [
+ {
+ "label": "Face-to-face meeting with customer",
+ "value": "face-to-face"
+ },
+ {
+ "label": "Correspondence: authenticated copy of identification document obtained",
+ "value": "correspondence-document"
+ },
+ {
+ "label": "Correspondence: residential address validated",
+ "value": "correspondence-address"
+ }
+ ]
+ }
+ },
+ {
+ "type": "choiceStacked",
+ "properties": {
+ "name": "acceptance.typeOfCorrespondence",
+ "id": ".acceptance.typeOfCorrespondence",
+ "label": "Type of correspondence service",
+ "choices": [
+ {
+ "label": "to the customer",
+ "value": "customer"
+ },
+ {
+ "label": "hold at bank",
+ "value": "bank"
+ },
+ {
+ "label": "to the member",
+ "value": "member"
+ },
+ {
+ "label": "to a third party",
+ "value": "third-party"
+ }
+ ]
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "acceptance.thirdPartyFullName",
+ "id": ".acceptance.thirdPartyFullName",
+ "label": "Third party full name",
+ "required": true
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "acceptance.thirdPartyAddress",
+ "id": ".acceptance.thirdPartyAddress",
+ "label": "Third party address",
+ "required": true
+ }
+ },
+ {
+ "type": "selectMultiple",
+ "properties": {
+ "name": "acceptance.language",
+ "id": ".acceptance.language",
+ "label": "Languages",
+ "choices": [
+ {
+ "label": "Espanol",
+ "value": "es"
+ }
+ ],
+ "unique": true
+ }
+ },
+ {
+ "type": "textArea",
+ "properties": {
+ "name": "acceptance.furtherInformation",
+ "id": ".acceptance.furtherInformation",
+ "label": "Further information"
+ }
+ }
+ ]
+ },
+ {
+ "title": "Information on the beneficial owner of the assets and/or controlling person",
+ "description": "Establishment of the beneficial owner of the assets and/or controlling person",
+ "fields": [
+ {
+ "type": "choiceStacked",
+ "properties": {
+ "name": "establishment",
+ "id": ".establishment",
+ "label": "The customer is",
+ "required": true,
+ "choices": [
+ {
+ "label": "a natural person and there are no doubts that this person is the sole beneficial owner of the assets",
+ "value": "natural"
+ },
+ {
+ "label": "a foundation (or a similar construct; incl. underlying companies)",
+ "value": "foundation"
+ },
+ {
+ "label": "a trust (incl. underlying companies)",
+ "value": "trust"
+ },
+ {
+ "label": "a life insurance policy with separately managed accounts/securities accounts",
+ "value": "insurance-wrapper"
+ },
+ {
+ "label": "all other cases",
+ "value": "other"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "title": "Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship",
+ "description": "Verification whether the customer, beneficial owners of the assets, controlling persons, authorized representatives or other involved persons are listed on an embargo/terrorism list (date of verification/result)",
+ "fields": [
+ {
+ "type": "textArea",
+ "properties": {
+ "name": "embargoEvaluation",
+ "id": ".embargoEvaluation",
+ "help": "The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated.",
+ "label": "Evaluation"
+ }
+ }
+ ]
+ },
+ {
+ "title": "In the case of cash transactions/occasional customers: Information on type and purpose of business relationship",
+ "description": "These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that no customer profile (VQF doc. No. 902.5) is created",
+ "fields": [
+ {
+ "type": "choiceStacked",
+ "properties": {
+ "name": "cashTransactions.typeOfBusiness",
+ "id": ".cashTransactions.typeOfBusiness",
+ "label": "Type of business relationship",
+ "choices": [
+ {
+ "label": "Money exchange",
+ "value": "money-exchange"
+ },
+ {
+ "label": "Money and asset transfer",
+ "value": "money-and-asset-transfer"
+ },
+ {
+ "label": "Other cash transactions. Specify below",
+ "value": "other"
+ }
+ ]
+ }
+ },
+ {
+ "type": "text",
+ "properties": {
+ "name": "cashTransactions.otherTypeOfBusiness",
+ "id": ".cashTransactions.otherTypeOfBusiness",
+ "required": true,
+ "label": "Specify other cash transactions:"
+ }
+ },
+ {
+ "type": "textArea",
+ "properties": {
+ "name": "cashTransactions.purpose",
+ "id": ".cashTransactions.purpose",
+ "label": "Purpose of the business relationship (purpose of service requested)"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ],
+ "not_yet_supported": []
+}
diff --git a/packages/aml-backoffice-ui/src/forms/index.ts b/packages/aml-backoffice-ui/src/forms/index.ts
index abeebfc6a..e89a8fb10 100644
--- a/packages/aml-backoffice-ui/src/forms/index.ts
+++ b/packages/aml-backoffice-ui/src/forms/index.ts
@@ -13,8 +13,7 @@
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 type { InternationalizationAPI } from "@gnu-taler/web-util/browser";
-import { FormMetadata } from "../context/ui-forms.js";
+import type { FormMetadata, InternationalizationAPI } from "@gnu-taler/web-util/browser";
import { v1 as simplest } from "./simplest.js";
const languages = (i18n: InternationalizationAPI) => [
diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts
index b52f2bf74..37ab0913d 100644
--- a/packages/aml-backoffice-ui/src/forms/simplest.ts
+++ b/packages/aml-backoffice-ui/src/forms/simplest.ts
@@ -15,9 +15,11 @@
*/
import type {
- InternationalizationAPI
+ DoubleColumnForm,
+ DoubleColumnFormSection,
+ InternationalizationAPI,
+ UIHandlerId
} from "@gnu-taler/web-util/browser";
-import { DoubleColumnForm, DoubleColumnFormSection, UIHandlerId } from "../context/ui-forms.js";
export const v1 = (i18n: InternationalizationAPI): DoubleColumnForm => ({
type: "double-column" as const,
@@ -30,7 +32,7 @@ export const v1 = (i18n: InternationalizationAPI): DoubleColumnForm => ({
properties: {
id: ".comment" as UIHandlerId,
name: "comment",
- label: i18n.str`Comments`,
+ label: i18n.str`Comment`,
},
},
],
diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts
index 752444bd2..033d1d950 100644
--- a/packages/aml-backoffice-ui/src/hooks/form.ts
+++ b/packages/aml-backoffice-ui/src/hooks/form.ts
@@ -19,10 +19,11 @@ import {
AmountJson,
TalerExchangeApi,
TranslatedString,
+ assertUnreachable,
} from "@gnu-taler/taler-util";
-import { UIFieldHandler } from "@gnu-taler/web-util/browser";
+import { Addon, InternationalizationAPI, UIFieldBaseDescription, UIFieldHandler, UIFormField, UIFormFieldBaseConfig, UIFormFieldConfig, UIHandlerId } from "@gnu-taler/web-util/browser";
import { useState } from "preact/hooks";
-import { UIFormFieldConfig, UIHandlerId } from "../context/ui-forms.js";
+import { getConverterById } from "../utils/converter.js";
// export type UIField = {
// value: string | undefined;
@@ -93,41 +94,17 @@ function constructFormHandler<T>(
updateForm(setValueDeeper(form, path, newValue));
}
- const currentValue: unknown = getValueDeeper(form, path)
- const currentError: unknown = errors === undefined ? undefined : getValueDeeper(errors, path)
+ const currentValue = getValueDeeper<string>(form as any, path, undefined)
+ const currentError = getValueDeeper<TranslatedString>(errors as any, path, undefined)
const field: UIFieldHandler = {
- // @ts-expect-error FIXME better typing
error: currentError,
- // @ts-expect-error FIXME better typing
value: currentValue,
onChange: updater,
- state: {},
+ state: {}, //FIXME: add the state of the field (hidden, )
};
return setValueDeeper(handleForm, path, field)
- /**
- * There is no clear way to know if this object is a custom field
- * or a group of fields
- */
- // if (typeof currentValue === "object") {
- // // @ts-expect-error FIXME better typing
- // const group = constructFormHandler(currentValue, updater, currentError);
- // // @ts-expect-error FIXME better typing
- // prev[fieldName] = group;
- // return prev;
- // }
-
- // const field: UIFieldHandler = {
- // // @ts-expect-error FIXME better typing
- // error: currentError,
- // // @ts-expect-error FIXME better typing
- // value: currentValue,
- // onChange: updater,
- // state: {},
- // };
- // handleForm[fieldName] = field;
- // return handleForm;
}, {} as FormHandler<T>);
return handler;
@@ -156,22 +133,40 @@ export function useFormState<T>(
return [handler, status];
}
+interface Tree<T> extends Record<string, Tree<T> | T> {}
-function getValueDeeper(
+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);
+}
+
+function getValueDeeper2(
object: Record<string, any>,
names: string[],
): UIFieldHandler {
if (names.length === 0) return object as UIFieldHandler;
const [head, ...rest] = names;
if (!head) {
- return getValueDeeper(object, rest);
+ return getValueDeeper2(object, rest);
}
if (object === undefined) {
throw Error("handler not found");
}
- return getValueDeeper(object[head], rest);
+ return getValueDeeper2(object[head], rest);
}
+
function setValueDeeper(object: any, names: string[], value: any): any {
if (names.length === 0) return value;
const [head, ...rest] = names;
@@ -183,3 +178,193 @@ function setValueDeeper(object: any, names: string[], value: any): any {
}
return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) };
}
+
+function getAddonById(_id: string | undefined): Addon {
+ return undefined!;
+}
+
+
+function converInputFieldsProps(
+ form: FormHandler<unknown>,
+ 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<unknown>,
+): 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);
+ }
+ }
+ });
+}
diff --git a/packages/aml-backoffice-ui/src/index.html b/packages/aml-backoffice-ui/src/index.html
index b7f73d0a2..0ed2f8178 100644
--- a/packages/aml-backoffice-ui/src/index.html
+++ b/packages/aml-backoffice-ui/src/index.html
@@ -30,7 +30,6 @@
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<title>Exchange Backoffice</title>
<!-- Entry point for the SPA. -->
- <script type="module" src="forms.js"></script>
<script type="module" src="index.js"></script>
<link rel="stylesheet" href="index.css" />
</head>
diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
index 11b6d053e..bb936cebf 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx
@@ -34,24 +34,26 @@ import {
import {
DefaultForm,
ErrorLoading,
+ FormMetadata,
InternationalizationAPI,
Loading,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { privatePages } from "../Routing.js";
-import { FormMetadata, useUiFormsContext } from "../context/ui-forms.js";
+import { useUiFormsContext } from "../context/ui-forms.js";
+import { preloadedForms } from "../forms/index.js";
import { useCaseDetails } from "../hooks/useCaseDetails.js";
import { ShowConsolidated } from "./ShowConsolidated.js";
-import { preloadedForms } from "../forms/index.js";
export type AmlEvent =
| AmlFormEvent
| AmlFormEventError
| KycCollectionEvent
| KycExpirationEvent;
+
type AmlFormEvent = {
type: "aml-form";
when: AbsoluteTime;
@@ -163,9 +165,9 @@ export function CaseDetails({ account }: { account: string }) {
const { i18n } = useTranslationContext();
const details = useCaseDetails(account);
- const {forms} = useUiFormsContext()
+ const { forms } = useUiFormsContext();
- const allForms = [...forms, ...preloadedForms(i18n)]
+ const allForms = [...forms, ...preloadedForms(i18n)];
if (!details) {
return <Loading />;
}
@@ -185,7 +187,12 @@ export function CaseDetails({ account }: { account: string }) {
}
const { aml_history, kyc_attributes } = details.body;
- const events = getEventsFromAmlHistory(aml_history, kyc_attributes, i18n, allForms);
+ const events = getEventsFromAmlHistory(
+ aml_history,
+ kyc_attributes,
+ i18n,
+ allForms,
+ );
if (showForm !== undefined) {
return (
diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
index bbfa65ca7..2f3ee054d 100644
--- a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx
@@ -23,35 +23,27 @@ import {
assertUnreachable,
} from "@gnu-taler/taler-util";
import {
- Addon,
Button,
+ FormMetadata,
InternationalizationAPI,
LocalNotificationBanner,
RenderAllFieldsByUiConfig,
- StringConverter,
- UIFieldHandler,
- UIFormField,
+ UIHandlerId,
useExchangeApiContext,
useLocalNotificationHandler,
- useTranslationContext,
+ useTranslationContext
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { privatePages } from "../Routing.js";
import {
- FormMetadata,
- UIFieldBaseDescription,
- UIFormFieldBaseConfig,
- UIFormFieldConfig,
- UIHandlerId,
- useUiFormsContext,
+ useUiFormsContext
} from "../context/ui-forms.js";
import { preloadedForms } from "../forms/index.js";
-import { FormErrors, FormHandler, useFormState } from "../hooks/form.js";
+import { FormErrors, convertUiField, useFormState } from "../hooks/form.js";
import { useOfficer } from "../hooks/officer.js";
import { Justification } from "./CaseDetails.js";
-import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
import { undefinedIfEmpty } from "./CreateAccount.js";
-import { getConverterById } from "../utils/converter.js";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
function searchForm(
i18n: InternationalizationAPI,
@@ -112,16 +104,18 @@ export function CaseUpdate({
theForm.config.design.forEach((section) => {
section.fields.forEach((field) => {
if ("id" in field.properties) {
+ //FIXME: this should be a validation
+ if (shape.indexOf(field.properties.id) !== -1) {
+ throw Error(`already present: ${field.properties.id}`)
+ }
shape.push(field.properties.id);
- // const path = field.properties.id.split(".");
- // defaultValue = setValueDeeper(defaultValue, path, undefined);
}
});
});
const [form, state] = useFormState<FormType>(shape, initial, (st) => {
const errors = undefinedIfEmpty<FormErrors<FormType>>({
- state: !st.state ? i18n.str`required` : undefined,
+ state: st.state === undefined ? i18n.str`required` : undefined,
threshold: !st.threshold ? i18n.str`required` : undefined,
when: !st.when ? i18n.str`required` : undefined,
comment: !st.comment ? i18n.str`required` : undefined,
@@ -140,8 +134,6 @@ export function CaseUpdate({
};
});
- console.log("NOW FORM", form);
-
const validatedForm = state.status !== "ok" ? undefined : state.result;
const submitHandler =
@@ -249,7 +241,6 @@ export function SelectForm({ account }: { account: string }) {
const { i18n } = useTranslationContext();
const { forms } = useUiFormsContext();
const pf = preloadedForms(i18n);
-
return (
<div>
<pre>New form for account: {account.substring(0, 16)}...</pre>
@@ -279,147 +270,3 @@ export function SelectForm({ account }: { account: string }) {
);
}
-function getValueDeeper(
- object: Record<string, any>,
- names: string[],
-): UIFieldHandler {
- if (names.length === 0) return object as UIFieldHandler;
- const [head, ...rest] = names;
- if (!head) {
- return getValueDeeper(object, rest);
- }
- if (object === undefined) {
- throw Error("handler not found");
- }
- return getValueDeeper(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<unknown>,
- p: UIFormFieldBaseConfig,
-) {
- return {
- converter: getConverterById(p.converterId),
- handler: getValueDeeper(form, p.id.split(".")),
- };
-}
-
-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}`,
- };
-}
-
-function convertUiField(
- i18n_: InternationalizationAPI,
- fieldConfig: UIFormFieldConfig[],
- form: FormHandler<unknown>,
-): 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 "absoluteTime": {
- return undefined!;
- }
- case "amount": {
- return {
- type: "amount",
- properties: {
- ...converBaseFieldsProps(i18n_, config.properties),
- ...converInputFieldsProps(form, config.properties),
- },
- } as UIFormField;
- }
- case "array": {
- return undefined!;
- }
- case "choiceHorizontal": {
- return {
- type: "choiceHorizontal",
- properties: {
- ...converBaseFieldsProps(i18n_, config.properties),
- ...converInputFieldsProps(form, config.properties),
- choices: config.properties.choices,
- },
- } as UIFormField;
- }
- case "choiceStacked":
- case "file":
- case "integer":
- case "selectMultiple":
- case "selectOne": {
- return undefined!;
- }
- 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 undefined!;
- }
- default: {
- assertUnreachable(config);
- }
- }
- });
-}
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx
index 3860bcd98..7b848487a 100644
--- a/packages/aml-backoffice-ui/src/pages/Cases.tsx
+++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx
@@ -24,6 +24,7 @@ import {
ErrorLoading,
InputChoiceHorizontal,
Loading,
+ UIHandlerId,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
@@ -34,7 +35,6 @@ import { privatePages } from "../Routing.js";
import { FormErrors, RecursivePartial, useFormState } from "../hooks/form.js";
import { undefinedIfEmpty } from "./CreateAccount.js";
import { Officer } from "./Officer.js";
-import { UIHandlerId } from "../context/ui-forms.js";
import { amlStateConverter } from "../utils/converter.js";
type FormType = {
diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
index 98160fb3a..f4904933b 100644
--- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx
@@ -18,6 +18,7 @@ import {
InputLine,
InternationalizationAPI,
LocalNotificationBanner,
+ UIHandlerId,
useLocalNotificationHandler,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
@@ -31,7 +32,6 @@ import {
} from "../hooks/form.js";
import { useOfficer } from "../hooks/officer.js";
import { usePreferences } from "../hooks/preferences.js";
-import { UIHandlerId } from "../context/ui-forms.js";
type FormType = {
password: string;
diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
index 0978d8bcc..3c0301e9f 100644
--- a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
+++ b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx
@@ -23,6 +23,8 @@ import {
DefaultForm,
FlexibleForm,
UIFormField,
+ UIFormFieldConfig,
+ UIHandlerId,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
@@ -40,22 +42,8 @@ export function ShowConsolidated({
const cons = getConsolidated(history, until);
- const form: FlexibleForm<Consolidated> = {
- behavior: (form) => {
- return {
- aml: {
- threshold: {
- hidden: !form.aml,
- },
- since: {
- hidden: !form.aml,
- },
- state: {
- hidden: !form.aml,
- },
- },
- };
- },
+ const form: FlexibleForm = {
+ type: "double-column",
design: [
{
title: i18n.str`AML`,
@@ -63,6 +51,8 @@ export function ShowConsolidated({
{
type: "amount",
properties: {
+ id: ".aml.threshold" as UIHandlerId,
+ currency: "NETZBON",
label: i18n.str`Threshold`,
name: "aml.threshold",
},
@@ -72,7 +62,7 @@ export function ShowConsolidated({
properties: {
label: i18n.str`State`,
name: "aml.state",
-
+ id: ".aml.state" as UIHandlerId,
choices: [
{
label: i18n.str`Frozen`,
@@ -95,10 +85,11 @@ export function ShowConsolidated({
? {
title: i18n.str`KYC`,
fields: Object.entries(cons.kyc).map(([key, field]) => {
- const result: UIFormField = {
+ const result: UIFormFieldConfig = {
type: "text",
properties: {
label: key as TranslatedString,
+ id: `kyc.${key}.value` as UIHandlerId,
name: `kyc.${key}.value`,
help: `${field.provider} since ${
field.since.t_ms === "never"
@@ -110,7 +101,7 @@ export function ShowConsolidated({
return result;
}),
}
- : undefined,
+ : undefined!,
],
};
return (
@@ -123,7 +114,7 @@ export function ShowConsolidated({
</h1>
<DefaultForm
key={`${String(Date.now())}`}
- form={form}
+ form={form as any}
initial={cons}
readOnly
onUpdate={() => {}}
diff --git a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
index b81e66468..084e639bf 100644
--- a/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
+++ b/packages/aml-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -17,6 +17,7 @@ import {
Button,
InputLine,
LocalNotificationBanner,
+ UIHandlerId,
useLocalNotificationHandler,
useTranslationContext
} from "@gnu-taler/web-util/browser";
@@ -24,7 +25,6 @@ import { VNode, h } from "preact";
import { FormErrors, useFormState } from "../hooks/form.js";
import { useOfficer } from "../hooks/officer.js";
import { undefinedIfEmpty } from "./CreateAccount.js";
-import { UIHandlerId } from "../context/ui-forms.js";
type FormType = {
password: string;
diff --git a/packages/aml-backoffice-ui/src/settings.json b/packages/aml-backoffice-ui/src/settings.json
new file mode 100644
index 000000000..932202b81
--- /dev/null
+++ b/packages/aml-backoffice-ui/src/settings.json
@@ -0,0 +1,4 @@
+{
+ "backendBaseURL": "http://exchange.taler.test:1180/",
+ "signupEmail": "do-not-contact-me@exchange.taler.test"
+} \ No newline at end of file
diff --git a/packages/aml-backoffice-ui/src/utils/converter.ts b/packages/aml-backoffice-ui/src/utils/converter.ts
index 25a824697..187a5412f 100644
--- a/packages/aml-backoffice-ui/src/utils/converter.ts
+++ b/packages/aml-backoffice-ui/src/utils/converter.ts
@@ -14,8 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson, Amounts, TalerExchangeApi } from "@gnu-taler/taler-util";
+import {
+ AbsoluteTime,
+ AmountJson,
+ Amounts,
+ TalerExchangeApi,
+} from "@gnu-taler/taler-util";
import { StringConverter } from "@gnu-taler/web-util/browser";
+import { format, parse } from "date-fns";
export const amlStateConverter = {
toStringUI: stringifyAmlState,
@@ -47,20 +53,63 @@ function parseAmlState(s: string | undefined): TalerExchangeApi.AmlState {
}
}
-const amountConverter: StringConverter<AmountJson> = {
- fromStringUI(v: string | undefined): AmountJson {
- // FIXME: requires currency
- return Amounts.parse(`NETZBON:${v}`) ?? Amounts.zeroOfCurrency("NETZBON");
- },
- toStringUI(v: unknown): string {
- return v === undefined ? "" : Amounts.stringifyValue(v as AmountJson);
- },
-};
+function amountConverter(config: any): StringConverter<AmountJson> {
+ const currency = config["currency"];
+ if (!currency || typeof currency !== "string") {
+ throw Error(`amount converter needs a currency`);
+ }
+ return {
+ fromStringUI(v: string | undefined): AmountJson {
+ // FIXME: requires currency
+ return Amounts.parse(`${currency}:${v}`) ?? Amounts.zeroOfCurrency(currency);
+ },
+ toStringUI(v: unknown): string {
+ return v === undefined ? "" : Amounts.stringifyValue(v as AmountJson);
+ },
+ };
+}
+
+function absTimeConverter(config: any): StringConverter<AbsoluteTime> {
+ const pattern = config["pattern"];
+ if (!pattern || typeof pattern !== "string") {
+ throw Error(`absTime converter needs a pattern`);
+ }
+ return {
+ fromStringUI(v: string | undefined): AbsoluteTime {
+ if (v === undefined) {
+ return AbsoluteTime.never();
+ }
+ try {
+ const time = parse(v, pattern, new Date());
+ return AbsoluteTime.fromMilliseconds(time.getTime());
+ } catch(e) {
+ return AbsoluteTime.never();
+ }
+ },
+ toStringUI(v: unknown): string {
+ if (v === undefined) return "";
+ const d = v as AbsoluteTime;
+ if (d.t_ms === "never") return "never";
+ try {
+ return format(d.t_ms, pattern)
+ } catch (e) {
+ return ""
+ }
+ },
+ };
+}
-export function getConverterById(id: string | undefined): StringConverter<unknown> {
+export function getConverterById(
+ id: string | undefined,
+ config: unknown,
+): StringConverter<unknown> {
+ if (id === "Taler.AbsoluteTime") {
+ // @ts-expect-error check this
+ return absTimeConverter(config);
+ }
if (id === "Taler.Amount") {
// @ts-expect-error check this
- return amountConverter;
+ return amountConverter(config);
}
if (id === "TalerExchangeApi.AmlState") {
// @ts-expect-error check this
diff --git a/packages/taler-util/src/codec.ts b/packages/taler-util/src/codec.ts
index 89db2de06..54d450d82 100644
--- a/packages/taler-util/src/codec.ts
+++ b/packages/taler-util/src/codec.ts
@@ -491,6 +491,19 @@ export function codecOptional<V>(innerCodec: Codec<V>): Codec<V | undefined> {
};
}
+export function codecForLazy<V>(innerCodec: () => Codec<V>): Codec<V> {
+ let instance: Codec<V> | undefined = undefined
+ return {
+ decode(x: any, c?: Context): V {
+ if (instance === undefined) {
+ instance = innerCodec()
+ }
+ return instance.decode(x, c);
+ },
+ };
+}
+
+
export type CodecType<T> = T extends Codec<infer X> ? X : any;
export function codecForEither<T extends Array<Codec<unknown>>>(
diff --git a/packages/web-util/src/forms/DefaultForm.tsx b/packages/web-util/src/forms/DefaultForm.tsx
index 1c635e089..338460170 100644
--- a/packages/web-util/src/forms/DefaultForm.tsx
+++ b/packages/web-util/src/forms/DefaultForm.tsx
@@ -2,14 +2,15 @@ import { Fragment, VNode, h } from "preact";
import { FormProvider, FormProviderProps, FormState } from "./FormProvider.js";
import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
import { TranslatedString } from "@gnu-taler/taler-util";
+// import { FlexibleForm } from "./ui-form.js";
/**
* Flexible form uses a DoubleColumForm for design
* and may have a dynamic properties defined by
* behavior function.
*/
-export interface FlexibleForm<T extends object> {
- design: DoubleColumnForm;
+export interface FlexibleForm_Deprecated<T extends object> {
+ design: DoubleColumnForm_Deprecated;
behavior?: (form: Partial<T>) => FormState<T>;
}
@@ -20,9 +21,9 @@ export interface FlexibleForm<T extends object> {
* have a description.
* Every sections contain a set of fields.
*/
-export type DoubleColumnForm = Array<DoubleColumnFormSection | undefined>;
+export type DoubleColumnForm_Deprecated = Array<DoubleColumnFormSection_Deprecated | undefined>;
-export type DoubleColumnFormSection = {
+export type DoubleColumnFormSection_Deprecated = {
title: TranslatedString;
description?: TranslatedString;
fields: UIFormField[];
@@ -39,14 +40,14 @@ export function DefaultForm<T extends object>({
onSubmit,
children,
readOnly,
-}: Omit<FormProviderProps<T>, "computeFormState"> & { form: FlexibleForm<T> }): VNode {
+}: Omit<FormProviderProps<T>, "computeFormState"> & { form: FlexibleForm_Deprecated<T> }): VNode {
return (
<FormProvider
initial={initial}
onUpdate={onUpdate}
onSubmit={onSubmit}
readOnly={readOnly}
- computeFormState={form.behavior}
+ // computeFormState={form.behavior}
>
<div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
{form.design.map((section, i) => {
diff --git a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
index 3887efe37..0d54c3f69 100644
--- a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
+++ b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
@@ -22,7 +22,7 @@
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,7 +43,7 @@ const initial: TargetObject = {
today: AbsoluteTime.now()
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
diff --git a/packages/web-util/src/forms/InputAmount.stories.tsx b/packages/web-util/src/forms/InputAmount.stories.tsx
index d5073ed86..f05887515 100644
--- a/packages/web-util/src/forms/InputAmount.stories.tsx
+++ b/packages/web-util/src/forms/InputAmount.stories.tsx
@@ -22,7 +22,7 @@
import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,7 +43,7 @@ const initial: TargetObject = {
amount: Amounts.parseOrThrow("USD:10")
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
diff --git a/packages/web-util/src/forms/InputArray.stories.tsx b/packages/web-util/src/forms/InputArray.stories.tsx
index eb61b04e3..143e73f02 100644
--- a/packages/web-util/src/forms/InputArray.stories.tsx
+++ b/packages/web-util/src/forms/InputArray.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -49,7 +49,7 @@ const initial: TargetObject = {
}]
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
diff --git a/packages/web-util/src/forms/InputArray.tsx b/packages/web-util/src/forms/InputArray.tsx
index ac4617c8c..1ac96437c 100644
--- a/packages/web-util/src/forms/InputArray.tsx
+++ b/packages/web-util/src/forms/InputArray.tsx
@@ -157,7 +157,8 @@ export function InputArray<T extends object, K extends keyof T>(
// elements should be present in the state object since this is expected to be an array
//@ts-ignore
- return state.elements[selectedIndex];
+ // return state.elements[selectedIndex];
+ return {}
}}
onSubmit={(v) => {
const newValue = [...list];
diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
index e1da89353..786dfe5bc 100644
--- a/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,7 +43,7 @@ const initial: TargetObject = {
comment: "0"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
diff --git a/packages/web-util/src/forms/InputChoiceStacked.stories.tsx b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
index 89f252e96..9a634d05c 100644
--- a/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
+++ b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,7 +43,7 @@ const initial: TargetObject = {
comment: "some initial comment"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
diff --git a/packages/web-util/src/forms/InputFile.stories.tsx b/packages/web-util/src/forms/InputFile.stories.tsx
index 9d9ad0bd7..eff18d071 100644
--- a/packages/web-util/src/forms/InputFile.stories.tsx
+++ b/packages/web-util/src/forms/InputFile.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,7 +43,7 @@ const initial: TargetObject = {
comment: "some initial comment"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
diff --git a/packages/web-util/src/forms/InputFile.tsx b/packages/web-util/src/forms/InputFile.tsx
index 6147eae59..cd0a96d1c 100644
--- a/packages/web-util/src/forms/InputFile.tsx
+++ b/packages/web-util/src/forms/InputFile.tsx
@@ -1,16 +1,14 @@
import { Fragment, VNode, h } from "preact";
import { UIFormProps } from "./FormProvider.js";
+import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
import { useField } from "./useField.js";
-import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
export function InputFile<T extends object, K extends keyof T>(
props: { maxBites: number; accept?: string } & UIFormProps<T, K>,
): VNode {
const {
- name,
label,
- placeholder,
tooltip,
required,
help: propsHelp,
@@ -26,6 +24,20 @@ export function InputFile<T extends object, K extends keyof T>(
if (state.hidden) {
return <div />;
}
+
+ const valueStr = !value ? "" : value.toString();
+ const firstColon = valueStr.indexOf(";");
+
+ const { fileName, dataUri } = valueStr.startsWith("file:")
+ ? {
+ fileName: valueStr.substring(5, firstColon),
+ dataUri: valueStr.substring(firstColon + 1),
+ }
+ : {
+ fileName: "",
+ dataUri: valueStr,
+ };
+
return (
<div class="col-span-full">
<LabelWithTooltipMaybeRequired
@@ -33,7 +45,7 @@ export function InputFile<T extends object, K extends keyof T>(
tooltip={tooltip}
required={required}
/>
- {!value || !(value as string).startsWith("data:image/") ? (
+ {!dataUri ? (
<div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 py-1">
<div class="text-center">
<svg
@@ -51,13 +63,12 @@ export function InputFile<T extends object, K extends keyof T>(
{!state.disabled && (
<div class="my-2 flex text-sm leading-6 text-gray-600">
<label
- for="file-upload"
+ for={String(props.name)}
class="relative cursor-pointer rounded-md bg-white font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500"
>
<span>Upload a file</span>
<input
- id="file-upload"
- name="file-upload"
+ id={String(props.name)}
type="file"
class="sr-only"
accept={accept}
@@ -69,6 +80,7 @@ export function InputFile<T extends object, K extends keyof T>(
if (f[0].size > maxBites) {
return onChange(undefined!);
}
+ const fileName = f[0].name;
return f[0].arrayBuffer().then((b) => {
const b64 = window.btoa(
new Uint8Array(b).reduce(
@@ -76,9 +88,15 @@ export function InputFile<T extends object, K extends keyof T>(
"",
),
);
- return onChange(
- `data:${f[0].type};base64,${b64}` as any,
- );
+ if (fileName) {
+ return onChange(
+ `file:${fileName};data:${f[0].type};base64,${b64}` as any,
+ );
+ } else {
+ return onChange(
+ `data:${f[0].type};base64,${b64}` as any,
+ );
+ }
});
}}
/>
@@ -90,10 +108,18 @@ export function InputFile<T extends object, K extends keyof T>(
</div>
) : (
<div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 relative">
- <img
- src={value as string}
- class=" h-24 w-full object-cover relative"
- />
+ {(dataUri as string).startsWith("data:image/") ? (
+ <img src={dataUri} class=" h-24 w-full object-cover relative" />
+ ) : (
+ <div />
+ )}
+ {fileName ? (
+ <div class="absolute rounded-lg border flex justify-center text-xl items-center text-white ">
+ {fileName}
+ </div>
+ ) : (
+ <Fragment />
+ )}
{!state.disabled && (
<div
diff --git a/packages/web-util/src/forms/InputInteger.stories.tsx b/packages/web-util/src/forms/InputInteger.stories.tsx
index 04a6d1049..378736a24 100644
--- a/packages/web-util/src/forms/InputInteger.stories.tsx
+++ b/packages/web-util/src/forms/InputInteger.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -38,7 +38,7 @@ const initial: TargetObject = {
age: 5,
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
diff --git a/packages/web-util/src/forms/InputLine.stories.tsx b/packages/web-util/src/forms/InputLine.stories.tsx
index 1c62a6164..dea5c142a 100644
--- a/packages/web-util/src/forms/InputLine.stories.tsx
+++ b/packages/web-util/src/forms/InputLine.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,7 +43,7 @@ const initial: TargetObject = {
comment: "some initial comment"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
diff --git a/packages/web-util/src/forms/InputSelectMultiple.stories.tsx b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
index b1649fecf..ab17545f5 100644
--- a/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
+++ b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -45,7 +45,7 @@ const initial: TargetObject = {
things: [],
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
diff --git a/packages/web-util/src/forms/InputSelectMultiple.tsx b/packages/web-util/src/forms/InputSelectMultiple.tsx
index 12e88fcc1..1bcf85061 100644
--- a/packages/web-util/src/forms/InputSelectMultiple.tsx
+++ b/packages/web-util/src/forms/InputSelectMultiple.tsx
@@ -13,7 +13,7 @@ export function InputSelectMultiple<T extends object, K extends keyof T>(
max?: number;
} & UIFormProps<T, K>,
): VNode {
- const { label, choices, placeholder, tooltip, required, unique, max } = props;
+ const { converter, label, choices, placeholder, tooltip, required, unique, max } = props;
//FIXME: remove deprecated
const fieldCtx = useField<T, K>(props.name);
const { value, onChange, state } =
diff --git a/packages/web-util/src/forms/InputSelectOne.stories.tsx b/packages/web-util/src/forms/InputSelectOne.stories.tsx
index f87aeda66..2ebde3096 100644
--- a/packages/web-util/src/forms/InputSelectOne.stories.tsx
+++ b/packages/web-util/src/forms/InputSelectOne.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,7 +43,7 @@ const initial: TargetObject = {
things: "one"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
diff --git a/packages/web-util/src/forms/InputText.stories.tsx b/packages/web-util/src/forms/InputText.stories.tsx
index 8eced7736..60b6ca224 100644
--- a/packages/web-util/src/forms/InputText.stories.tsx
+++ b/packages/web-util/src/forms/InputText.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,7 +43,7 @@ const initial: TargetObject = {
comment: "some initial comment"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
diff --git a/packages/web-util/src/forms/InputTextArea.stories.tsx b/packages/web-util/src/forms/InputTextArea.stories.tsx
index 6713548a8..ab1a695f5 100644
--- a/packages/web-util/src/forms/InputTextArea.stories.tsx
+++ b/packages/web-util/src/forms/InputTextArea.stories.tsx
@@ -23,7 +23,7 @@ import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
DefaultForm as TestedComponent,
- FlexibleForm,
+ FlexibleForm_Deprecated,
} from "./DefaultForm.js";
export default {
@@ -43,7 +43,7 @@ const initial: TargetObject = {
comment: "some initial comment"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
diff --git a/packages/web-util/src/forms/InputToggle.stories.tsx b/packages/web-util/src/forms/InputToggle.stories.tsx
index e10098718..fcc57ffe2 100644
--- a/packages/web-util/src/forms/InputToggle.stories.tsx
+++ b/packages/web-util/src/forms/InputToggle.stories.tsx
@@ -22,7 +22,7 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import * as tests from "@gnu-taler/web-util/testing";
import {
- FlexibleForm,
+ FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
@@ -43,7 +43,7 @@ const initial: TargetObject = {
comment: "some initial comment"
}
-const form: FlexibleForm<TargetObject> = {
+const form: FlexibleForm_Deprecated<TargetObject> = {
design: [{
title: "this is a simple form" as TranslatedString,
fields: [{
diff --git a/packages/web-util/src/forms/index.ts b/packages/web-util/src/forms/index.ts
index 4ff71f197..8c6c23ec5 100644
--- a/packages/web-util/src/forms/index.ts
+++ b/packages/web-util/src/forms/index.ts
@@ -19,5 +19,6 @@ export * from "./InputTextArea.js"
export * from "./InputToggle.js"
export * from "./TimePicker.js"
export * from "./forms.js"
+export * from "./ui-form.js"
export * from "./useField.js"
diff --git a/packages/web-util/src/forms/ui-form.ts b/packages/web-util/src/forms/ui-form.ts
new file mode 100644
index 000000000..9bbc2e96c
--- /dev/null
+++ b/packages/web-util/src/forms/ui-form.ts
@@ -0,0 +1,451 @@
+import {
+ buildCodecForObject,
+ buildCodecForUnion,
+ Codec,
+ codecForBoolean,
+ codecForConstString,
+ codecForLazy,
+ codecForList,
+ codecForNumber,
+ codecForString,
+ codecForTimestamp,
+ codecOptional,
+ Integer,
+ TalerProtocolTimestamp,
+} from "@gnu-taler/taler-util";
+
+export type FlexibleForm = DoubleColumnForm;
+
+export interface DoubleColumnForm {
+ type: "double-column";
+ design: Array<DoubleColumnFormSection>;
+ // behavior?: (form: Partial<T>) => FormState<T>;
+}
+
+export type DoubleColumnFormSection = {
+ title: string;
+ description?: string;
+ fields: UIFormFieldConfig[];
+};
+
+// export interface BaseForm {
+// state: TalerExchangeApi.AmlState;
+// threshold: AmountJson;
+// }
+
+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: UIFieldBaseDescription & {
+ fields: UIFormFieldConfig[];
+ };
+};
+
+type UIFormFieldConfigChoiseHorizontal = {
+ type: "choiceHorizontal";
+ properties: UIFormFieldBaseConfig & {
+ choices: Array<SelectUiChoice>;
+ };
+};
+
+type UIFormFieldConfigChoiseStacked = {
+ type: "choiceStacked";
+ properties: UIFormFieldBaseConfig & {
+ choices: Array<SelectUiChoice>;
+ };
+};
+
+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<SelectUiChoice>;
+ };
+};
+type UIFormFieldConfigSelectOne = {
+ type: "selectOne";
+ properties: UIFormFieldBaseConfig & {
+ choices: Array<SelectUiChoice>;
+ };
+};
+type UIFormFieldConfigText = {
+ type: "text";
+ properties: UIFormFieldBaseConfig;
+};
+type UIFormFieldConfigTextArea = {
+ type: "textArea";
+ properties: UIFormFieldBaseConfig;
+};
+type UIFormFieldConfigToggle = {
+ type: "toggle";
+ properties: UIFormFieldBaseConfig;
+};
+
+export 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;
+
+ /* name of the field, useful for a11y */
+ name: string;
+
+ /* if the field should be initialy hidden */
+ hidden?: boolean;
+ /* ui element to show before */
+ addonBeforeId?: string;
+ /* ui element to show after */
+ addonAfterId?: string;
+};
+
+export type UIFormFieldBaseConfig = UIFieldBaseDescription & {
+ /* example to be shown inside the field */
+ placeholder?: string;
+
+ /* show a mark as required */
+ required?: boolean;
+
+ /* readonly and dim */
+ disabled?: boolean;
+
+ /* 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<UIHandlerId>;
+
+const codecForUIFormFieldBaseDescriptionTemplate = <
+ T extends UIFieldBaseDescription,
+>() =>
+ buildCodecForObject<T>()
+ .property("addonAfterId", codecOptional(codecForString()))
+ .property("addonBeforeId", codecOptional(codecForString()))
+ .property("hidden", codecOptional(codecForBoolean()))
+ .property("help", codecOptional(codecForString()))
+ .property("label", codecForString())
+ .property("name", codecForString())
+ .property("tooltip", codecOptional(codecForString()));
+
+const codecForUIFormFieldBaseConfigTemplate = <
+ T extends UIFormFieldBaseConfig,
+>() =>
+ codecForUIFormFieldBaseDescriptionTemplate<T>()
+ .property("id", codecForUiFieldId())
+ .property("converterId", codecOptional(codecForString()))
+ .property("disabled", codecOptional(codecForBoolean()))
+ .property("required", codecOptional(codecForBoolean()))
+ .property("placeholder", codecOptional(codecForString()));
+
+const codecForUIFormFieldBaseConfig = (): Codec<UIFormFieldBaseConfig> =>
+ 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<UIFormFieldConfigAbsoluteTime> =>
+ buildCodecForObject<UIFormFieldConfigAbsoluteTime>()
+ .property("type", codecForConstString("absoluteTime"))
+ .property("properties", codecForUIFormFieldAbsoluteTimeConfig())
+ .build("UIFormFieldConfigAbsoluteTime");
+
+const codecForUIFormFieldAmountConfig = (): Codec<
+ UIFormFieldConfigAmount["properties"]
+> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigAmount["properties"]>()
+ .property("currency", codecForString())
+ .property("max", codecOptional(codecForNumber()))
+ .property("min", codecOptional(codecForNumber()))
+ .build("UIFormFieldConfigAmount.properties");
+
+const codecForUiFormFieldAmount = (): Codec<UIFormFieldConfigAmount> =>
+ buildCodecForObject<UIFormFieldConfigAmount>()
+ .property("type", codecForConstString("amount"))
+ .property("properties", codecForUIFormFieldAmountConfig())
+ .build("UIFormFieldConfigAmount");
+
+const codecForUIFormFieldArrayConfig = (): Codec<
+ UIFormFieldConfigArray["properties"]
+> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigArray["properties"]>()
+ .property("labelFieldId", codecForUiFieldId())
+ .property("fields", codecForList(codecForUiFormField()))
+ .build("UIFormFieldConfigArray.properties");
+
+const codecForUiFormFieldArray = (): Codec<UIFormFieldConfigArray> =>
+ buildCodecForObject<UIFormFieldConfigArray>()
+ .property("type", codecForConstString("array"))
+ .property("properties", codecForUIFormFieldArrayConfig())
+ .build("UIFormFieldConfigArray");
+
+const codecForUiFormFieldCaption = (): Codec<UIFormFieldConfigCaption> =>
+ buildCodecForObject<UIFormFieldConfigCaption>()
+ .property("type", codecForConstString("caption"))
+ .property("properties", codecForUIFormFieldBaseConfig())
+ .build("UIFormFieldConfigCaption");
+
+const codecForUiFormSelectUiChoice = (): Codec<SelectUiChoice> =>
+ buildCodecForObject<SelectUiChoice>()
+ .property("description", codecOptional(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<UIFormFieldConfigChoiseHorizontal> =>
+ buildCodecForObject<UIFormFieldConfigChoiseHorizontal>()
+ .property("type", codecForConstString("choiceHorizontal"))
+ .property("properties", codecForUIFormFieldWithChoiseConfig())
+ .build("UIFormFieldConfigChoiseHorizontal");
+
+const codecForUiFormFieldChoiceStacked =
+ (): Codec<UIFormFieldConfigChoiseStacked> =>
+ buildCodecForObject<UIFormFieldConfigChoiseStacked>()
+ .property("type", codecForConstString("choiceStacked"))
+ .property("properties", codecForUIFormFieldWithChoiseConfig())
+ .build("UIFormFieldConfigChoiseStacked");
+
+const codecForUIFormFieldFileConfig = (): Codec<
+ UIFormFieldConfigFile["properties"]
+> =>
+ codecForUIFormFieldBaseConfigTemplate<UIFormFieldConfigFile["properties"]>()
+ .property("accept", codecOptional(codecForString()))
+ .property("maxBytes", codecOptional(codecForNumber()))
+ .property("minBytes", codecOptional(codecForNumber()))
+ .build("UIFormFieldConfigFile.properties");
+
+const codecForUiFormFieldFile = (): Codec<UIFormFieldConfigFile> =>
+ buildCodecForObject<UIFormFieldConfigFile>()
+ .property("type", codecForConstString("file"))
+ .property("properties", codecForUIFormFieldFileConfig())
+ .build("UIFormFieldConfigFile");
+
+const codecForUIFormFieldWithFieldsConfig = (): Codec<
+ UIFormFieldConfigGroup["properties"]
+> =>
+ codecForUIFormFieldBaseDescriptionTemplate<
+ UIFormFieldConfigGroup["properties"]
+ >()
+ .property("fields", codecForList(codecForUiFormField()))
+ .build("UIFormFieldConfigGroup.properties");
+
+const codecForUiFormFieldGroup = (): Codec<UIFormFieldConfigGroup> =>
+ buildCodecForObject<UIFormFieldConfigGroup>()
+ .property("type", codecForConstString("group"))
+ .property("properties", codecForUIFormFieldWithFieldsConfig())
+ .build("UiFormFieldGroup");
+
+const codecForUiFormFieldInteger = (): Codec<UIFormFieldConfigInteger> =>
+ buildCodecForObject<UIFormFieldConfigInteger>()
+ .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<UIFormFieldConfigSelectMultiple> =>
+ buildCodecForObject<UIFormFieldConfigSelectMultiple>()
+ .property("type", codecForConstString("selectMultiple"))
+ .property("properties", codecForUIFormFieldSelectMultipleConfig())
+ .build("UiFormFieldSelectMultiple");
+
+const codecForUiFormFieldSelectOne = (): Codec<UIFormFieldConfigSelectOne> =>
+ buildCodecForObject<UIFormFieldConfigSelectOne>()
+ .property("type", codecForConstString("selectOne"))
+ .property("properties", codecForUIFormFieldWithChoiseConfig())
+ .build("UIFormFieldConfigSelectOne");
+
+const codecForUiFormFieldText = (): Codec<UIFormFieldConfigText> =>
+ buildCodecForObject<UIFormFieldConfigText>()
+ .property("type", codecForConstString("text"))
+ .property("properties", codecForUIFormFieldBaseConfig())
+ .build("UIFormFieldConfigText");
+
+const codecForUiFormFieldTextArea = (): Codec<UIFormFieldConfigTextArea> =>
+ buildCodecForObject<UIFormFieldConfigTextArea>()
+ .property("type", codecForConstString("textArea"))
+ .property("properties", codecForUIFormFieldBaseConfig())
+ .build("UIFormFieldConfigTextArea");
+
+const codecForUiFormFieldToggle = (): Codec<UIFormFieldConfigToggle> =>
+ buildCodecForObject<UIFormFieldConfigToggle>()
+ .property("type", codecForConstString("toggle"))
+ .property("properties", codecForUIFormFieldBaseConfig())
+ .build("UIFormFieldConfigToggle");
+
+const codecForUiFormField = (): Codec<UIFormFieldConfig> =>
+ buildCodecForUnion<UIFormFieldConfig>()
+ .discriminateOn("type")
+ .alternative("array", codecForLazy(codecForUiFormFieldArray))
+ .alternative("group", codecForLazy(codecForUiFormFieldGroup))
+ .alternative("absoluteTime", codecForUiFormFieldAbsoluteTime())
+ .alternative("amount", codecForUiFormFieldAmount())
+ .alternative("caption", codecForUiFormFieldCaption())
+ .alternative("choiceHorizontal", codecForUiFormFieldChoiceHorizontal())
+ .alternative("choiceStacked", codecForUiFormFieldChoiceStacked())
+ .alternative("file", codecForUiFormFieldFile())
+ .alternative("integer", codecForUiFormFieldInteger())
+ .alternative("selectMultiple", codecForUiFormFieldSelectMultiple())
+ .alternative("selectOne", codecForUiFormFieldSelectOne())
+ .alternative("text", codecForUiFormFieldText())
+ .alternative("textArea", codecForUiFormFieldTextArea())
+ .alternative("toggle", codecForUiFormFieldToggle())
+ .build("UIFormField");
+
+const codecForDoubleColumnFormSection = (): Codec<DoubleColumnFormSection> =>
+ buildCodecForObject<DoubleColumnFormSection>()
+ .property("title", codecForString())
+ .property("description", codecOptional(codecForString()))
+ .property("fields", codecForList(codecForUiFormField()))
+ .build("DoubleColumnFormSection");
+
+const codecForDoubleColumnForm = (): Codec<DoubleColumnForm> =>
+ buildCodecForObject<DoubleColumnForm>()
+ .property("type", codecForConstString("double-column"))
+ .property("design", codecForList(codecForDoubleColumnFormSection()))
+ .build("DoubleColumnForm");
+
+const codecForFlexibleForm = (): Codec<FlexibleForm> =>
+ buildCodecForUnion<FlexibleForm>()
+ .discriminateOn("type")
+ .alternative("double-column", codecForDoubleColumnForm())
+ .build<FlexibleForm>("FlexibleForm");
+
+const codecForFormMetadata = (): Codec<FormMetadata> =>
+ buildCodecForObject<FormMetadata>()
+ .property("label", codecForString())
+ .property("id", codecForString())
+ .property("version", codecForNumber())
+ .property("config", codecForFlexibleForm())
+ .build("FormMetadata");
+
+export const codecForUIForms = (): Codec<UiForms> =>
+ buildCodecForObject<UiForms>()
+ .property("forms", codecForList(codecForFormMetadata()))
+ .build("UiForms");
+
+export type FormMetadata = {
+ label: string;
+ id: string;
+ version: number;
+ config: FlexibleForm;
+};
+
+export interface UiForms {
+ // Where libeufin backend is localted
+ // default: window.origin without "webui/"
+ forms: Array<FormMetadata>;
+}