diff options
Diffstat (limited to 'packages')
-rwxr-xr-x | packages/aml-backoffice-ui/build.mjs | 4 | ||||
-rwxr-xr-x | packages/aml-backoffice-ui/dev.mjs | 4 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/App.tsx | 5 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/forms.ts | 24 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/forms/index.ts | 4 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/forms/simplest.ts | 4 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/hooks/form.ts | 12 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CaseDetails.tsx | 5 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx | 169 | ||||
-rw-r--r-- | packages/web-util/src/forms/FormProvider.tsx | 4 |
10 files changed, 180 insertions, 55 deletions
diff --git a/packages/aml-backoffice-ui/build.mjs b/packages/aml-backoffice-ui/build.mjs index bd7a088cf..04a6f646b 100755 --- a/packages/aml-backoffice-ui/build.mjs +++ b/packages/aml-backoffice-ui/build.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -20,7 +20,7 @@ import { build } from "@gnu-taler/web-util/build"; await build({ type: "production", source: { - js: ["src/index.tsx", "src/forms.ts"], + js: ["src/index.tsx"], assets: [{ base: "src", files: ["src/index.html"] }], }, destination: "./dist/prod", diff --git a/packages/aml-backoffice-ui/dev.mjs b/packages/aml-backoffice-ui/dev.mjs index bc6fcd6c1..4b58116d1 100755 --- a/packages/aml-backoffice-ui/dev.mjs +++ b/packages/aml-backoffice-ui/dev.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -18,7 +18,7 @@ import { serve } from "@gnu-taler/web-util/node"; import { initializeDev } from "@gnu-taler/web-util/build"; -const devEntryPoints = ["src/stories.tsx", "src/index.tsx", "src/forms.ts"]; +const devEntryPoints = ["src/stories.tsx", "src/index.tsx"]; const build = initializeDev({ type: "development", diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx index 9ccf21755..ae8b574b6 100644 --- a/packages/aml-backoffice-ui/src/App.tsx +++ b/packages/aml-backoffice-ui/src/App.tsx @@ -29,15 +29,18 @@ 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"; const WITH_LOCAL_STORAGE_CACHE = false; export function App(): VNode { const [settings, setSettings] = useState<UiSettings>(); + const [forms, setForms] = useState<UiForms>(); useEffect(() => { fetchUiSettings(setSettings); + fetchUiForms(setForms); }, []); - if (!settings) return <Loading />; + if (!settings || !forms) return <Loading />; const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL); return ( diff --git a/packages/aml-backoffice-ui/src/forms.ts b/packages/aml-backoffice-ui/src/forms.ts deleted file mode 100644 index 3ecec2bb0..000000000 --- a/packages/aml-backoffice-ui/src/forms.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022-2024 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -export * from "./forms/index.js"; - -/** - * this file is here to have a flat dist folder - * - * this file is being build in a bundle separated - * from the main one. - */ diff --git a/packages/aml-backoffice-ui/src/forms/index.ts b/packages/aml-backoffice-ui/src/forms/index.ts index b32978c29..abeebfc6a 100644 --- a/packages/aml-backoffice-ui/src/forms/index.ts +++ b/packages/aml-backoffice-ui/src/forms/index.ts @@ -129,10 +129,10 @@ const languages = (i18n: InternationalizationAPI) => [ ]; -const forms: (i18n: InternationalizationAPI) => Array<FormMetadata> = (i18n) => [ +export const preloadedForms: (i18n: InternationalizationAPI) => Array<FormMetadata> = (i18n) => [ { label: i18n.str`Simple comment`, - id: "simple_comment", + id: "__simple_comment", version: 1, config: simplest(i18n), // }, { diff --git a/packages/aml-backoffice-ui/src/forms/simplest.ts b/packages/aml-backoffice-ui/src/forms/simplest.ts index d32c759cb..c7ba95462 100644 --- a/packages/aml-backoffice-ui/src/forms/simplest.ts +++ b/packages/aml-backoffice-ui/src/forms/simplest.ts @@ -17,7 +17,7 @@ import type { InternationalizationAPI } from "@gnu-taler/web-util/browser"; -import { BaseForm, DoubleColumnForm, DoubleColumnFormSection, UIHandlerId } from "../context/ui-forms.js"; +import { DoubleColumnForm, DoubleColumnFormSection, UIHandlerId } from "../context/ui-forms.js"; export const v1 = (i18n: InternationalizationAPI): DoubleColumnForm => ({ type: "double-column" as const, @@ -84,7 +84,7 @@ export function resolutionSection( type: "amount", properties: { id: ".threshold" as UIHandlerId, - currency: "USD", + currency: "NETZBON", name: "threshold", label: i18n.str`New threshold`, }, diff --git a/packages/aml-backoffice-ui/src/hooks/form.ts b/packages/aml-backoffice-ui/src/hooks/form.ts index e14e29819..edeae6085 100644 --- a/packages/aml-backoffice-ui/src/hooks/form.ts +++ b/packages/aml-backoffice-ui/src/hooks/form.ts @@ -19,8 +19,8 @@ import { TalerExchangeApi, TranslatedString, } from "@gnu-taler/taler-util"; +import { UIFieldHandler } from "@gnu-taler/web-util/browser"; import { useState } from "preact/hooks"; -import { UIField } from "@gnu-taler/web-util/browser"; // export type UIField = { // value: string | undefined; @@ -28,13 +28,13 @@ import { UIField } from "@gnu-taler/web-util/browser"; // error: TranslatedString | undefined; // }; -type FormHandler<T> = { +export type FormHandler<T> = { [k in keyof T]?: T[k] extends string - ? UIField + ? UIFieldHandler : T[k] extends AmountJson - ? UIField + ? UIFieldHandler : T[k] extends TalerExchangeApi.AmlState - ? UIField + ? UIFieldHandler : FormHandler<T[k]>; }; @@ -100,7 +100,7 @@ function constructFormHandler<T>( return prev; } - const field: UIField = { + const field: UIFieldHandler = { // @ts-expect-error FIXME better typing error: currentError, // @ts-expect-error FIXME better typing diff --git a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx index 1ad8c9453..62c54d222 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseDetails.tsx @@ -34,18 +34,17 @@ import { import { DefaultForm, ErrorLoading, - FlexibleForm, 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 { useCaseDetails } from "../hooks/useCaseDetails.js"; import { ShowConsolidated } from "./ShowConsolidated.js"; -import { FormMetadata, useUiFormsContext } from "../context/ui-forms.js"; export type AmlEvent = | AmlFormEvent diff --git a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx index 64bfb90f1..f04d404d0 100644 --- a/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx +++ b/packages/aml-backoffice-ui/src/pages/CaseUpdate.tsx @@ -20,12 +20,14 @@ import { HttpStatusCode, TalerExchangeApi, TalerProtocolTimestamp, - assertUnreachable, + assertUnreachable } from "@gnu-taler/taler-util"; import { Button, + InternationalizationAPI, LocalNotificationBanner, RenderAllFieldsByUiConfig, + UIFieldHandler, UIFormField, useExchangeApiContext, useLocalNotificationHandler, @@ -33,12 +35,40 @@ import { } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { privatePages } from "../Routing.js"; -import { UIFormFieldConfig, useUiFormsContext } from "../context/ui-forms.js"; -import { useFormState } from "../hooks/form.js"; +import { + FormMetadata, + UIFormFieldConfig, + useUiFormsContext, +} from "../context/ui-forms.js"; +import { preloadedForms } from "../forms/index.js"; +import { FormHandler, useFormState } from "../hooks/form.js"; import { useOfficer } from "../hooks/officer.js"; import { Justification } from "./CaseDetails.js"; import { HandleAccountNotReady } from "./HandleAccountNotReady.js"; +function searchForm( + i18n: InternationalizationAPI, + forms: FormMetadata[], + formId: string, +): FormMetadata | undefined { + { + const found = forms.find((v) => v.id === formId); + if (found) return found; + } + { + const pf = preloadedForms(i18n); + const found = pf.find((v) => v.id === formId); + if (found) return found; + } + return undefined; +} + +type FormType = { + when: AbsoluteTime; + state: TalerExchangeApi.AmlState; + threshold: AmountJson; +}; + export function CaseUpdate({ account, type: formId, @@ -52,11 +82,10 @@ export function CaseUpdate({ lib: { exchange: api }, } = useExchangeApiContext(); - // const [notification, notify, handleError] = useLocalNotification(); const [notification, withErrorHandler] = useLocalNotificationHandler(); const { config } = useExchangeApiContext(); const { forms } = useUiFormsContext(); - const initial = { + const initial: FormType = { when: AbsoluteTime.now(), state: TalerExchangeApi.AmlState.pending, threshold: Amounts.zeroOfCurrency(config.currency), @@ -65,19 +94,31 @@ export function CaseUpdate({ if (officer.state !== "ready") { return <HandleAccountNotReady officer={officer} />; } - const theForm = forms.find((v) => v.id === formId); + const theForm = searchForm(i18n, forms, formId); if (!theForm) { return <div>form with id {formId} not found</div>; } - const [form, state] = useFormState(initial, (st) => { + let defaultValue = initial + theForm.config.design.forEach((section) => { + section.fields.forEach((field) => { + if ("id" in field.properties) { + const path = field.properties.id.split("."); + defaultValue = setValueDeeper(defaultValue, path, undefined); + } + }); + }); + + + const [form, state] = useFormState<FormType>(defaultValue, (st) => { return { status: "ok", result: st as any, errors: undefined, }; }); - + + console.log("FORM", form) const validatedForm = state.status === "fail" ? undefined : state.result; const submitHandler = @@ -125,8 +166,73 @@ export function CaseUpdate({ }, ); - function convertUiField(_f: UIFormFieldConfig[]): UIFormField[] { - return []; + function convertUiField( + fieldConfig: UIFormFieldConfig[], + form: FormHandler<unknown>, + ): UIFormField[] { + return fieldConfig.map((config) => { + switch (config.type) { + case "absoluteTime": { + return undefined!; + } + case "amount": { + return { + type: "amount", + properties: { + ...(config.properties as any), + handler: getValueDeeper(form, config.properties.id.split(".")), + }, + } as UIFormField; + } + case "array": { + return undefined!; + } + case "caption": { + return undefined!; + } + case "choiceHorizontal": { + return { + type: "choiceHorizontal", + properties: { + handler: getValueDeeper(form, config.properties.id.split(".")), + choices: config.properties.choices, + }, + } as UIFormField; + } + case "choiceStacked": + case "file": + case "group": + case "integer": + case "selectMultiple": + case "selectOne": { + return undefined!; + } + case "text": { + return { + type: "text", + properties: { + ...(config.properties as any), + handler: getValueDeeper(form, config.properties.id.split(".")), + }, + } as UIFormField; + } + case "textArea": { + return { + type: "text", + properties: { + ...(config.properties as any), + handler: getValueDeeper(form, config.properties.id.split(".")), + }, + } as UIFormField; + } + case "toggle": { + return undefined!; + } + default: { + assertUnreachable(config); + } + } + }); } return ( @@ -155,7 +261,7 @@ export function CaseUpdate({ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> <RenderAllFieldsByUiConfig key={i} - fields={convertUiField(section.fields)} + fields={convertUiField(section.fields, form)} /> </div> </div> @@ -185,7 +291,10 @@ export function CaseUpdate({ } 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> @@ -200,6 +309,44 @@ export function SelectForm({ account }: { account: string }) { </a> ); })} + {pf.map((form) => { + return ( + <a + key={form.id} + href={privatePages.caseUpdate.url({ cid: account, type: form.id })} + class="m-4 block rounded-md w-fit border-0 p-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-600" + > + {form.label} + </a> + ); + })} </div> ); } + +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) }; +} diff --git a/packages/web-util/src/forms/FormProvider.tsx b/packages/web-util/src/forms/FormProvider.tsx index f4cdf8a68..5e08efb32 100644 --- a/packages/web-util/src/forms/FormProvider.tsx +++ b/packages/web-util/src/forms/FormProvider.tsx @@ -75,10 +75,10 @@ export interface UIFormProps<T extends object, K extends keyof T> // converter to string and back converter?: StringConverter<T[K]>; - handler?: UIField; + handler?: UIFieldHandler; } -export type UIField = { +export type UIFieldHandler = { value: string | undefined; onChange: (s: string) => void; state: FieldUIOptions; |