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:
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>;
-}