taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit ddd2e25bc39cba3769835fbf4f8c32ddf401151f
parent 0ddb81e6ddfef7b275dd0bfc28a74853ae5b7ade
Author: Florian Dold <dold@taler.net>
Date:   Mon, 31 Aug 2026 15:27:18 +0200

exchange web UIs: use compiled-in form definitions

Diffstat:
Mpackages/taler-exchange-aml-webui/build.mjs | 2+-
Mpackages/taler-exchange-aml-webui/dev.mjs | 2+-
Mpackages/taler-exchange-aml-webui/src/context/ui-forms.test.ts | 48++++++------------------------------------------
Mpackages/taler-exchange-aml-webui/src/context/ui-forms.ts | 79++++++++-----------------------------------------------------------------------
Dpackages/taler-exchange-aml-webui/src/forms.json | 3---
Mpackages/taler-exchange-kyc-webui/build.mjs | 12++++++------
Mpackages/taler-exchange-kyc-webui/dev.mjs | 12++++++------
Mpackages/taler-exchange-kyc-webui/src/app.tsx | 9++-------
Mpackages/taler-exchange-kyc-webui/src/context/ui-forms.ts | 42+++++++++---------------------------------
Dpackages/taler-exchange-kyc-webui/src/forms.json | 215-------------------------------------------------------------------------------
Mpackages/web-util/src/forms/forms-types.ts | 320-------------------------------------------------------------------------------
11 files changed, 39 insertions(+), 705 deletions(-)

diff --git a/packages/taler-exchange-aml-webui/build.mjs b/packages/taler-exchange-aml-webui/build.mjs @@ -22,7 +22,7 @@ await build({ importMeta: import.meta, source: { js: ["src/index.tsx"], - assets: [{ base: "src", files: ["src/index.html","src/forms.json"] }], + assets: [{ base: "src", files: ["src/index.html"] }], }, destination: "./dist/prod", css: "postcss", diff --git a/packages/taler-exchange-aml-webui/dev.mjs b/packages/taler-exchange-aml-webui/dev.mjs @@ -25,7 +25,7 @@ const build = initializeDev({ importMeta: import.meta, source: { js: devEntryPoints, - assets: [{ base: "src", files: ["src/index.html","src/forms.json","src/settings.json"] }], + assets: [{ base: "src", files: ["src/index.html", "src/settings.json"] }], }, destination: "./dist/dev", css: "postcss", diff --git a/packages/taler-exchange-aml-webui/src/context/ui-forms.test.ts b/packages/taler-exchange-aml-webui/src/context/ui-forms.test.ts @@ -8,52 +8,16 @@ */ import { i18n, setupI18n } from "@gnu-taler/taler-util"; -import { FormMetadata, preloadedForms } from "@gnu-taler/web-util/browser"; import assert from "node:assert/strict"; import test from "node:test"; -import { inheritBuiltinOfficerViews } from "./ui-forms.js"; +import { builtInUiForms } from "./ui-forms.js"; setupI18n("en", {}); -test("configured ToS forms inherit the built-in officer view", () => { - const builtins = preloadedForms(i18n); - const builtinTerms = builtins.find(({ id }) => id === "accept-tos"); +test("the AML UI exposes the compiled-in form registry", () => { + const { forms } = builtInUiForms(i18n); + const builtinTerms = forms.find(({ id }) => id === "accept-tos"); assert.ok(builtinTerms?.officerView); - - const configuredTerms: FormMetadata = { - id: "deployment-terms", - version: builtinTerms.version + 1, - label: "Deployment-specific terms", - description: "The form definition supplied through forms.json", - config: { - type: "accept-tos", - tosUrl: "https://exchange.example/terms", - tosVersion: "terms-v8", - providerName: "Example Exchange", - }, - }; - - const [result] = inheritBuiltinOfficerViews(builtins, [configuredTerms]); - - assert.equal(result.officerView, builtinTerms.officerView); - assert.equal(result.version, configuredTerms.version); - assert.equal(result.label, configuredTerms.label); - assert.equal(result.config, configuredTerms.config); -}); - -test("configured programmatic officer views take precedence", () => { - const CustomOfficerView = () => null; - const configured: FormMetadata = { - id: "accept-tos", - version: 1, - label: "Custom terms", - config: { type: "single-column", fields: [] }, - officerView: CustomOfficerView, - }; - - const [result] = inheritBuiltinOfficerViews(preloadedForms(i18n), [ - configured, - ]); - - assert.equal(result.officerView, CustomOfficerView); + assert.ok(forms.some(({ id }) => id === "generic_note")); + assert.equal(new Set(forms.map(({ id }) => id)).size, forms.length); }); diff --git a/packages/taler-exchange-aml-webui/src/context/ui-forms.ts b/packages/taler-exchange-aml-webui/src/context/ui-forms.ts @@ -15,23 +15,21 @@ */ import { - codecForUIForms, FormMetadata, preloadedForms, - UiForms, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { ComponentChildren, createContext, h, VNode } from "preact"; -import { useContext, useEffect, useState } from "preact/hooks"; +import { useContext } from "preact/hooks"; /** * * @author Sebastian Javier Marchano (sebasjm) */ -export type Type = UiForms; +export type Type = { forms: FormMetadata[] }; -const defaultForms: UiForms = { +const defaultForms: Type = { forms: [], }; const Context = createContext<Type>(defaultForms); @@ -40,32 +38,10 @@ export type BaseForm = Record<string, unknown>; export const useUiFormsContext = (): Type => useContext(Context); -/** - * JSON form definitions cannot contain programmatic officer views. Inherit - * those views from the corresponding built-in form while retaining every - * serializable property supplied by the deployment. - */ -export function inheritBuiltinOfficerViews( - builtins: FormMetadata[], - configured: FormMetadata[], -): FormMetadata[] { - const viewsByFormId = new Map( - builtins.flatMap((form) => - form.officerView ? [[form.id, form.officerView] as const] : [], - ), - ); - const viewsByFormType = new Map([ - ["accept-tos", viewsByFormId.get("accept-tos")], - ]); - return configured.map((form) => ({ - ...form, - officerView: - form.officerView ?? - viewsByFormId.get(form.id) ?? - (typeof form.config === "function" - ? undefined - : viewsByFormType.get(form.config.type)), - })); +export function builtInUiForms( + i18n: Parameters<typeof preloadedForms>[0], +): Type { + return { forms: preloadedForms(i18n) }; } export const UiFormsProvider = ({ @@ -74,48 +50,9 @@ export const UiFormsProvider = ({ children: ComponentChildren; }): VNode => { const { i18n } = useTranslationContext(); - const [forms, setForms] = useState<FormMetadata[]>(); - const pf = preloadedForms(i18n); - - useEffect(() => { - fetchUiForms((resp) => { - setForms(resp.forms); - }); - }, []); - - const value = - !forms || !forms.length - ? pf - : [...pf, ...inheritBuiltinOfficerViews(pf, forms)]; return h(Context.Provider, { - value: { forms: value }, + value: builtInUiForms(i18n), children, }); }; - -function removeUndefineField<T extends object>(obj: T): T { - const keys = Object.keys(obj) as Array<keyof T>; - return keys.reduce((prev, cur) => { - if (typeof prev[cur] === "undefined") { - delete prev[cur]; - } - return prev; - }, obj); -} - -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); - }); -} diff --git a/packages/taler-exchange-aml-webui/src/forms.json b/packages/taler-exchange-aml-webui/src/forms.json @@ -1,3 +0,0 @@ -{ - "forms": [] -} diff --git a/packages/taler-exchange-kyc-webui/build.mjs b/packages/taler-exchange-kyc-webui/build.mjs @@ -22,12 +22,12 @@ await build({ importMeta: import.meta, source: { js: ["src/index.tsx"], - assets: [{ - base: "src", - files: [ - "src/index.html","src/forms.json", - ] - }], + assets: [ + { + base: "src", + files: ["src/index.html"], + }, + ], }, destination: "./dist/prod", css: "postcss", diff --git a/packages/taler-exchange-kyc-webui/dev.mjs b/packages/taler-exchange-kyc-webui/dev.mjs @@ -25,12 +25,12 @@ const build = initializeDev({ importMeta: import.meta, source: { js: devEntryPoints, - assets: [{ - base: "src", - files: [ - "src/index.html","src/forms.json", "src/settings.json", - ] - }], + assets: [ + { + base: "src", + files: ["src/index.html", "src/settings.json"], + }, + ], }, destination: "./dist/dev", public: "/app", diff --git a/packages/taler-exchange-kyc-webui/src/app.tsx b/packages/taler-exchange-kyc-webui/src/app.tsx @@ -29,7 +29,6 @@ import { NotificationProvider, TalerWalletIntegrationBrowserProvider, TranslationProvider, - UiForms, } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; @@ -47,14 +46,10 @@ const WITH_LOCAL_STORAGE_CACHE = false; export function App(): VNode { const [settings, setSettings] = useState<KycUiSettings>(); - const [forms, setForms] = useState<UiForms>({ - forms: [], - }); useEffect(() => { fetchSettings(setSettings); - // fetchUiForms(setForms); }, []); - if (!settings || !forms) return <Loading />; + if (!settings) return <Loading />; const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL); return ( @@ -97,7 +92,7 @@ export function App(): VNode { > <TalerWalletIntegrationBrowserProvider> <BrowserHashNavigationProvider> - <UiFormsProvider value={forms}> + <UiFormsProvider> <NotifierProvider> <Routing /> </NotifierProvider> diff --git a/packages/taler-exchange-kyc-webui/src/context/ui-forms.ts b/packages/taler-exchange-kyc-webui/src/context/ui-forms.ts @@ -14,7 +14,11 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { codecForUIForms, UiForms } from "@gnu-taler/web-util/browser"; +import { + FormMetadata, + preloadedForms, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { ComponentChildren, createContext, h, VNode } from "preact"; import { useContext } from "preact/hooks"; @@ -23,9 +27,9 @@ import { useContext } from "preact/hooks"; * @author Sebastian Javier Marchano (sebasjm) */ -export type Type = UiForms; +export type Type = { forms: FormMetadata[] }; -const defaultForms: UiForms = { +const defaultForms: Type = { forms: [], }; @@ -37,40 +41,12 @@ export const useUiFormsContext = (): Type => useContext(Context); export const UiFormsProvider = ({ children, - value, }: { - value: UiForms; children: ComponentChildren; }): VNode => { + const { i18n } = useTranslationContext(); return h(Context.Provider, { - value, + value: { forms: preloadedForms(i18n) }, children, }); }; - -function removeUndefineField<T extends object>(obj: T): T { - const keys = Object.keys(obj) as Array<keyof T>; - 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); - }); - return; -} diff --git a/packages/taler-exchange-kyc-webui/src/forms.json b/packages/taler-exchange-kyc-webui/src/forms.json @@ -1,215 +0,0 @@ -{ - "forms": [ - { - "label": "VQF starting form", - "id": "vqf-type", - "version": 1, - "config": { - "type": "double-column", - "design": [ - { - "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": [ - { - "id": "LEGAL_ENTITY_TYPE", - "label": "The customer is", - "required": true, - "type": "choiceStacked", - "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": "an operation legal entity or partnership", - "value": "OPERATIONAL" - }, - { - "label": "a trust (incl. underlying companies)", - "value": "TRUST" - }, - { - "label": "a life insurance policy with separately managed accounts/securities accounts", - "value": "LIFEINSURANCE" - }, - { - "label": "all other cases", - "value": "OTHER" - } - ] - } - ] - } - ] - } - }, - { - "label": "VQF natural person form", - "id": "vqf-natural", - "version": 1, - "config": { - "type": "double-column", - "design": [ - { - "title": "Natural person form", - "fields": [ - { - "type": "choiceStacked", - "id": "LEGAL_ENTITY_TYPE", - "label": "The customer is", - "required": true, - "choices": [ - { - "label": "natural", - "value": "NATURAL" - } - ] - } - ] - } - ] - } - }, - { - "label": "VQF legal entity form", - "id": "vqf-operational", - "version": 1, - "config": { - "type": "double-column", - "design": [ - { - "title": "Legal entity form", - "fields": [ - { - "type": "choiceStacked", - "id": "LEGAL_ENTITY_TYPE", - "label": "The customer is", - "required": true, - "choices": [ - { - "label": "legal entity", - "value": "NATURAL" - } - ] - } - ] - } - ] - } - }, - { - "label": "VQF foundation form", - "id": "vqf-foundation", - "version": 1, - "config": { - "type": "double-column", - "design": [ - { - "title": "foundation form", - "fields": [ - { - "type": "choiceStacked", - "id": "LEGAL_ENTITY_TYPE", - "label": "The customer is", - "required": true, - "choices": [ - { - "label": "foundation", - "value": "NATURAL" - } - ] - } - ] - } - ] - } - }, - { - "label": "VQF trust form", - "id": "vqf-trust", - "version": 1, - "config": { - "type": "double-column", - "design": [ - { - "title": "Natural trust form", - "fields": [ - { - "type": "choiceStacked", - "id": "LEGAL_ENTITY_TYPE", - "label": "The customer is", - "required": true, - "choices": [ - { - "label": "trust", - "value": "NATURAL" - } - ] - } - ] - } - ] - } - }, - { - "label": "VQF insurance form", - "id": "vqf-insurance", - "version": 1, - "config": { - "type": "double-column", - "design": [ - { - "title": "insurance form", - "fields": [ - { - "type": "choiceStacked", - "id": "LEGAL_ENTITY_TYPE", - "label": "The customer is", - "required": true, - "choices": [ - { - "label": "insurance", - "value": "NATURAL" - } - ] - } - ] - } - ] - } - }, - { - "label": "VQF other form", - "id": "vqf-other", - "version": 1, - "config": { - "type": "double-column", - "design": [ - { - "title": "other form", - "fields": [ - { - "type": "choiceStacked", - "id": "LEGAL_ENTITY_TYPE", - "label": "The customer is", - "required": true, - "choices": [ - { - "label": "other", - "value": "NATURAL" - } - ] - } - ] - } - ] - } - } - ], - "not_yet_supported": [] -} diff --git a/packages/web-util/src/forms/forms-types.ts b/packages/web-util/src/forms/forms-types.ts @@ -15,24 +15,9 @@ */ import { - buildCodecForObject, - buildCodecForUnion, - Codec, - codecForAny, - codecForBoolean, - codecForConstString, - codecForLazy, - codecForList, - codecForNumber, - codecForString, - codecForStringURL, - codecForTimestamp, - codecOptional, - codecOptionalDefault, Integer, TalerProtocolTimestamp, TranslatedString, - codecForEither, } from "@gnu-taler/taler-util"; import type { FunctionComponent } from "preact"; @@ -359,307 +344,6 @@ export interface FileFieldData { export type UIHandlerId = string; -// FIXME: validate well formed ui field id -const codecForUiFieldId = codecForString as () => Codec<UIHandlerId>; - -const codecForUIFormFieldBaseDescriptionTemplate = < - T extends UIFieldElementDescription, ->() => - buildCodecForObject<T>() - .property("hidden", codecOptional(codecForBoolean())) - .property("disabled", codecOptional(codecForBoolean())) - .property("required", codecOptional(codecForBoolean())) - .property("help", codecOptional(codecForString())) - .property("label", codecForString()) - .property("technicalName", codecOptional(codecForString())) - .property("tooltip", codecOptional(codecForString())); - -const codecForUIFormFieldBaseConfigTemplate = < - T extends UIFormFieldBaseConfig, ->() => - codecForUIFormFieldBaseDescriptionTemplate<T>() - .property("id", codecForUiFieldId()) - .property("placeholder", codecOptional(codecForString())); - -const codecForUiFormFieldAbsoluteTime = (): Codec<UIFormFieldAbsoluteTime> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldAbsoluteTime>() - .property("type", codecForConstString("absoluteTimeText")) - .property("pattern", codecForString()) - .property("max", codecOptional(codecForTimestamp)) - .property("min", codecOptional(codecForTimestamp)) - .build("UIFormFieldAbsoluteTime"); - -const codecForUiFormFieldIsoDate = (): Codec<UIFormFieldIsoDate> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldIsoDate>() - .property("type", codecForConstString("isoDateText")) - .property("pattern", codecForString()) - .property("defaultValue", codecOptional(codecForString())) - .property("defaultCalendarValue", codecOptional(codecForString())) - .property("max", codecOptional(codecForTimestamp)) - .property("min", codecOptional(codecForTimestamp)) - .build("UIFormFieldIsoTime"); - -const codecForUiFormFieldAmount = (): Codec<UIFormFieldAmount> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldAmount>() - .property("type", codecForConstString("amount")) - .property("currency", codecForString()) - .property("max", codecOptional(codecForNumber())) - .property("min", codecOptional(codecForNumber())) - .build("UIFormFieldAmount"); - -const codecForUiFormFieldArray = (): Codec<UIFormFieldArray> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldArray>() - .property("type", codecForConstString("array")) - .property("labelFieldId", codecForUiFieldId()) - .property("tooltip", codecOptional(codecForString())) - .property("fields", codecForList(codecForUiFormField())) - .build("UIFormFieldArray"); - -const codecForUiFormFieldCaption = (): Codec<UIFormElementCaption> => - codecForUIFormFieldBaseDescriptionTemplate<UIFormElementCaption>() - .property("type", codecForConstString("caption")) - .build("UIFormFieldCaption"); - -const codecForUIFormElementLink = (): Codec<UIFormElementDownloadLink> => - codecForUIFormFieldBaseConfigTemplate<UIFormElementDownloadLink>() - .property("type", codecForConstString("download-link")) - .property("url", codecForString()) - .property("media", codecOptional(codecForString())) - .property("fileName", codecOptional(codecForString())) - .build("UIFormElementLink"); - -const codecForUIFormElementExternalLink = - (): Codec<UIFormElementExternalLink> => - codecForUIFormFieldBaseConfigTemplate<UIFormElementExternalLink>() - .property("type", codecForConstString("external-link")) - .property("url", codecForString()) - .property("media", codecOptional(codecForString())) - .build("UIFormElementExternalLink"); - -const codecForUiFormFieldHtmlIFrame = (): Codec<UIFormElementHtmlIframe> => - codecForUIFormFieldBaseDescriptionTemplate<UIFormElementHtmlIframe>() - .property("type", codecForConstString("htmlIframe")) - .property("url", codecForStringURL()) - .build("codecForUiFormFieldHtmlIFrame"); - -const codecForUiFormSelectUiChoice = (): Codec<SelectUiChoice> => - buildCodecForObject<SelectUiChoice>() - .property("description", codecOptional(codecForString())) - .property("disabled", codecOptional(codecForBoolean())) - .property("disabledReason", codecOptional(codecForString())) - .property("exclusiveGroup", codecOptional(codecForString())) - .property("label", codecForString()) - .property("value", codecForEither(codecForString(), codecForBoolean())) - .build("SelectUiChoice"); - -const codecForUiFormFieldChoiceHorizontal = - (): Codec<UIFormFieldChoiceHorizontal> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldChoiceHorizontal>() - .property("type", codecForConstString("choiceHorizontal")) - .property("choices", codecForList(codecForUiFormSelectUiChoice())) - .build("UIFormFieldChoiseHorizontal"); - -const codecForUiFormFieldDrilldown = (): Codec<UIFormFieldDrilldown> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldDrilldown>() - .property("type", codecForConstString("drilldown")) - // Declared `any` on this variant, so nothing stricter to validate. - .property("choices", codecForAny()) - .build("UIFormFieldDrilldown"); - -const codecForUiFormFieldChoiceStacked = (): Codec<UIFormFieldChoiceStacked> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldChoiceStacked>() - .property("type", codecForConstString("choiceStacked")) - .property("choices", codecForList(codecForUiFormSelectUiChoice())) - .build("UIFormFieldChoiseStacked"); - -const codecForUiFormFieldFile = (): Codec<UIFormFieldFile> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldFile>() - .property("type", codecForConstString("file")) - .property("accept", codecOptional(codecForString())) - .property("maxBytes", codecOptional(codecForNumber())) - .build("UIFormFieldFile"); - -const codecForUiFormFieldGroup = (): Codec<UIFormElementGroup> => - codecForUIFormFieldBaseDescriptionTemplate<UIFormElementGroup>() - .property("type", codecForConstString("group")) - .property("fields", codecForList(codecForUiFormField())) - .build("UiFormFieldGroup"); - -const codecForUiFormVoid = (): Codec<UIFormVoid> => - codecForUIFormFieldBaseConfigTemplate<UIFormVoid>() - .property("type", codecForConstString("void")) - .build("UIFormVoid"); - -const codecForUIFormFieldPhone = (): Codec<UIFormFieldPhone> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldPhone>() - .property("type", codecForConstString("phone")) - .build("UIFormFieldPhone"); - -const codecForUiFormFieldInteger = (): Codec<UIFormFieldInteger> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldInteger>() - .property("type", codecForConstString("integer")) - // .property("properties", codecForUIFormFieldBaseConfig()) - .property("max", codecOptional(codecForNumber())) - .property("min", codecOptional(codecForNumber())) - .build("UIFormFieldInteger"); - -const codecForUiFormFieldSecret = (): Codec<UIFormFieldSecret> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldSecret>() - .property("type", codecForConstString("secret")) - .build("UIFormFieldSecret"); - -const codecForUiFormFieldDuration = (): Codec<UIFormFieldDuration> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldDuration>() - .property("type", codecForConstString("duration")) - .build("UiFormFieldDuration"); - -const codecForUiFormFieldDurationText = (): Codec<UIFormFieldDurationText> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldDurationText>() - .property("type", codecForConstString("durationText")) - .build("UiFormFieldDuration"); - -const codecForUiFormFieldSelectMultiple = - (): Codec<UIFormFieldSelectMultiple> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldSelectMultiple>() - .property("type", codecForConstString("selectMultiple")) - .property("max", codecOptional(codecForNumber())) - .property("min", codecOptional(codecForNumber())) - .property("unique", codecOptional(codecForBoolean())) - .property("choices", codecForList(codecForUiFormSelectUiChoice())) - .build("UiFormFieldSelectMultiple"); - -const codecForUiFormFieldSelectOne = (): Codec<UIFormFieldSelectOne> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldSelectOne>() - .property("type", codecForConstString("selectOne")) - .property("choices", codecForList(codecForUiFormSelectUiChoice())) - .property( - "preferredChoiceVals", - codecOptional(codecForList(codecForString())), - ) - .build("UIFormFieldSelectOne"); - -const codecForUiFormFieldText = (): Codec<UIFormFieldText> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldText>() - .property("type", codecForConstString("text")) - .build("UIFormFieldText"); - -const codecForUiFormFieldTextArea = (): Codec<UIFormFieldTextArea> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldTextArea>() - .property("type", codecForConstString("textArea")) - .build("UIFormFieldTextArea"); - -const codecForUiFormFieldToggle = (): Codec<UIFormFieldToggle> => - codecForUIFormFieldBaseConfigTemplate<UIFormFieldToggle>() - .property( - "threeState", - // Safe for an optional field: this never yields undefined. Codec<V> - // describes output, not which inputs are tolerated. - codecOptionalDefault(codecForBoolean(), false) as unknown as Codec< - boolean | undefined - >, - ) - .property("falseValue", codecForAny()) - .property("trueValue", codecForAny()) - .property( - "onlyTrueValue", - // Supplying a default is safe for an optional field: this never - // yields undefined. Codec<V> describes a codec's output, not which - // inputs it tolerates, so invariance cannot see that. - codecOptionalDefault(codecForBoolean(), false) as unknown as Codec< - boolean | undefined - >, - ) - .property("type", codecForConstString("toggle")) - .build("UIFormFieldToggle"); - -const codecForUiFormField = (): Codec<UIFormElementConfig> => - buildCodecForUnion<UIFormElementConfig>() - .discriminateOn("type") - .alternative("array", codecForLazy(codecForUiFormFieldArray)) - .alternative("group", codecForLazy(codecForUiFormFieldGroup)) - .alternative("void", codecForUiFormVoid()) - .alternative("download-link", codecForUIFormElementLink()) - .alternative("external-link", codecForUIFormElementExternalLink()) - .alternative("absoluteTimeText", codecForUiFormFieldAbsoluteTime()) - .alternative("isoDateText", codecForUiFormFieldIsoDate()) - .alternative("amount", codecForUiFormFieldAmount()) - .alternative("caption", codecForUiFormFieldCaption()) - .alternative("htmlIframe", codecForUiFormFieldHtmlIFrame()) - .alternative("choiceHorizontal", codecForUiFormFieldChoiceHorizontal()) - .alternative("drilldown", codecForUiFormFieldDrilldown()) - .alternative("choiceStacked", codecForUiFormFieldChoiceStacked()) - .alternative("file", codecForUiFormFieldFile()) - .alternative("integer", codecForUiFormFieldInteger()) - .alternative("phone", codecForUIFormFieldPhone()) - .alternative("secret", codecForUiFormFieldSecret()) - .alternative("selectMultiple", codecForUiFormFieldSelectMultiple()) - .alternative("duration", codecForUiFormFieldDuration()) - .alternative("durationText", codecForUiFormFieldDurationText()) - .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 codecForDoubleColumnFormDesign = (): Codec<DoubleColumnFormDesign> => - buildCodecForObject<DoubleColumnFormDesign>() - .property("type", codecForConstString("double-column")) - .property("title", codecOptional(codecForString())) - .property("sections", codecForList(codecForDoubleColumnFormSection())) - .build("DoubleColumnFormDesign"); - -const codecForSingleColumnFormDesign = (): Codec<SingleColumnFormDesign> => - buildCodecForObject<SingleColumnFormDesign>() - .property("type", codecForConstString("single-column")) - .property("fields", codecForList(codecForUiFormField())) - .build("SingleColumnFormDesign"); - -const codecForAcceptTosFormDesign = (): Codec<AcceptTosFormDesign> => - buildCodecForObject<AcceptTosFormDesign>() - .property("type", codecForConstString("accept-tos")) - .property("tosUrl", codecForStringURL()) - .property("tosVersion", codecForString()) - .property("providerName", codecOptional(codecForString())) - .property("linkOnly", codecOptional(codecForBoolean())) - .build("AcceptTosFormDesign"); - -const codecForFormDesign = (): Codec<FormDesign> => - buildCodecForUnion<FormDesign>() - .discriminateOn("type") - .alternative("double-column", codecForDoubleColumnFormDesign()) - .alternative("single-column", codecForSingleColumnFormDesign()) - .alternative("accept-tos", codecForAcceptTosFormDesign()) - .build<FormDesign>("FormDesign"); - -const codecForFormMetadata = (): Codec<FormMetadata> => - buildCodecForObject<FormMetadata>() - .property("label", codecForString()) - .property("description", codecOptional(codecForString())) - .property("id", codecForString()) - .property("version", codecForNumber()) - .property( - "config", - // A FormDesign may also be given as a function, which JSON cannot - // represent, so decoding only yields the data form. - codecForFormDesign() as unknown as Codec< - FormDesign | ((context: any) => FormDesign) - >, - ) - .property("contextForm", codecOptional(codecForFormDesign())) - .build("FormMetadata"); - -export const codecForUIForms = (): Codec<UiForms> => - buildCodecForObject<UiForms>() - .property("forms", codecForList(codecForFormMetadata())) - .build("UiForms"); - export type FormMetadata = { label: string; description?: string; @@ -677,7 +361,3 @@ export type FormOfficerViewProps = { context: unknown; data: Readonly<Record<string, unknown>>; }; - -export interface UiForms { - forms: Array<FormMetadata>; -}