taler-typescript-core

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

commit d793d66b73a698a1a96238483c447fb5039015a9
parent 89ed039a8f5b2cd7774bcdf25ff2271bcc03e813
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Tue, 20 Jan 2026 15:35:21 -0300

fix #10871

Diffstat:
Mpackages/aml-backoffice-ui/src/pages/AccountDetails.tsx | 2+-
Mpackages/aml-backoffice-ui/src/pages/AccountList.tsx | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mpackages/kyc-ui/src/pages/Start.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/Routing.tsx | 5++++-
Mpackages/merchant-backoffice-ui/src/components/SolveMFA.tsx | 236+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mpackages/merchant-backoffice-ui/src/components/form/FormProvider.tsx | 31++++++++++++++-----------------
Mpackages/merchant-backoffice-ui/src/components/form/Input.tsx | 19+++++++++++++++++--
Mpackages/merchant-backoffice-ui/src/components/form/InputImage.tsx | 2+-
Dpackages/merchant-backoffice-ui/src/components/form/InputPayto.tsx | 51---------------------------------------------------
Mpackages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx | 5+++--
Mpackages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx | 7+++++--
Mpackages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx | 5+++--
Mpackages/merchant-backoffice-ui/src/components/modal/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/admin/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx | 4++--
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx | 44+++++++++++++++++++++-----------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx | 9+++++----
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx | 20++++++++++++++++----
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx | 38+++++++++++++++++++++-----------------
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/update/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/groups/list/Table.tsx | 28++++++++++++++++++++--------
Mpackages/merchant-backoffice-ui/src/paths/instance/groups/list/UpdatePage.tsx | 28++++++++++++++--------------
Mpackages/merchant-backoffice-ui/src/paths/instance/groups/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx | 4++--
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx | 4++--
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx | 4++--
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx | 7++++++-
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/password/index.tsx | 4++--
Mpackages/merchant-backoffice-ui/src/paths/instance/pots/create/CreatePage.tsx | 2++
Mpackages/merchant-backoffice-ui/src/paths/instance/pots/list/Table.tsx | 20++++++++++++++++----
Mpackages/merchant-backoffice-ui/src/paths/instance/pots/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/pots/update/UpdatePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/pots/update/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/reports/list/Table.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/reports/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/reports/update/UpdatePage.tsx | 34+++++++++++++++++++---------------
Mpackages/merchant-backoffice-ui/src/paths/instance/reports/update/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/statistics/list/OrdersChart.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/statistics/list/RevenueChart.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx | 37++++++++++++++++++-------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx | 41+++++++++++++++++++++--------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx | 37++++++++++++++++++-------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx | 38++++++++++++++++++--------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx | 43+++++++++++++++++++++++--------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx | 4++--
Mpackages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx | 46+++++++++++++++++++++++-----------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/update/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx | 37++++++++++++++++++-------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx | 37++++++++++++++++++-------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/login/index.tsx | 81+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx | 52++++++++++++++++++++++++++--------------------------
Mpackages/taler-util/src/http-client/merchant.ts | 2++
Mpackages/web-util/src/components/Button.tsx | 53+++++++++++++++++++++++++++++++----------------------
72 files changed, 661 insertions(+), 602 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/pages/AccountDetails.tsx b/packages/aml-backoffice-ui/src/pages/AccountDetails.tsx @@ -263,7 +263,7 @@ export function AccountDetails({ <Fragment> <div class="flex space-x-2 mb-4"> <i18n.Translate>Export as PDF</i18n.Translate> - <ButtonBetter onClick={downloadPdf}> + <ButtonBetter type="button" onClick={downloadPdf}> <img class="size-6 w-6" src={pdfIcon} /> </ButtonBetter> </div> diff --git a/packages/aml-backoffice-ui/src/pages/AccountList.tsx b/packages/aml-backoffice-ui/src/pages/AccountList.tsx @@ -21,7 +21,7 @@ import { Paytos, TalerError, assertUnreachable, - opFixedSuccess + opFixedSuccess, } from "@gnu-taler/taler-util"; import { Attention, @@ -168,8 +168,14 @@ export function AccountList({ : `not-investigated`; const time = format(new Date(), "yyyyMMdd_HHmmss"); - const downloadCsv = safeFunctionHandler(lib.exchange.getAmlAccountsAsOtherFormat.bind(lib.exchange), session ? [session, "text/csv"] : undefined); - const downloadXls = safeFunctionHandler(lib.exchange.getAmlAccountsAsOtherFormat.bind(lib.exchange), session ? [session, "application/vnd.ms-excel"] : undefined); + const downloadCsv = safeFunctionHandler( + lib.exchange.getAmlAccountsAsOtherFormat.bind(lib.exchange), + session ? [session, "text/csv"] : undefined, + ); + const downloadXls = safeFunctionHandler( + lib.exchange.getAmlAccountsAsOtherFormat.bind(lib.exchange), + session ? [session, "application/vnd.ms-excel"] : undefined, + ); downloadCsv.onSuccess = (result) => { setExported({ @@ -196,10 +202,10 @@ export function AccountList({ </p> <div class="flex space-x-2 mt-4"> <i18n.Translate>Export as file</i18n.Translate> - <ButtonBetter onClick={downloadCsv}> + <ButtonBetter type="button" onClick={downloadCsv}> <img class="size-6 w-6" src={csvIcon} /> </ButtonBetter> - <ButtonBetter onClick={downloadXls}> + <ButtonBetter type="button" onClick={downloadXls}> <img class="size-6 w-6" src={xlsIcon} /> </ButtonBetter> </div> @@ -291,22 +297,53 @@ export function AccountList({ {records.map((r, i) => { const uri = Paytos.asString(r.full_payto); if (i === 1) { - r.open_time = AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now()) - r.close_time = AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), Duration.fromSpec({ minutes: 5 }))) + r.open_time = AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.now(), + ); + r.close_time = AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ minutes: 5 }), + ), + ); } else if (i === 2) { - r.open_time = AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now()) + r.open_time = AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.now(), + ); } - const openTime = r.open_time.t_s !== "never" ? format(r.open_time.t_s * 1000, "yyyy/MM/dd HH:mm") : undefined; - const closeTime = r.close_time.t_s !== "never" ? format(r.close_time.t_s * 1000, "yyyy/MM/dd HH:mm") : undefined; - const openDescription = openTime - ? closeTime - ? <span><i18n.Translate>From {openTime}<br />To {closeTime}</i18n.Translate></span> - : i18n.str`Since ${openTime}` - : i18n.str`Not opened`; + const openTime = + r.open_time.t_s !== "never" + ? format(r.open_time.t_s * 1000, "yyyy/MM/dd HH:mm") + : undefined; + const closeTime = + r.close_time.t_s !== "never" + ? format(r.close_time.t_s * 1000, "yyyy/MM/dd HH:mm") + : undefined; + const openDescription = openTime ? ( + closeTime ? ( + <span> + <i18n.Translate> + From {openTime} + <br /> + To {closeTime} + </i18n.Translate> + </span> + ) : ( + i18n.str`Since ${openTime}` + ) + ) : ( + i18n.str`Not opened` + ); - const paramsDesc = Object.entries(uri.params).map(([name, value]) => { - return <div>{name}: {value}</div> - }) + const paramsDesc = Object.entries(uri.params).map( + ([name, value]) => { + return ( + <div> + {name}: {value} + </div> + ); + }, + ); return ( <Fragment key={r.h_payto}> <tr class="hover:bg-gray-100 "> @@ -323,9 +360,7 @@ export function AccountList({ > {uri.displayName} </a> - <p class="text-gray-500 text-xs"> - {paramsDesc} - </p> + <p class="text-gray-500 text-xs">{paramsDesc}</p> </div> </td> <td class="whitespace-nowrap px-3 text-sm text-gray-900"> @@ -682,4 +717,3 @@ const fields: FieldSet<CustomerAccountSummary> = [ ]; type FieldSet<T> = Field<T>[]; type Field<T> = { name: string; type: string; convert: (o: T) => string }; - diff --git a/packages/kyc-ui/src/pages/Start.tsx b/packages/kyc-ui/src/pages/Start.tsx @@ -235,7 +235,7 @@ function LinkGenerator({ req }: { req: KycRequirementInformation }): VNode { ) : ( // href={redirectUrl} <p class="text-sm font-semibold leading-6 text-gray-900"> - <ButtonBetter onClick={start}> + <ButtonBetter onClick={start} type="button"> <span class="absolute inset-x-0 -top-px bottom-0"></span> <i18n.Translate context="KYC_REQUIREMENT_LINK_DESCRIPTION"> {req.description} diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx @@ -249,7 +249,10 @@ export function Routing(_p: Props): VNode { <Route default component={() => ( - <LoginPage showCreateAccount={config.have_self_provisioning} /> + <LoginPage + showCreateAccount={config.have_self_provisioning} + focus + /> )} /> </Router> diff --git a/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx b/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx @@ -27,13 +27,13 @@ import { import { FormErrors, FormProvider } from "./form/FormProvider.js"; import { Input } from "./form/Input.js"; - const TALER_SCREEN_ID = 5; export interface Props { onCompleted: SafeHandlerTemplate<[challenges: string[]], any>; onCancel(): void; currentChallenge: ChallengeResponse; + focus?: boolean; } interface Form { @@ -45,11 +45,13 @@ function SolveChallenge({ expiration, onCancel, onSolved, + focus, }: { onCancel: () => void; challenge: Challenge; expiration: AbsoluteTime; onSolved: () => void; + focus?: boolean; }): VNode { const { i18n } = useTranslationContext(); const { state: session, lib, logIn } = useSessionContext(); @@ -124,75 +126,78 @@ function SolveChallenge({ <i18n.Translate>Validation code sent.</i18n.Translate> </p> </header> - <section - class="modal-card-body" - style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} + <FormProvider<Form> + name="settings" + errors={errors} + object={value} + valueHandler={valueHandler} > - {(function (): VNode { - switch (challenge.tan_channel) { - case TanChannel.SMS: - return ( - <i18n.Translate> - The verification code sent to the phone number ending - with "<b>{challenge.tan_info}</b>" - </i18n.Translate> - ); - case TanChannel.EMAIL: - return ( - <i18n.Translate> - The verification code sent to the email address starting - with "<b>{challenge.tan_info}</b>" - </i18n.Translate> - ); - } - })()} - <FormProvider<Form> - name="settings" - errors={errors} - object={value} - valueHandler={valueHandler} + <section + class="modal-card-body" + style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} > + {(function (): VNode { + switch (challenge.tan_channel) { + case TanChannel.SMS: + return ( + <i18n.Translate> + The verification code sent to the phone number ending + with "<b>{challenge.tan_info}</b>" + </i18n.Translate> + ); + case TanChannel.EMAIL: + return ( + <i18n.Translate> + The verification code sent to the email address + starting with "<b>{challenge.tan_info}</b>" + </i18n.Translate> + ); + } + })()} <Input<Form> label={i18n.str`Verification code`} name="code" + focus={focus} readonly={showExpired} /> - </FormProvider> - {expiration.t_ms === "never" ? undefined : ( - <p> - <i18n.Translate> - It will expire at{" "} - <span>{format( - expiration.t_ms, - datetimeFormatForSettings(settings), - )}</span> - </i18n.Translate> - </p> - )} - {showExpired ? ( - <p> - <i18n.Translate> - The challenge is expired and can't be solved but you can go - back and create a new challenge. - </i18n.Translate> - </p> - ) : undefined} - </section> - <footer - class="modal-card-foot " - style={{ - justifyContent: "space-between", - border: "1px solid", - borderTop: 0, - }} - > - <button class="button" type="button" onClick={onCancel}> - <i18n.Translate>Back</i18n.Translate> - </button> - <ButtonBetterBulma type="submit" onClick={verify}> - <i18n.Translate>Verify</i18n.Translate> - </ButtonBetterBulma> - </footer> + {expiration.t_ms === "never" ? undefined : ( + <p> + <i18n.Translate> + It will expire at{" "} + <span> + {format( + expiration.t_ms, + datetimeFormatForSettings(settings), + )} + </span> + </i18n.Translate> + </p> + )} + {showExpired ? ( + <p> + <i18n.Translate> + The challenge is expired and can't be solved but you can + go back and create a new challenge. + </i18n.Translate> + </p> + ) : undefined} + </section> + <footer + class="modal-card-foot " + style={{ + justifyContent: "space-between", + border: "1px solid", + borderTop: 0, + }} + > + <button class="button" type="button" onClick={onCancel}> + <i18n.Translate>Back</i18n.Translate> + </button> + <ButtonBetterBulma type="submit" onClick={verify}> + <i18n.Translate>Verify</i18n.Translate> + </ButtonBetterBulma> + </footer> + </FormProvider> </div> </div> </div> @@ -204,6 +209,7 @@ export function SolveMFAChallenges({ currentChallenge, onCompleted, onCancel, + focus, }: Props): VNode { const { i18n } = useTranslationContext(); const { state: session, lib, logIn } = useSessionContext(); @@ -225,49 +231,10 @@ export function SolveMFAChallenges({ const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const [settings] = usePreference(); - if (selected) { - return ( - <SolveChallenge - onCancel={() => setSelected(undefined)} - challenge={selected.ch} - expiration={selected.expiration} - // onSolved={() => { - // setSelected(undefined); - // const newSolved = [...solved, selected.ch.challenge_id]; - - // const done = currentChallenge.combi_and - // ? newSolved.length === currentChallenge.challenges.length - // : newSolved.length > 0; - - // if (done) { - // onCompleted(newSolved); - // } else { - // setSolved(newSolved); - // } - // }} - onSolved={() => { - setSelected(undefined); - const total = [...solved, selected.ch.challenge_id]; - const enough = currentChallenge.combi_and - ? total.length === currentChallenge.challenges.length - : total.length > 0; - if (enough) { - onCompleted.withArgs(total).call(); - } else { - setSolved(total); - } - }} - /> - ); - } - - const hasSolvedEnough = currentChallenge.combi_and - ? solved.length === currentChallenge.challenges.length - : solved.length > 0; - const sendMessage = safeFunctionHandler((ch: Challenge) => lib.instance.sendChallenge(ch.challenge_id), ); + sendMessage.onSuccess = (success, ch) => { if (success.earliest_retransmission) { setRetransmission({ @@ -313,6 +280,47 @@ export function SolveMFAChallenges({ return opEmptySuccess(); }); + if (selected) { + return ( + <SolveChallenge + onCancel={() => setSelected(undefined)} + challenge={selected.ch} + expiration={selected.expiration} + focus={focus} + // onSolved={() => { + // setSelected(undefined); + // const newSolved = [...solved, selected.ch.challenge_id]; + + // const done = currentChallenge.combi_and + // ? newSolved.length === currentChallenge.challenges.length + // : newSolved.length > 0; + + // if (done) { + // onCompleted(newSolved); + // } else { + // setSolved(newSolved); + // } + // }} + onSolved={() => { + setSelected(undefined); + const total = [...solved, selected.ch.challenge_id]; + const enough = currentChallenge.combi_and + ? total.length === currentChallenge.challenges.length + : total.length > 0; + if (enough) { + onCompleted.withArgs(total).call(); + } else { + setSolved(total); + } + }} + /> + ); + } + + const hasSolvedEnough = currentChallenge.combi_and + ? solved.length === currentChallenge.challenges.length + : solved.length > 0; + return ( <Fragment> <LocalNotificationBannerBulma notification={notification} /> @@ -344,7 +352,7 @@ export function SolveMFAChallenges({ </i18n.Translate> )} </section> - {currentChallenge.challenges.map((challenge) => { + {currentChallenge.challenges.map((challenge, idx) => { const time = retransmission[challenge.tan_channel]; const alreadySent = !AbsoluteTime.isExpired(time); const noNeedToComplete = @@ -400,28 +408,16 @@ export function SolveMFAChallenges({ }} > <ButtonBetterBulma - // disabled={ - // hasSolvedEnough || solved.indexOf(d.challenge_id) !== -1 - // } + type="button" class="button" - // onClick={() => { - // setSelected({ - // ch: d, - // expiration: AbsoluteTime.never(), - // }); - // }} onClick={doSelect} > <i18n.Translate>I have a code</i18n.Translate> </ButtonBetterBulma> <ButtonBetterBulma - // disabled={ - // hasSolvedEnough || - // solved.indexOf(d.challenge_id) !== -1 || - // alreadySent - // } + type="button" onClick={doSend} - // onClick={() => doSendCodeImpl(d)} + focus={idx === 0 && focus} > <i18n.Translate>Send me a message</i18n.Translate> </ButtonBetterBulma> @@ -440,7 +436,7 @@ export function SolveMFAChallenges({ <button class="button" onClick={onCancel}> <i18n.Translate>Cancel</i18n.Translate> </button> - <div/> + <div /> </footer> </div> </div> diff --git a/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx b/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx @@ -19,7 +19,12 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { FacadeCredentials, Location, TalerMerchantApi, TranslatedString } from "@gnu-taler/taler-util"; +import { + FacadeCredentials, + Location, + TalerMerchantApi, + TranslatedString, +} from "@gnu-taler/taler-util"; import { ComponentChildren, createContext, h, VNode } from "preact"; import { useContext, useMemo } from "preact/hooks"; @@ -29,7 +34,7 @@ export interface Props<T> { object?: Partial<T>; errors?: FormErrors<T>; name?: string; - valueHandler: Updater<Partial<T>> | null; + valueHandler?: Updater<Partial<T>>; children: ComponentChildren; } @@ -58,15 +63,7 @@ export function FormProvider<T>({ return ( <FormContext.Provider value={value}> - <form - // class="field" - // onSubmit={(e) => { - // // e.preventDefault(); - // // if (valueHandler) valueHandler(object); - // }} - > - {children} - </form> + <form>{children}</form> </FormContext.Provider> ); } @@ -100,14 +97,14 @@ export type TalerForm = { export type FormErrors<T> = { [P in keyof T]?: T[P] extends Location - ? FormErrors<T[P]> - : T[P] extends FacadeCredentials - ? FormErrors<T[P]> - : T[P] extends TalerForm ? FormErrors<T[P]> - : T[P] extends Partial<TalerForm> + : T[P] extends FacadeCredentials ? FormErrors<T[P]> - : TranslatedString | undefined; + : T[P] extends TalerForm + ? FormErrors<T[P]> + : T[P] extends Partial<TalerForm> + ? FormErrors<T[P]> + : TranslatedString | undefined; }; export type FormtoStr<T> = { diff --git a/packages/merchant-backoffice-ui/src/components/form/Input.tsx b/packages/merchant-backoffice-ui/src/components/form/Input.tsx @@ -29,21 +29,34 @@ interface Props<T> extends InputProps<T> { inputExtra?: any; side?: ComponentChildren; children?: ComponentChildren; + focus?: boolean; +} + +export function doAutoFocus<T extends HTMLElement>( + element: T | null | undefined, +) { + if (element) { + setTimeout(() => { + element.focus({ preventScroll: true }); + }, 100); + } } const defaultToString = (f?: any): string => f || ""; const defaultFromString = (v: string): any => v as any; -const TextInput = ({ inputType, error, ...rest }: any) => +const TextInput = ({ inputType, focus, error, ...rest }: any) => inputType === "multiline" ? ( <textarea {...rest} + ref={focus ? doAutoFocus : undefined} class={error ? "textarea is-danger" : "textarea"} rows="3" /> ) : ( <input {...rest} + ref={focus ? doAutoFocus : undefined} class={error ? "input is-danger" : "input"} type={inputType} /> @@ -58,6 +71,7 @@ export function Input<T>({ expand, help, children, + focus, inputType, inputExtra, side, @@ -71,7 +85,7 @@ export function Input<T>({ <label class="label"> {label} {required && ( - <span class="has-text-danger" style={{marginLeft:5}}> + <span class="has-text-danger" style={{ marginLeft: 5 }}> * </span> )} @@ -99,6 +113,7 @@ export function Input<T>({ readonly={readonly} disabled={readonly} name={String(name)} + focus={focus} value={toStr(value)} onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void => onChange(fromStr(e.currentTarget.value)) diff --git a/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx b/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx @@ -115,7 +115,7 @@ export function InputImage<T>({ </p> )} {!value && ( - <button class="button" onClick={() => image.current?.click()}> + <button class="button" type="button" onClick={() => image.current?.click()}> <i18n.Translate>Add</i18n.Translate> </button> )} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx @@ -1,51 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { h, VNode } from "preact"; -import { InputArray } from "./InputArray.js"; -import { PAYTO_REGEX } from "../../utils/constants.js"; -import { InputProps } from "./useField.js"; - -export type Props<T> = InputProps<T>; - -const PAYTO_START_REGEX = /^payto:\/\//; - -export function InputPayto<T>({ - name, - readonly, - placeholder, - tooltip, - label, - help, -}: Props<keyof T>): VNode { - return ( - <InputArray<T> - name={name} - readonly={readonly} - addonBefore="payto://" - label={label} - placeholder={placeholder} - help={help} - tooltip={tooltip} - toStr={(v?: string) => (!v ? "" : v.replace(PAYTO_START_REGEX, ""))} - fromStr={(v: string) => `payto://${v}`} - /> - ); -} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx @@ -26,7 +26,6 @@ import { FormErrors, FormProvider } from "./FormProvider.js"; import { InputWithAddon } from "./InputWithAddon.js"; import { TranslatedString } from "@gnu-taler/taler-util"; - const TALER_SCREEN_ID = 12; type Entity = { @@ -84,7 +83,9 @@ export function InputSearchOnList<T extends Entity>({ {selected.description} </p> <div class="buttons is-right mt-5"> - <button type="button" class="button is-info" + <button + type="button" + class="button is-info" onClick={() => onChange(undefined)} > <i18n.Translate>Clear</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx b/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx @@ -112,7 +112,8 @@ export function InputTaxes<T>({ name, label }: Props<keyof T>): VNode { tooltip={i18n.str`Taxes can be in currencies that differ from the main currency used by the merchant.`} > <i18n.Translate> - Enter the currency and value separated by a colon (e.g., &quot;USD:2.3&quot;). + Enter the currency and value separated by a colon (e.g., + &quot;USD:2.3&quot;). </i18n.Translate> </Input> @@ -123,7 +124,9 @@ export function InputTaxes<T>({ name, label }: Props<keyof T>): VNode { /> <div class="buttons is-right mt-5"> - <button type="button" class="button is-info" + <button + type="submit" + class="button is-info" data-tooltip={i18n.str`Add tax to the tax list`} disabled={hasErrors} onClick={submit} diff --git a/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx @@ -23,6 +23,7 @@ import { import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { useSessionContext } from "../../context/session.js"; +import { FormProvider } from "./FormProvider.js"; function NotificationFieldFoot({ notification, @@ -68,7 +69,7 @@ export function JumpToElementById({ <div class="level"> <div class="level-left"> <div class="level-item"> - <form> + <FormProvider> <div class="field has-addons"> <div class="control"> <input @@ -95,7 +96,7 @@ export function JumpToElementById({ </ButtonBetterBulma> </span> </div> - </form> + </FormProvider> </div> </div> </div> diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx @@ -32,10 +32,10 @@ import { } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, h, VNode } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; -import { doAutoFocus } from "../../../../web-util/src/components/utils.js"; import { DEFAULT_REQUEST_TIMEOUT } from "../../utils/constants.js"; import { Spinner } from "../exception/loading.js"; import { QR } from "../exception/QR.js"; +import { doAutoFocus } from "../form/Input.js"; const TALER_SCREEN_ID = 18; diff --git a/packages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx @@ -19,7 +19,7 @@ */ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, h, VNode } from "preact"; -import { doAutoFocus } from "../../../../web-util/src/components/utils.js"; +import { doAutoFocus } from "../form/Input.js"; const TALER_SCREEN_ID = 19; diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx @@ -53,7 +53,7 @@ export default function Instances({ if (result.type === "fail") { switch (result.case) { case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx @@ -64,10 +64,10 @@ export default function AccessTokenListPage({ onCreate }: Props): VNode { return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } case HttpStatusCode.Forbidden: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx @@ -57,7 +57,6 @@ import { FragmentPersonaFlag } from "../../../../components/menu/SideBar.js"; import { UIElement, usePreference } from "../../../../hooks/preference.js"; import { InputPassword } from "../../../../components/form/InputPassword.js"; - const TALER_SCREEN_ID = 33; type Entity = TalerMerchantApi.AccountAddDetails & { @@ -73,9 +72,8 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const [{ persona }] = usePreference(); - const accountAuthType = persona === "developer" - ? ["none", "basic", "bearer"] - : ["none", "basic"]; + const accountAuthType = + persona === "developer" ? ["none", "basic", "bearer"] : ["none", "basic"]; const [state, setState] = useState<Partial<Entity>>({ credit_facade_credentials: { @@ -312,6 +310,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { side={ <ButtonBetterBulma class="button is-info" + type="button" data-tooltip={i18n.str`Compare info from server with account form`} onClick={test} > @@ -320,26 +319,25 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { } /> </FragmentPersonaFlag> + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <ButtonBetterBulma + data-tooltip={ + hasErrors + ? i18n.str`Please complete the marked fields` + : i18n.str`Confirm operation` + } + onClick={add} + type="submit" + > + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </div> </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <ButtonBetterBulma - data-tooltip={ - hasErrors - ? i18n.str`Please complete the marked fields` - : i18n.str`Confirm operation` - } - onClick={add} - type="submit" - > - <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> - </div> </div> <div class="column" /> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx @@ -57,7 +57,7 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx @@ -58,7 +58,6 @@ import { FragmentPersonaFlag } from "../../../../components/menu/SideBar.js"; import { UIElement, usePreference } from "../../../../hooks/preference.js"; import { InputPassword } from "../../../../components/form/InputPassword.js"; - const TALER_SCREEN_ID = 36; type Entity = TalerMerchantApi.BankAccountDetail & WithId; @@ -76,9 +75,10 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const [{ persona }] = usePreference(); - const accountAuthType = persona === "developer" - ? ["unedit", "none", "basic", "bearer"] - : ["unedit", "none", "basic"]; + const accountAuthType = + persona === "developer" + ? ["unedit", "none", "basic", "bearer"] + : ["unedit", "none", "basic"]; const [state, setState] = useState<Partial<FormType>>({ payto_uri: account.payto_uri, @@ -398,6 +398,7 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { threeState side={ <ButtonBetterBulma + type="button" class="button is-info" data-tooltip={i18n.str`Compare info from server with account form`} onClick={test} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx @@ -77,7 +77,7 @@ export default function UpdateValidator({ return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx @@ -19,7 +19,11 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { assertUnreachable, HttpStatusCode, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + assertUnreachable, + HttpStatusCode, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; import { ButtonBetterBulma, LocalNotificationBannerBulma, @@ -87,7 +91,12 @@ export function CardTable({ class="has-tooltip-left" data-tooltip={i18n.str`Add new devices`} > - <button class="button is-info" accessKey="+" type="button" onClick={onCreate}> + <button + class="button is-info" + accessKey="+" + type="button" + onClick={onCreate} + > <span class="icon is-small"> <i class="mdi mdi-plus mdi-36px" /> </span> @@ -139,7 +148,8 @@ function Table({ return ( <div class="table-container"> {onLoadMoreBefore && ( - <button type="button" + <button + type="button" class="button is-fullwidth" data-tooltip={i18n.str`Load more devices before the first one`} onClick={onLoadMoreBefore} @@ -187,6 +197,7 @@ function Table({ <td class="is-actions-cell right-sticky"> <div class="buttons is-right"> <ButtonBetterBulma + type="button" class="button is-danger is-small has-tooltip-left" data-tooltip={i18n.str`Delete selected category from the database`} onClick={onDelete.withArgs(String(i.category_id))} @@ -201,7 +212,8 @@ function Table({ </tbody> </table> {onLoadMoreAfter && ( - <button type="button" + <button + type="button" class="button is-fullwidth" data-tooltip={i18n.str`Load more devices after the last one`} onClick={onLoadMoreAfter} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx @@ -60,7 +60,7 @@ export default function ListCategories({ onCreate, onSelect }: Props): VNode { return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx @@ -42,7 +42,7 @@ import { useSessionContext } from "../../../../context/session.js"; import { WithId } from "../../../../declaration.js"; import { ProductWithId, - useInstanceProductsFromIds + useInstanceProductsFromIds, } from "../../../../hooks/product.js"; const TALER_SCREEN_ID = 39; @@ -145,22 +145,25 @@ export function UpdatePage({ category, onUpdated, onBack }: Props): VNode { label={i18n.str`Name`} tooltip={i18n.str`Name of the category`} /> + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <ButtonBetterBulma + type="submit" + data-tooltip={i18n.str`Confirm operation`} + onClick={update} + > + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </div> + <ProductListSmall + onSelect={() => {}} + list={category.products} + /> </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <ButtonBetterBulma - data-tooltip={i18n.str`Confirm operation`} - onClick={update} - > - <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> - </div> - <ProductListSmall onSelect={() => {}} list={category.products} /> </div> </div> </section> @@ -289,7 +292,8 @@ function Table({ </tbody> </table> {onLoadMoreAfter && ( - <button type="button" + <button + type="button" class="button is-fullwidth" data-tooltip={i18n.str`Load more products after the last one`} onClick={onLoadMoreAfter} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/index.tsx @@ -54,7 +54,7 @@ export default function UpdateCategory({ return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/groups/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/groups/list/Table.tsx @@ -19,7 +19,11 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { assertUnreachable, HttpStatusCode, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + assertUnreachable, + HttpStatusCode, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; import { ButtonBetterBulma, LocalNotificationBannerBulma, @@ -87,7 +91,12 @@ export function CardTable({ class="has-tooltip-left" data-tooltip={i18n.str`Add new group`} > - <button class="button is-info" accessKey="+" type="button" onClick={onCreate}> + <button + class="button is-info" + accessKey="+" + type="button" + onClick={onCreate} + > <span class="icon is-small"> <i class="mdi mdi-plus mdi-36px" /> </span> @@ -139,7 +148,8 @@ function Table({ return ( <div class="table-container"> {onLoadMoreBefore && ( - <button type="button" + <button + type="button" class="button is-fullwidth" data-tooltip={i18n.str`Load more groups before the first one`} onClick={onLoadMoreBefore} @@ -164,20 +174,21 @@ function Table({ return ( <tr key={i.group_serial}> <td - // onClick={(): void => onSelect(i)} - // style={{ cursor: "pointer" }} + // onClick={(): void => onSelect(i)} + // style={{ cursor: "pointer" }} > {i.group_name} </td> <td - // onClick={(): void => onSelect(i)} - // style={{ cursor: "pointer" }} + // onClick={(): void => onSelect(i)} + // style={{ cursor: "pointer" }} > {i.description} </td> <td class="is-actions-cell right-sticky"> <div class="buttons is-right"> <ButtonBetterBulma + type="button" class="button is-danger is-small has-tooltip-left" data-tooltip={i18n.str`Delete selected group from the database`} onClick={onDelete.withArgs(String(i.group_serial))} @@ -192,7 +203,8 @@ function Table({ </tbody> </table> {onLoadMoreAfter && ( - <button type="button" + <button + type="button" class="button is-fullwidth" data-tooltip={i18n.str`Load more groups after the last one`} onClick={onLoadMoreAfter} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/groups/list/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/groups/list/UpdatePage.tsx @@ -148,21 +148,21 @@ export function UpdatePage({ group, onUpdated, onBack }: Props): VNode { name="description" label={i18n.str`Description`} /> + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <ButtonBetterBulma + type="submit" + data-tooltip={i18n.str`Confirm operation`} + onClick={update} + > + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </div> </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <ButtonBetterBulma - data-tooltip={i18n.str`Confirm operation`} - onClick={update} - > - <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> - </div> </div> </div> </section> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/groups/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/groups/list/index.tsx @@ -49,7 +49,7 @@ export default function ListProductGroups({ onCreate }: Props): VNode { return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx @@ -55,7 +55,7 @@ export default function OrderCreate({ onConfirm, onBack }: Props): VNode { if (detailsResult.type === "fail") { switch (detailsResult.case) { case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } case HttpStatusCode.NotFound: { return <NotFoundPageOrAdminCreate />; @@ -75,7 +75,7 @@ export default function OrderCreate({ onConfirm, onBack }: Props): VNode { return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(inventoryResult); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx @@ -92,7 +92,7 @@ function ContractTerms_V0({ value }: { value: CT0 }) { return ( <InputGroup name="contract_terms" label={i18n.str`Contract terms`}> - <FormProvider<CT0> object={value} valueHandler={null}> + <FormProvider<CT0> object={value}> <Input<CT0> readonly name="summary" @@ -178,7 +178,7 @@ function ContractTerms_V1({ value }: { value: CT1 }) { return ( <InputGroup name="contract_terms" label={i18n.str`Contract terms`}> - <FormProvider<CT1> object={value} valueHandler={null}> + <FormProvider<CT1> object={value}> <Input<CT1> readonly name="summary" diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx @@ -55,7 +55,7 @@ export default function Update({ oid, onBack }: Props): VNode { return <i18n.Translate>Order unknown</i18n.Translate>; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx @@ -129,7 +129,7 @@ export default function OrderList({ return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); @@ -225,7 +225,7 @@ function RefundModalForTable({ return <i18n.Translate>Order unknown</i18n.Translate>; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx @@ -19,7 +19,11 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { assertUnreachable, HttpStatusCode, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + assertUnreachable, + HttpStatusCode, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; import { ButtonBetterBulma, LocalNotificationBannerBulma, @@ -184,6 +188,7 @@ function Table({ <td class="is-actions-cell right-sticky"> <div class="buttons is-right"> <ButtonBetterBulma + type="button" class="button is-danger is-small has-tooltip-left" data-tooltip={i18n.str`Delete selected devices from the database`} onClick={onDelete.withArgs(i.otp_device_id)} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx @@ -57,7 +57,7 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx @@ -68,7 +68,7 @@ export default function UpdateValidator({ return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage /> + return <LoginPage focus /> } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx @@ -60,7 +60,7 @@ export default function PasswordPage({ onCancel, onChange }: Props): VNode { if (result.type === "fail") { switch (result.case) { case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } case HttpStatusCode.NotFound: { return <NotFoundPageOrAdminCreate />; @@ -179,7 +179,7 @@ export function AdminPassword({ if (result.type === "fail") { switch (result.case) { case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } case HttpStatusCode.NotFound: { return <NotFoundPageOrAdminCreate />; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/create/CreatePage.tsx @@ -75,6 +75,8 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; + case HttpStatusCode.Conflict: + return i18n.str`There is already a money pot with the same id.`; case HttpStatusCode.NotFound: return i18n.str`Not found`; default: diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/list/Table.tsx @@ -19,7 +19,11 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { assertUnreachable, HttpStatusCode, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + assertUnreachable, + HttpStatusCode, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; import { ButtonBetterBulma, LocalNotificationBannerBulma, @@ -87,7 +91,12 @@ export function CardTable({ class="has-tooltip-left" data-tooltip={i18n.str`Add new pots`} > - <button class="button is-info" accessKey="+" type="button" onClick={onCreate}> + <button + class="button is-info" + accessKey="+" + type="button" + onClick={onCreate} + > <span class="icon is-small"> <i class="mdi mdi-plus mdi-36px" /> </span> @@ -139,7 +148,8 @@ function Table({ return ( <div class="table-container"> {onLoadMoreBefore && ( - <button type="button" + <button + type="button" class="button is-fullwidth" data-tooltip={i18n.str`Load more pots before the first one`} onClick={onLoadMoreBefore} @@ -178,6 +188,7 @@ function Table({ <td class="is-actions-cell right-sticky"> <div class="buttons is-right"> <ButtonBetterBulma + type="button" class="button is-danger is-small has-tooltip-left" data-tooltip={i18n.str`Delete selected pots from the database`} onClick={onDelete.withArgs(String(i.pot_serial))} @@ -192,7 +203,8 @@ function Table({ </tbody> </table> {onLoadMoreAfter && ( - <button type="button" + <button + type="button" class="button is-fullwidth" data-tooltip={i18n.str`Load more pots after the last one`} onClick={onLoadMoreAfter} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/list/index.tsx @@ -50,7 +50,7 @@ export default function ListMoneyPots({ onCreate, onSelect }: Props): VNode { return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/update/UpdatePage.tsx @@ -92,7 +92,6 @@ export function UpdatePage({ moneyPot, onUpdated, onBack }: Props): VNode { } } - let invalidAmount: TranslatedString | undefined = undefined; const errors = undefinedIfEmpty<FormErrors<Entity>>({ pot_name: !state.pot_name ? i18n.str`Required` : undefined, @@ -188,6 +187,7 @@ export function UpdatePage({ moneyPot, onUpdated, onBack }: Props): VNode { </button> )} <ButtonBetterBulma + type="button" data-tooltip={i18n.str`Confirm operation`} onClick={update} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/update/index.tsx @@ -54,7 +54,7 @@ export default function UpdateMoneyPots({ return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx @@ -89,7 +89,7 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx @@ -51,7 +51,7 @@ export default function UpdateProduct({ return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage /> + return <LoginPage focus /> } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/list/Table.tsx @@ -194,6 +194,7 @@ function Table({ <div class="buttons is-right"> <ButtonBetterBulma class="button is-danger is-small has-tooltip-left" + type="button" data-tooltip={i18n.str`Delete selected scheduled report from the database`} onClick={onDelete.withArgs(String(i.report_serial))} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/list/index.tsx @@ -50,7 +50,7 @@ export default function ListScheduledReport({ onCreate, onSelect }: Props): VNod return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/update/UpdatePage.tsx @@ -176,7 +176,11 @@ export function UpdatePage({ report, onUpdated, onBack }: Props): VNode { <section class="section is-main-section"> <div class="columns"> <div class="column is-four-fifths"> - <FormProvider object={state} valueHandler={setState} errors={errors}> + <FormProvider + object={state} + valueHandler={setState} + errors={errors} + > <Input<Entity> name="description" label={i18n.str`Description`} @@ -224,21 +228,21 @@ export function UpdatePage({ report, onUpdated, onBack }: Props): VNode { label={i18n.str`Report frequency`} useProtocolDuration /> + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <ButtonBetterBulma + type="submit" + data-tooltip={i18n.str`Confirm operation`} + onClick={update} + > + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </div> </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <ButtonBetterBulma - data-tooltip={i18n.str`Confirm operation`} - onClick={update} - > - <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> - </div> </div> </div> </section> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/update/index.tsx @@ -55,7 +55,7 @@ export default function UpdateScheduledReport({ return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/OrdersChart.tsx b/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/OrdersChart.tsx @@ -85,7 +85,7 @@ export function OrdersChart({ if (counters.type === "fail") { switch (counters.case) { case HttpStatusCode.Unauthorized: - return <LoginPage />; + return <LoginPage focus />; case HttpStatusCode.NotFound: return <NotFoundPageOrAdminCreate />; case HttpStatusCode.BadGateway: diff --git a/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/RevenueChart.tsx b/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/RevenueChart.tsx @@ -102,7 +102,7 @@ export function RevenueChart({ if (revenues.type === "fail") { switch (revenues.case) { case HttpStatusCode.Unauthorized: - return <LoginPage />; + return <LoginPage focus />; case HttpStatusCode.NotFound: return <NotFoundPageOrAdminCreate />; case HttpStatusCode.BadGateway: diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx @@ -324,26 +324,25 @@ export function CreatePage({ tooltip={i18n.str`Use to verify transactions in offline mode.`} /> )} + <div class="buttons is-right mt-5"> + {onBack && ( + <button type="button" class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <ButtonBetterBulma + data-tooltip={ + hasErrors + ? i18n.str`Please complete the marked fields` + : i18n.str`Confirm operation` + } + type="submit" + onClick={create} + > + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </div> </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button type="button" class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <ButtonBetterBulma - data-tooltip={ - hasErrors - ? i18n.str`Please complete the marked fields` - : i18n.str`Confirm operation` - } - type="submit" - onClick={create} - > - <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> - </div> </div> <div class="column" /> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx @@ -48,7 +48,7 @@ export default function CreateTemplate({ onConfirm, onBack }: Props): VNode { if (result.type === "fail") { switch (result.case) { case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } case HttpStatusCode.NotFound: { return <NotFoundPageOrAdminCreate />; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx @@ -75,7 +75,7 @@ export default function ListTemplates({ return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx @@ -51,7 +51,7 @@ export default function TemplateQrPage({ tid, onBack }: Props): VNode { return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx @@ -205,7 +205,9 @@ export function UpdatePage({ template, onUpdated, onBack }: Props): VNode { const contract_summary = state.summary_editable ? undefined : state.summary; const template_contract: TalerMerchantApi.TemplateContractDetails = { minimum_age: state.minimum_age!, - pay_duration: state.pay_duration ? Duration.toTalerProtocolDuration(state.pay_duration) : TalerProtocolDuration.forever(), + pay_duration: state.pay_duration + ? Duration.toTalerProtocolDuration(state.pay_duration) + : TalerProtocolDuration.forever(), amount: contract_amount, summary: contract_summary, currency: @@ -383,26 +385,25 @@ export function UpdatePage({ template, onUpdated, onBack }: Props): VNode { tooltip={i18n.str`Use to verify transactions in offline mode.`} /> )} + <div class="buttons is-right mt-5"> + {onBack && ( + <button type="button" class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <ButtonBetterBulma + data-tooltip={ + errors !== undefined + ? i18n.str`Please complete the marked fields` + : i18n.str`Confirm operation` + } + type="submit" + onClick={update} + > + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </div> </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button type="button" class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <ButtonBetterBulma - data-tooltip={ - errors !== undefined - ? i18n.str`Please complete the marked fields` - : i18n.str`Confirm operation` - } - type="submit" - onClick={update} - > - <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> - </div> </div> </div> </section> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx @@ -59,7 +59,7 @@ export default function UpdateTemplate({ return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx @@ -163,26 +163,25 @@ export function UsePage({ readonly={!!template.template_contract.summary} tooltip={i18n.str`Title of the order to be shown to the customer`} /> + <div class="buttons is-right mt-5"> + {onBack && ( + <button type="button" class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <ButtonBetterBulma + data-tooltip={ + errors !== undefined + ? i18n.str`Please complete the marked fields` + : "confirm operation" + } + type="submit" + onClick={useTemplate} + > + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </div> </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button type="button" class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <ButtonBetterBulma - data-tooltip={ - errors !== undefined - ? i18n.str`Please complete the marked fields` - : "confirm operation" - } - type="submit" - onClick={useTemplate} - > - <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> - </div> </div> <div class="column" /> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx @@ -59,7 +59,7 @@ export default function TemplateUsePage({ return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx @@ -166,27 +166,25 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { withForever useProtocolDuration /> + <div class="buttons is-right mt-5"> + {onBack && ( + <button type="button" class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <ButtonBetterBulma + onClick={create} + data-tooltip={ + !create.args + ? i18n.str`Please complete the marked fields` + : "confirm operation" + } + type="submit" + > + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </div> </FormProvider> - {/* <Test /> */} - - <div class="buttons is-right mt-5"> - {onBack && ( - <button type="button"class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <ButtonBetterBulma - onClick={create} - data-tooltip={ - !create.args - ? i18n.str`Please complete the marked fields` - : "confirm operation" - } - type="submit" - > - <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> - </div> </div> <div class="column" /> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx @@ -69,7 +69,7 @@ export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode { return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx @@ -19,7 +19,11 @@ * @author Christian Blättler */ -import { assertUnreachable, HttpStatusCode, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + assertUnreachable, + HttpStatusCode, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; import { ButtonBetterBulma, LocalNotificationBannerBulma, @@ -142,26 +146,25 @@ export function UpdatePage({ onUpdated, onBack, tokenFamily }: Props) { withForever useProtocolDuration /> + <div class="buttons is-right mt-5"> + {onBack && ( + <button type="button" class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <ButtonBetterBulma + data-tooltip={ + hasErrors + ? i18n.str`Please complete the marked fields` + : i18n.str`Confirm operation` + } + type="submit" + onClick={update} + > + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </div> </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button type="button"class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <ButtonBetterBulma - data-tooltip={ - hasErrors - ? i18n.str`Please complete the marked fields` - : i18n.str`Confirm operation` - } - type="submit" - onClick={update} - > - <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> - </div> </div> </div> </section> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx @@ -57,7 +57,7 @@ export default function UpdateTokenFamily({ return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx @@ -139,7 +139,7 @@ export default function ListTransfer({}: Props): VNode { if (result.type === "fail") { switch (result.case) { case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } case HttpStatusCode.NotFound: { return <NotFoundPageOrAdminCreate />; @@ -168,7 +168,7 @@ export default function ListTransfer({}: Props): VNode { if (result.type === "fail") { switch (result.case) { case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } case HttpStatusCode.NotFound: { return <NotFoundPageOrAdminCreate />; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx @@ -214,30 +214,30 @@ export function UpdatePage({ valueHandler={valueHandler} > <DefaultInstanceFormFields showId={false} /> - </FormProvider> - <div class="buttons is-right mt-4"> - <button - type="button" - class="button" - onClick={onBack} - data-tooltip="cancel operation" - > - <i18n.Translate>Cancel</i18n.Translate> - </button> - - <ButtonBetterBulma - type="submit" - onClick={update} - data-tooltip={ - hasErrors - ? i18n.str`Please complete the marked fields` - : i18n.str`Confirm operation` - } - > - <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> - </div> + <div class="buttons is-right mt-4"> + <button + type="button" + class="button" + onClick={onBack} + data-tooltip="cancel operation" + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + + <ButtonBetterBulma + type="submit" + onClick={update} + data-tooltip={ + hasErrors + ? i18n.str`Please complete the marked fields` + : i18n.str`Confirm operation` + } + > + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </div> + </FormProvider> </div> <div class="column" /> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx @@ -79,7 +79,7 @@ function CommonUpdate( if (result.type === "fail") { switch (result.case) { case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } case HttpStatusCode.NotFound: { return <NotFoundPageOrAdminCreate />; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx @@ -272,26 +272,25 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { label={i18n.str`Http body`} tooltip={i18n.str`Body template used by the webhook.`} /> + <div class="buttons is-right mt-5"> + {onBack && ( + <button type="button" class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <ButtonBetterBulma + data-tooltip={ + hasErrors + ? i18n.str`Please complete the marked fields` + : i18n.str`Confirm operation` + } + type="submit" + onClick={create} + > + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </div> </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button type="button" class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <ButtonBetterBulma - data-tooltip={ - hasErrors - ? i18n.str`Please complete the marked fields` - : i18n.str`Confirm operation` - } - type="submit" - onClick={create} - > - <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> - </div> </div> <div class="column" /> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx @@ -193,6 +193,7 @@ function Table({ <td class="is-actions-cell right-sticky"> <div class="buttons is-right"> <ButtonBetterBulma + type="button" class="button is-danger is-small has-tooltip-left" data-tooltip={i18n.str`Delete selected webhook from the database`} onClick={deleteWebhook.withArgs(i.webhook_id)} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx @@ -50,7 +50,7 @@ export default function ListWebhooks({ onCreate, onSelect }: Props): VNode { return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx @@ -210,26 +210,25 @@ export function UpdatePage({ webhook, onConfirm, onBack }: Props): VNode { label={i18n.str`Body`} tooltip={i18n.str`Body template used by the webhook`} /> + <div class="buttons is-right mt-5"> + {onBack && ( + <button type="button" class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <ButtonBetterBulma + data-tooltip={ + hasErrors + ? i18n.str`Please complete the marked fields` + : i18n.str`Confirm operation` + } + onClick={update} + type="submit" + > + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </div> </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button type="button" class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <ButtonBetterBulma - data-tooltip={ - hasErrors - ? i18n.str`Please complete the marked fields` - : i18n.str`Confirm operation` - } - onClick={update} - type="submit" - > - <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> - </div> </div> </div> </section> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx @@ -60,7 +60,7 @@ export default function UpdateWebhook({ return <NotFoundPageOrAdminCreate />; } case HttpStatusCode.Unauthorized: { - return <LoginPage />; + return <LoginPage focus />; } default: { assertUnreachable(result); diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -38,11 +38,14 @@ import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { SolveMFAChallenges } from "../../components/SolveMFA.js"; import { useSessionContext } from "../../context/session.js"; +import { FormProvider } from "../../components/form/FormProvider.js"; +import { doAutoFocus } from "../../components/form/Input.js"; const TALER_SCREEN_ID = 79; interface Props { showCreateAccount?: boolean; + focus?: boolean; } export const TEMP_TEST_TOKEN = (description: TranslatedString) => @@ -60,7 +63,7 @@ export const FOREVER_REFRESHABLE_TOKEN = (description: TranslatedString) => }) as LoginTokenRequest; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; -export function LoginPage({ showCreateAccount }: Props): VNode { +export function LoginPage({ showCreateAccount, focus }: Props): VNode { const [password, setPassword] = useState(""); const { state, logIn, getInstanceForUsername, config } = useSessionContext(); @@ -114,6 +117,7 @@ export function LoginPage({ showCreateAccount }: Props): VNode { currentChallenge={mfa.pendingChallenge} onCompleted={retry} onCancel={mfa.doCancelChallenge} + focus /> ); } @@ -132,11 +136,11 @@ export function LoginPage({ showCreateAccount }: Props): VNode { <i18n.Translate>Login required</i18n.Translate> </p> </header> - <section - class="modal-card-body" - style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} - > - <form> + <FormProvider> + <section + class="modal-card-body" + style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} + > <div class="field is-horizontal"> <div class="field-label is-normal"> <label class="label"> @@ -155,11 +159,9 @@ export function LoginPage({ showCreateAccount }: Props): VNode { <input class="input" type="text" + ref={focus ? doAutoFocus : undefined} // placeholder={i18n.str`instance name`} name="username" - onKeyPress={(e) => - e.keyCode === 13 ? login.call() : null - } value={username} onInput={(e): void => setUsername(e?.currentTarget.value) @@ -189,9 +191,6 @@ export function LoginPage({ showCreateAccount }: Props): VNode { type={hidePassword ? "password" : "text"} // placeholder={i18n.str`current password`} name="token" - onKeyPress={(e) => - e.keyCode === 13 ? login.call() : null - } value={password} onInput={(e): void => setPassword(e?.currentTarget.value) @@ -218,35 +217,35 @@ export function LoginPage({ showCreateAccount }: Props): VNode { </div> </div> </div> - </form> - </section> - <footer - class="modal-card-foot " - style={{ - justifyContent: "space-between", - border: "1px solid", - borderTop: 0, - }} - > - {!config.have_self_provisioning ? ( - <div /> - ) : ( - <a - href={ - !username || username === "admin" - ? undefined - : `#/account/reset/${username}` - } - class="button " - disabled={!username || username === "admin"} - > - <i18n.Translate>Forgot password</i18n.Translate> - </a> - )} - <ButtonBetterBulma onClick={login} type="submit"> - <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> - </footer> + </section> + <footer + class="modal-card-foot " + style={{ + justifyContent: "space-between", + border: "1px solid", + borderTop: 0, + }} + > + {!config.have_self_provisioning ? ( + <div /> + ) : ( + <a + href={ + !username || username === "admin" + ? undefined + : `#/account/reset/${username}` + } + class="button " + disabled={!username || username === "admin"} + > + <i18n.Translate>Forgot password</i18n.Translate> + </a> + )} + <ButtonBetterBulma onClick={login} type="submit"> + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </footer> + </FormProvider> </div> {!showCreateAccount ? undefined : ( <div style={{ marginTop: 8 }}> diff --git a/packages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx @@ -96,7 +96,7 @@ export function ResetAccount({ { challengeIds }, ); - return forgot; + return forgot; }, hasErrors ? undefined : [value.password!, []], ); @@ -150,15 +150,15 @@ export function ResetAccount({ </i18n.Translate> </p> </header> - <section - class="modal-card-body" - style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} + <FormProvider<Form> + name="settings" + errors={errors} + object={value} + valueHandler={valueHandler} > - <FormProvider<Form> - name="settings" - errors={errors} - object={value} - valueHandler={valueHandler} + <section + class="modal-card-body" + style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} > <InputPassword<Form> label={i18n.str`New password`} @@ -170,23 +170,23 @@ export function ResetAccount({ inputType="password" name="repeat" /> - </FormProvider> - </section> - <footer - class="modal-card-foot " - style={{ - justifyContent: "space-between", - border: "1px solid", - borderTop: 0, - }} - > - <button type="button" class="button" onClick={onCancel}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - <ButtonBetterBulma type="submit" onClick={reset}> - <i18n.Translate>Reset</i18n.Translate> - </ButtonBetterBulma> - </footer> + </section> + <footer + class="modal-card-foot " + style={{ + justifyContent: "space-between", + border: "1px solid", + borderTop: 0, + }} + > + <button type="button" class="button" onClick={onCancel}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + <ButtonBetterBulma type="submit" onClick={reset}> + <i18n.Translate>Reset</i18n.Translate> + </ButtonBetterBulma> + </footer> + </FormProvider> </div> </div> </div> diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts @@ -2999,6 +2999,8 @@ export class TalerMerchantInstanceHttpClient { return opSuccessFromHttp(resp, codecForPotAddedResponse()); case HttpStatusCode.Unauthorized: return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: diff --git a/packages/web-util/src/components/Button.tsx b/packages/web-util/src/components/Button.tsx @@ -20,6 +20,7 @@ import { SafeHandlerTemplate, useTranslationContext, } from "../index.browser.js"; +import { doAutoFocus } from "./utils.js"; export interface ButtonHandler { onClick: (() => Promise<void>) | undefined; @@ -71,10 +72,12 @@ export function Button({ } type PropsBetter = Omit< - Omit<HTMLAttributes<HTMLButtonElement>, "onClick">, + Omit<Omit<HTMLAttributes<HTMLButtonElement>, "type">, "onClick">, "disabled" > & { + type: "button" | "submit"; onClick: SafeHandlerTemplate<any, any> | undefined; + focus?: boolean; }; /** * FIXME: removed deprecated and change for this one @@ -83,15 +86,16 @@ type PropsBetter = Omit< */ export function ButtonBetter({ children, + focus, onClick, ...rest }: PropsBetter): VNode { const [running, setRunning] = useState(false); return ( <button - type="button" {...rest} disabled={running || !onClick || !onClick.args} + ref={focus ? doAutoFocus : undefined} onClick={(e) => { e.preventDefault(); if (!onClick || !onClick.args) { @@ -107,33 +111,38 @@ export function ButtonBetter({ </button> ); } - +/** + * we should have a button-type and a submit-type + * submit tpye should not have focus sin the focus in the form + * submit should only be used on forms and there should be only one + */ // FIXME: we should stop using bulma css and remove all of this support export function ButtonBetterBulma({ children, + focus, onClick, ...rest -}: PropsBetter & {"data-tooltip"?: string,}): VNode { +}: PropsBetter & { "data-tooltip"?: string }): VNode { const [running, setRunning] = useState(false); return ( - <button - class="button is-success" - type="button" - {...rest as any} - disabled={running || !onClick || !onClick.args} - onClick={(e) => { - e.preventDefault(); - if (!onClick || !onClick.args) { - return; - } - setRunning(true); - onClick.call().finally(() => { - setRunning(false); - }); - }} - > - {running ? <Wait /> : children} - </button> + <button + class="button is-success" + {...rest} + ref={focus ? doAutoFocus : undefined} + disabled={running || !onClick || !onClick.args} + onClick={(e) => { + e.preventDefault(); + if (!onClick || !onClick.args) { + return; + } + setRunning(true); + onClick.call().finally(() => { + setRunning(false); + }); + }} + > + {running ? <Wait /> : children} + </button> ); }