commit cabec1c40e93b0ca531868910bf30c0e62ac1d40 parent 08dbbb053dae1359fd3e16a6fc6c1f3460c6c307 Author: Sebastian <sebasjm@gmail.com> Date: Sun, 23 Feb 2025 12:23:58 -0300 fix #9548 Diffstat:
13 files changed, 224 insertions(+), 340 deletions(-)
diff --git a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx @@ -246,17 +246,12 @@ function Navigation(): VNode { Icon: PeopleIcon, label: i18n.str`Dashboard`, }, - { - route: privatePages.investigation, - Icon: ToInvestigateIcon, - label: i18n.str`Investigation`, - }, + { route: privatePages.accounts, Icon: HomeIcon, label: i18n.str`Accounts` }, { route: privatePages.transfers, Icon: TransfersIcon, label: i18n.str`Transfers`, }, - { route: privatePages.active, Icon: HomeIcon, label: i18n.str`Active` }, { route: privatePages.search, Icon: SearchIcon, diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx @@ -15,37 +15,32 @@ */ import { - decodeCrockFromURI, urlPattern, useCurrentLocation, useNavigationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; +import { Fragment, h, VNode } from "preact"; -import { - assertUnreachable, - parsePaytoUri, - PaytoString, -} from "@gnu-taler/taler-util"; +import { assertUnreachable } from "@gnu-taler/taler-util"; import { useEffect } from "preact/hooks"; import { ExchangeAmlFrame } from "./ExchangeAmlFrame.js"; +import { useCurrentDecisionRequest } from "./hooks/decision-request.js"; import { useOfficer } from "./hooks/officer.js"; -import { Cases, CasesUnderInvestigation } from "./pages/Cases.js"; -import { Officer } from "./pages/Officer.js"; import { CaseDetails } from "./pages/CaseDetails.js"; import { CaseUpdate, SelectForm } from "./pages/CaseUpdate.js"; +import { Accounts } from "./pages/Cases.js"; +import { Dashboard } from "./pages/Dashboard.js"; import { HandleAccountNotReady } from "./pages/HandleAccountNotReady.js"; -import { Search } from "./pages/Search.js"; import { MeasureList } from "./pages/MeasureList.js"; +import { NewMeasure } from "./pages/NewMeasure.js"; +import { Officer } from "./pages/Officer.js"; +import { Search } from "./pages/Search.js"; +import { Transfers } from "./pages/Transfers.js"; import { AmlDecisionRequestWizard, WizardSteps, } from "./pages/decision/AmlDecisionRequestWizard.js"; -import { useCurrentDecisionRequest } from "./hooks/decision-request.js"; -import { Dashboard } from "./pages/Dashboard.js"; -import { NewMeasure } from "./pages/NewMeasure.js"; -import { Transfers } from "./pages/Transfers.js"; export function Routing(): VNode { const session = useOfficer(); @@ -127,9 +122,8 @@ export const privatePages = { measuresNew: urlPattern(/\/measures\/new/, () => "#/measures/new"), measures: urlPattern(/\/measures/, () => "#/measures"), search: urlPattern(/\/search/, () => "#/search"), - investigation: urlPattern(/\/investigation/, () => "#/investigation"), transfers: urlPattern(/\/transfers/, () => "#/transfers"), - active: urlPattern(/\/active/, () => "#/active"), + accounts: urlPattern(/\/accounts/, () => "#/accounts"), caseUpdate: urlPattern<{ cid: string; type: string }>( /\/case\/(?<cid>[a-zA-Z0-9]+)\/new\/(?<type>[a-zA-Z0-9_.]+)/, ({ cid, type }) => `#/case/${cid}/new/${type}`, @@ -278,13 +272,8 @@ function PrivateRouting(): VNode { case "caseNew": { return <SelectForm account={location.values.cid} />; } - case "investigation": { - return ( - <CasesUnderInvestigation routeToCaseById={privatePages.caseDetails} /> - ); - } - case "active": { - return <Cases routeToCaseById={privatePages.caseDetails} />; + case "accounts": { + return <Accounts routeToCaseById={privatePages.caseDetails} />; } case "search": { return <Search />; diff --git a/packages/aml-backoffice-ui/src/hooks/decisions.ts b/packages/aml-backoffice-ui/src/hooks/decisions.ts @@ -42,7 +42,11 @@ export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1; * @param args * @returns */ -export function useCurrentDecisionsUnderInvestigation() { +export function useCurrentDecisions({ + investigated, +}: { + investigated?: boolean; +} = {}) { const officer = useOfficer(); const session = officer.state === "ready" ? officer.account : undefined; const { @@ -59,8 +63,8 @@ export function useCurrentDecisionsUnderInvestigation() { return await api.getAmlDecisions(officer, { order: "dec", offset, - investigation: true, active: true, + investigation, limit: PAGINATED_LIST_REQUEST, }); } @@ -68,48 +72,10 @@ export function useCurrentDecisionsUnderInvestigation() { const { data, error } = useSWR< TalerExchangeResultByMethod<"getAmlDecisions">, TalerHttpError - >(!session ? undefined : [session, offset, "getAmlDecisions"], fetcher); - - if (error) return error; - if (data === undefined) return undefined; - if (data.type !== "ok") return data; - - return buildPaginatedResult(data.body.records, offset, setOffset, (d) => - String(d.rowid), + >( + !session ? undefined : [session, offset, investigated, "getAmlDecisions"], + fetcher, ); -} - -/** - * @param account - * @param args - * @returns - */ -export function useCurrentDecisions() { - const officer = useOfficer(); - const session = officer.state === "ready" ? officer.account : undefined; - const { - lib: { exchange: api }, - } = useExchangeApiContext(); - - const [offset, setOffset] = useState<string>(); - - async function fetcher([officer, offset]: [ - OfficerAccount, - string | undefined, - boolean | undefined, - ]) { - return await api.getAmlDecisions(officer, { - order: "dec", - offset, - active: true, - limit: PAGINATED_LIST_REQUEST, - }); - } - - const { data, error } = useSWR< - TalerExchangeResultByMethod<"getAmlDecisions">, - TalerHttpError - >(!session ? undefined : [session, offset, "getAmlDecisions"], fetcher); if (error) return error; if (data === undefined) return undefined; diff --git a/packages/aml-backoffice-ui/src/hooks/transfers.ts b/packages/aml-backoffice-ui/src/hooks/transfers.ts @@ -18,6 +18,8 @@ import { useState } from "preact/hooks"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import { AmountJson, + Amounts, + AmountString, OfficerAccount, OperationOk, opFixedSuccess, @@ -87,9 +89,9 @@ export function useTransferDebit() { * @returns */ export function useTransferList({ - account, direction, -}: { direction?: "credit" | "debit"; account?: AmountJson } = {}) { + threshold, +}: { direction?: "credit" | "debit"; threshold?: AmountJson } = {}) { const officer = useOfficer(); const session = officer.state === "ready" ? officer.account : undefined; const { @@ -99,22 +101,25 @@ export function useTransferList({ const [offset, setOffset] = useState<string>(); const isDebit = "debit" === direction; - async function fetcher([officer, offset, isDebit]: [ + async function fetcher([officer, offset, isDebit, threshold]: [ OfficerAccount, string, boolean, + AmountJson | undefined, ]) { if (isDebit) { return await api.getTransfersDebit(officer, { order: "dec", offset, limit: PAGINATED_LIST_REQUEST, + threshold, }); } return await api.getTransfersCredit(officer, { order: "dec", offset, limit: PAGINATED_LIST_REQUEST, + threshold, }); } @@ -122,7 +127,9 @@ export function useTransferList({ TalerExchangeResultByMethod<"getTransfersCredit">, TalerHttpError >( - !session ? undefined : [session, offset, isDebit, "getTransfersCredit"], + !session + ? undefined + : [session, offset, isDebit, threshold, "getTransfersCredit"], fetcher, ); diff --git a/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx b/packages/aml-backoffice-ui/src/pages/Cases.stories.tsx @@ -20,34 +20,8 @@ */ import * as tests from "@gnu-taler/web-util/testing"; -import { CasesUI as TestedComponent } from "./Cases.js"; +import { Accounts as TestedComponent } from "./Cases.js"; export default { title: "cases", }; - -export const OneRow = tests.createExample(TestedComponent, { - records: [ - { - // current_state: TalerExchangeApi.AmlState.normal, - h_payto: "QWEQWEQWEQWE", - rowid: 1, - decision_time: { - t_s: "never" - }, - is_active: false, - limits: { - custom_measures: {}, - expiration_time: { - t_s: "never" - }, - rules: [], - successor_measure: undefined, - }, - to_investigate: false, - justification: undefined, - properties: undefined, - // threshold: "USD:1" as AmountString, - }, - ], -}); diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -21,15 +21,13 @@ import { } from "@gnu-taler/taler-util"; import { Attention, + InputToggle, Loading, RouteDefinition, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { - useCurrentDecisions, - useCurrentDecisionsUnderInvestigation, -} from "../hooks/decisions.js"; +import { useCurrentDecisions } from "../hooks/decisions.js"; import { useState } from "preact/hooks"; import { privatePages } from "../Routing.js"; @@ -40,73 +38,76 @@ type FormType = { // state: TalerExchangeApi.AmlState; }; -function JumpByIdForm({ - caseByIdRoute, +export function Accounts({ + routeToCaseById: caseByIdRoute, }: { - caseByIdRoute: RouteDefinition<{ cid: string }>; + routeToCaseById: RouteDefinition<{ cid: string }>; }): VNode { const { i18n } = useTranslationContext(); - const [account, setAccount] = useState<string>(""); - return ( - <form class="mt-5 sm:flex sm:items-center"> - <div class="w-full sm:max-w-xs"> - <input - type="email" - name="email" - id="email" - onChange={(e) => { - setAccount(e.currentTarget.value); - }} - aria-label="Email" - class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6" - placeholder={i18n.str`Search by ID`} - /> - </div> - <a - href={caseByIdRoute.url({ cid: account })} - class="mt-3 inline-flex w-full items-center justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 sm:ml-3 sm:mt-0 sm:w-auto" - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - stroke-width="1.5" - stroke="currentColor" - class="size-6 w-6 h-6" - > - <path - stroke-linecap="round" - stroke-linejoin="round" - d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" - /> - </svg> - </a> - </form> - ); -} + const [filtered, setFiltered] = useState<boolean>(); + const list = useCurrentDecisions({ investigated: filtered }); -export function CasesUI({ - records, - onFirstPage, - onNext, - filtered, - caseByIdRoute, -}: { - filtered: boolean; - caseByIdRoute: RouteDefinition<{ cid: string }>; - onFirstPage?: () => void; - onNext?: () => void; - records: TalerExchangeApi.AmlDecision[]; -}): VNode { - const { i18n } = useTranslationContext(); + if (!list) { + return <Loading />; + } + if (list instanceof TalerError) { + return <ErrorLoadingWithDebug error={list} />; + } + + if (list.type === "fail") { + switch (list.case) { + case HttpStatusCode.Forbidden: + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + This account signature is invalid, contact administrator or + create a new one. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + case HttpStatusCode.NotFound: + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + The designated AML account is not known, contact administrator + or create a new one. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + case HttpStatusCode.Conflict: + return ( + <Fragment> + <Attention type="danger" title={i18n.str`Operation denied`}> + <i18n.Translate> + The designated AML account is not enabled, contact administrator + or create a new one. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + default: + assertUnreachable(list); + } + } + + const records = list.body; + const onFirstPage = list.isFirstPage ? undefined : list.loadFirst; + const onNext = list.isLastPage ? undefined : list.loadNext; return ( <div> <div class="sm:flex sm:items-center"> - {filtered ? ( + {filtered === true ? ( <div class="px-2 sm:flex-auto"> <h1 class="text-base font-semibold leading-6 text-gray-900"> - <i18n.Translate>Cases under investigation</i18n.Translate> + <i18n.Translate>Accounts under investigation</i18n.Translate> </h1> <p class="mt-2 text-sm text-gray-700 w-80"> <i18n.Translate> @@ -115,20 +116,35 @@ export function CasesUI({ </i18n.Translate> </p> </div> + ) : filtered === false ? ( + <div class="px-2 sm:flex-auto"> + <h1 class="text-base font-semibold leading-6 text-gray-900"> + <i18n.Translate>Accounts without investigation</i18n.Translate> + </h1> + <p class="mt-2 text-sm text-gray-700 w-80"> + <i18n.Translate> + A list of all the accounts which are active. + </i18n.Translate> + </p> + </div> ) : ( <div class="px-2 sm:flex-auto"> <h1 class="text-base font-semibold leading-6 text-gray-900"> - <i18n.Translate>Cases</i18n.Translate> + <i18n.Translate>Accounts</i18n.Translate> </h1> <p class="mt-2 text-sm text-gray-700 w-80"> <i18n.Translate> - A list of all the known account by the exchange. + A list of all the known accounts by the exchange. </i18n.Translate> </p> </div> )} - <JumpByIdForm caseByIdRoute={caseByIdRoute} /> + <JumpByIdForm + caseByIdRoute={caseByIdRoute} + fitered={filtered} + onTog={setFiltered} + /> </div> <div class="mt-8 flow-root"> <div class="overflow-x-auto"> @@ -190,156 +206,6 @@ export function CasesUI({ ); } -export function Cases({ - routeToCaseById, -}: { - routeToCaseById: RouteDefinition<{ cid: string }>; -}) { - const list = useCurrentDecisions(); - const { i18n } = useTranslationContext(); - - if (!list) { - return <Loading />; - } - if (list instanceof TalerError) { - return <ErrorLoadingWithDebug error={list} />; - } - - if (list.type === "fail") { - switch (list.case) { - case HttpStatusCode.Forbidden: - return ( - <Fragment> - <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - This account signature is invalid, contact administrator or - create a new one. - </i18n.Translate> - </Attention> - <Officer /> - </Fragment> - ); - case HttpStatusCode.NotFound: - return ( - <Fragment> - <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - The designated AML account is not known, contact administrator - or create a new one. - </i18n.Translate> - </Attention> - <Officer /> - </Fragment> - ); - case HttpStatusCode.Conflict: - return ( - <Fragment> - <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - The designated AML account is not enabled, contact administrator - or create a new one. - </i18n.Translate> - </Attention> - <Officer /> - </Fragment> - ); - default: - assertUnreachable(list); - } - } - - return ( - <CasesUI - filtered={false} - records={list.body} - caseByIdRoute={routeToCaseById} - onFirstPage={list.isFirstPage ? undefined : list.loadFirst} - onNext={list.isLastPage ? undefined : list.loadNext} - // filter={stateFilter} - // onChangeFilter={(d) => { - // setStateFilter(d); - // }} - /> - ); -} -export function CasesUnderInvestigation({ - routeToCaseById, -}: { - routeToCaseById: RouteDefinition<{ cid: string }>; -}) { - const list = useCurrentDecisionsUnderInvestigation(); - const { i18n } = useTranslationContext(); - - if (!list) { - return <Loading />; - } - if (list instanceof TalerError) { - return <ErrorLoadingWithDebug error={list} />; - } - - if (list.type === "fail") { - switch (list.case) { - case HttpStatusCode.Forbidden: - return ( - <Fragment> - <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - This account signature is invalid, contact administrator or - create a new one. - </i18n.Translate> - </Attention> - <Officer /> - </Fragment> - ); - case HttpStatusCode.NotFound: - return ( - <Fragment> - <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - The designated AML account is not known, contact administrator - or create a new one. - </i18n.Translate> - </Attention> - <Officer /> - </Fragment> - ); - case HttpStatusCode.Conflict: - return ( - <Fragment> - <Attention type="danger" title={i18n.str`Operation denied`}> - <i18n.Translate> - The designated AML account is not enabled, contact administrator - or create a new one. - </i18n.Translate> - </Attention> - <Officer /> - </Fragment> - ); - default: - assertUnreachable(list); - } - } - - return ( - <CasesUI - filtered={true} - records={list.body} - caseByIdRoute={routeToCaseById} - onFirstPage={list.isFirstPage ? undefined : list.loadFirst} - onNext={list.isLastPage ? undefined : list.loadNext} - // filter={stateFilter} - // onChangeFilter={(d) => { - // setStateFilter(d); - // }} - /> - ); -} - -// function ToInvestigateIcon(): VNode { -// return <svg title="requires investigation" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 w-6"> -// <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /> -// </svg> -// } export const ToInvestigateIcon = () => ( <svg title="requires investigation" @@ -472,3 +338,63 @@ export function Pagination({ </nav> ); } + +function JumpByIdForm({ + caseByIdRoute, + fitered, + onTog, +}: { + caseByIdRoute: RouteDefinition<{ cid: string }>; + fitered?: boolean; + onTog: (d: boolean) => void; +}): VNode { + const { i18n } = useTranslationContext(); + const [account, setAccount] = useState<string>(""); + return ( + <form class="mt-5 sm:flex sm:items-center flex flex-col"> + <div class="flex flex-row"> + <div class="w-full sm:max-w-xs"> + <input + name="account" + onChange={(e) => { + setAccount(e.currentTarget.value); + }} + class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6" + placeholder={i18n.str`Search by ID`} + /> + </div> + <a + href={caseByIdRoute.url({ cid: account })} + class="mt-3 inline-flex w-full items-center justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 sm:ml-3 sm:mt-0 sm:w-auto" + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-6 w-6 h-6" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" + /> + </svg> + </a> + </div> + <div class="flex flex-row"> + <InputToggle<any, string> + threeState + name="inv" + label={i18n.str`Only investigated`} + handler={{ + onChange: onTog, + value: fitered, + state: {}, + }} + /> + </div> + </form> + ); +} diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx b/packages/aml-backoffice-ui/src/pages/Search.tsx @@ -539,7 +539,9 @@ const genericFields: ( help: i18n.str`As defined by RFC 8905`, placeholder: i18n.str`payto://`, validator(value) { - return parsePaytoUri(value) === undefined ? i18n.str`invalid` : undefined; + return value && parsePaytoUri(value) === undefined + ? i18n.str`invalid` + : undefined; }, }, ]; @@ -579,7 +581,7 @@ const walletFields: (i18n: InternationalizationAPI) => UIFormElementConfig[] = ( help: i18n.str`Wallet reserve public key`, placeholder: i18n.str`abcdef1235`, validator(value) { - return value.length !== 16 + return value && value.length !== 16 ? i18n.str`Should be 16 characters` : undefined; }, diff --git a/packages/aml-backoffice-ui/src/pages/Transfers.tsx b/packages/aml-backoffice-ui/src/pages/Transfers.tsx @@ -28,6 +28,10 @@ export function Transfers(): VNode { const { i18n, dateLocale } = useTranslationContext(); const { config } = useExchangeApiContext(); + type FormType = { + direction: "credit" | "debit"; + threshold: AmountJson; + }; const design: FormDesign = { type: "single-column", fields: [ @@ -46,10 +50,17 @@ export function Transfers(): VNode { }, ], }, + { + type: "amount", + id: "threshold" as UIHandlerId, + label: i18n.str`Threshold`, + help: i18n.str`All amounts below the given threshold will be filtered.`, + currency: config.config.currency, + }, ], }; - const form = useForm( + const form = useForm<FormType>( design, { direction: "credit" }, // createFormValidator(i18n), @@ -60,8 +71,11 @@ export function Transfers(): VNode { | "debit" | undefined; + const threshold = form.status.result.threshold; + const resp = useTransferList({ direction, + threshold, }); const isDebit = direction === "debit"; diff --git a/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx b/packages/aml-backoffice-ui/src/pages/decision/Properties.tsx @@ -273,7 +273,8 @@ function calculatePropertiesBasedOnState( (prev, cur) => { const since = AbsoluteTime.fromProtocolTimestamp(cur.collection_time); Object.entries(cur.attributes ?? {}).forEach(([key, value]) => { - prev[key] = { value: value.text, since }; + const v: any = value; + prev[key] = { value: "text" in (v as any) ? v.text : value, since }; }); return prev; }, diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts @@ -1067,7 +1067,7 @@ export class TalerExchangeHttpClient { */ async getTransfersDebit( auth: OfficerAccount, - params: PaginationParams & { threshold?: AmountString } = {}, + params: PaginationParams & { threshold?: AmountJson } = {}, ) { const url = new URL(`aml/${auth.id}/transfers-debit`, this.baseUrl); diff --git a/packages/web-util/src/forms/fields/InputAmount.stories.tsx b/packages/web-util/src/forms/fields/InputAmount.stories.tsx @@ -53,6 +53,11 @@ const design: FormDesign = { id: "amount" as UIHandlerId, currency: "ARS", }, + { + type: "text", + label: "label of the field" as TranslatedString, + id: "msg" as UIHandlerId, + }, ], }, ], diff --git a/packages/web-util/src/forms/fields/InputAmount.tsx b/packages/web-util/src/forms/fields/InputAmount.tsx @@ -2,32 +2,25 @@ import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util"; import { VNode, h } from "preact"; import { UIFormProps } from "../FormProvider.js"; import { InputLine } from "./InputLine.js"; -import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; export function InputAmount<T extends object, K extends keyof T>( - props: { currency?: string } & UIFormProps<T, K>, + props: { currency: string } & UIFormProps<T, K>, ): VNode { - const { value } = - props.handler ?? noHandlerPropsAndNoContextForField(props.name); - const currency = - !value || !(value as any).currency - ? props.currency - : (value as any).currency; return ( <InputLine<T, K> {...props} type="text" before={{ type: "text", - text: currency as TranslatedString, + text: props.currency as TranslatedString, }} //@ts-ignore converter={ props.converter ?? { fromStringUI: (v): AmountJson => { return ( - Amounts.parse(`${currency}:${v}`) ?? - Amounts.zeroOfCurrency(currency) + Amounts.parse(`${props.currency}:${v}`) ?? + Amounts.zeroOfCurrency(props.currency) ); }, toStringUI: (v: AmountJson) => { diff --git a/packages/web-util/src/hooks/useForm.ts b/packages/web-util/src/hooks/useForm.ts @@ -213,13 +213,25 @@ function validateRequiredFields<FormType>( result = setValueIntoPath(result, path, e); } if (formElement.validator) { - const message = formElement.validator(v as any); - if (message !== undefined) { - const e: ErrorAndLabel = { - label: formElement.label as TranslatedString, - message, - }; - result = setValueIntoPath(result, path, e); + try { + const message = formElement.validator(v as any); + if (message !== undefined) { + const e: ErrorAndLabel = { + label: formElement.label as TranslatedString, + message, + }; + result = setValueIntoPath(result, path, e); + } + } catch (e) { + console.log( + `Validation function failed. Contact developers ${String(e)}`, + ); + console.error(e); + result = setValueIntoPath( + result, + path, + `Validation function failed. Contact developers ${String(e)}`, + ); } } }