commit 5c9f6d7a755a7ccb23371e2fd4bfad106fa18ce3 parent 12d0515bb8b21e500a3a5a292c532c8b440b73a5 Author: Sebastian <sebasjm@taler-systems.com> Date: Wed, 18 Mar 2026 08:54:00 -0300 fix #10894 Diffstat:
17 files changed, 558 insertions(+), 248 deletions(-)
diff --git a/packages/merchant-backoffice-ui/src/components/form/Input.tsx b/packages/merchant-backoffice-ui/src/components/form/Input.tsx @@ -49,7 +49,17 @@ export type SupportedAutocomplete = | "email" | "tel"; -interface Props<T> extends InputProps<T> { +type CompatibleHTMLInput = Omit< + Omit< + Omit< + Omit<h.JSX.HTMLAttributes<HTMLInputElement>, "readonly">, + "placeholder" + >, + "name" + >, + "label" +>; +interface Props<T> extends InputProps<T>, CompatibleHTMLInput { inputType?: SupportedTextInputType; expand?: boolean; toStr?: (v?: any) => string; @@ -80,7 +90,12 @@ export function InternalTextInputSwitch({ focus, hasError, ...rest -}: Props<any> & { hasError?: boolean }): VNode { +}: Props<any> & { + value?: string; + class?: string; + disabled?: boolean; + hasError?: boolean; +}): VNode { if (inputType === "multiline") { return ( <textarea diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx @@ -1,212 +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 { - Duration, - InternationalizationAPI, - TalerProtocolDuration, - TranslatedString, -} from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { formatDuration, intervalToDuration } from "date-fns"; -import { ComponentChildren, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { SimpleModal } from "../modal/index.js"; -import { DurationPicker } from "../picker/DurationPicker.js"; -import { InputProps, useField } from "./useField.js"; -import { Tooltip } from "../Tooltip.js"; - -const TALER_SCREEN_ID = 8; - -export interface Props<T> extends InputProps<T> { - expand?: boolean; - readonly?: boolean; - withForever?: boolean; - side?: ComponentChildren; - withoutClear?: boolean; - useProtocolDuration?: boolean; -} - -export function durationToString( - i18n: InternationalizationAPI, - d: Duration | undefined, -): TranslatedString { - if (!d) return "" as TranslatedString; - if (d.d_ms === "forever") return i18n.str`Forever`; - if (d.d_ms === undefined) { - throw Error( - `assertion error: duration should have a d_ms but got '${JSON.stringify( - d, - )}'`, - ); - } - return formatDuration(intervalToDuration({ start: 0, end: d.d_ms }), { - locale: { - formatDistance: (name, value) => { - switch (name) { - case "xMonths": - return i18n.str`${value}M`; - case "xYears": - return i18n.str`${value}Y`; - case "xDays": - return i18n.str`${value}d`; - case "xHours": - return i18n.str`${value}h`; - case "xMinutes": - return i18n.str`${value}min`; - case "xSeconds": - return i18n.str`${value}sec`; - default: - throw Error("not reached"); - } - }, - localize: { - day: () => "s", - month: () => "m", - ordinalNumber: () => "th", - dayPeriod: () => "p", - quarter: () => "w", - era: () => "e", - }, - }, - }) as TranslatedString; -} - -export function InputDuration<T>({ - name, - expand, - placeholder, - tooltip, - label, - help, - readonly, - withForever, - withoutClear, - side, - useProtocolDuration, -}: Props<keyof T>): VNode { - const [opened, setOpened] = useState(false); - const { i18n } = useTranslationContext(); - - const { error, required, value: anyValue, onChange } = useField<T>(name); - const value: Duration | undefined = - anyValue && anyValue.d_us !== undefined - ? Duration.fromTalerProtocolDuration(anyValue) - : anyValue; - const strValue = durationToString(i18n, value); - - return ( - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {required && ( - <span class="has-text-danger" style={{ marginLeft: 5 }}> - * - </span> - )} - {tooltip && ( - <Tooltip text={tooltip}> - <i class="icon mdi mdi-information" /> - </Tooltip> - )} - </label> - </div> - - <div class="field-body is-flex-grow-3"> - <div class="field"> - <div class="field has-addons"> - <p class={expand ? "control is-expanded " : "control "}> - <input - class="input" - type="text" - readonly - value={strValue} - placeholder={placeholder} - onClick={() => { - if (!readonly) setOpened(true); - }} - /> - </p> - <div - class="control" - onClick={() => { - if (!readonly) setOpened(true); - }} - > - <a class="button is-static"> - <i class="icon mdi mdi-clock" /> - </a> - </div> - </div> - {error && ( - <p class="help is-danger" style={{ fontSize: 16 }}> - {error} - </p> - )} - <span class="has-text-grey">{help}</span> - </div> - - {withForever && ( - <Tooltip text={i18n.str`Change the value to never`}> - <button - type="button" - class="button is-info mr-3" - onClick={() => onChange({ d_ms: "forever" } as any)} - > - <i18n.Translate>Forever</i18n.Translate> - </button> - </Tooltip> - )} - {!readonly && !withoutClear && ( - <Tooltip text={i18n.str`Change the value to empty`}> - <button - type="button" - class="button is-info mr-3" - onClick={() => onChange(undefined as any)} - > - <i18n.Translate>Clear</i18n.Translate> - </button> - </Tooltip> - )} - {side} - </div> - - {opened && ( - <SimpleModal onCancel={() => setOpened(false)}> - <DurationPicker - days - hours - minutes - value={!value || value.d_ms === "forever" ? 0 : value.d_ms} - onChange={(v) => { - let duration: any = { d_ms: v }; - if (useProtocolDuration === true) { - onChange(Duration.toTalerProtocolDuration(duration) as any); - } else { - onChange(duration); - } - }} - /> - </SimpleModal> - )} - </div> - ); -} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDurationDropdown.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDurationDropdown.tsx @@ -0,0 +1,281 @@ +/* + 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 { + assertUnreachable, + Duration, + InternationalizationAPI, + TranslatedString, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ComponentChildren, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Tooltip } from "../Tooltip.js"; +import { InternalTextInputSwitch } from "./Input.js"; +import { InputProps, useField } from "./useField.js"; + +const TALER_SCREEN_ID = 8; + +export interface Props<T> extends InputProps<T> { + expand?: boolean; + readonly?: boolean; + withForever?: boolean; + side?: ComponentChildren; + withoutClear?: boolean; + focus?: boolean; + useProtocolDuration?: boolean; +} + +function defaultToInputNumberString( + d: Duration | undefined, + unit: PosibleUnits, +): string | undefined { + if (!d) return undefined; + if (d.d_ms === "forever") return undefined; + switch (unit) { + case "days": + return String(Math.trunc(d.d_ms / (1000 * 60 * 60 * 24))); + case "hours": + return String(Math.trunc(d.d_ms / (1000 * 60 * 60))); + case "minutes": + return String(Math.trunc(d.d_ms / (1000 * 60))); + case "seconds": + return String(Math.trunc(d.d_ms / 1000)); + default: + assertUnreachable(unit); + } +} +function defaultFromInputToDuration( + value: string | undefined, + unit: PosibleUnits, +): Duration | undefined { + if (!value) return undefined; + const num = Number.parseInt(value, 10); + if (Number.isNaN(num)) return undefined; + + switch (unit) { + case "days": + return Duration.fromSpec({ days: num }); + case "hours": + return Duration.fromSpec({ hours: num }); + case "minutes": + return Duration.fromSpec({ minutes: num }); + case "seconds": + return Duration.fromSpec({ seconds: num }); + default: + assertUnreachable(unit); + } +} + +const UNIT_VALUES = ["days", "hours", "minutes", "seconds"] as const; +type PosibleUnits = (typeof UNIT_VALUES)[number]; + +function isDivisible(v: number, size: number) { + return v / size - Math.trunc(v / size) === 0; +} + +function bestInitialUnit(v: Duration | undefined): PosibleUnits { + if (v === undefined) { + return "seconds"; + } + if (v.d_ms === "forever") { + return "seconds"; + } + if (isDivisible(v.d_ms, 1000 * 60 * 60 * 24)) { + return "days"; + } + if (isDivisible(v.d_ms, 1000 * 60 * 60)) { + return "hours"; + } + if (isDivisible(v.d_ms, 1000 * 60)) { + return "minutes"; + } + if (isDivisible(v.d_ms, 1000)) { + return "seconds"; + } + return "seconds"; +} + +function translateUnits( + unit: PosibleUnits, + i18n: InternationalizationAPI, +): TranslatedString { + switch (unit) { + case "days": + return i18n.str`Days`; + case "hours": + return i18n.str`Hours`; + case "minutes": + return i18n.str`Minutes`; + case "seconds": + return i18n.str`Seconds`; + default: + assertUnreachable(unit); + } +} + +export function InputDurationDropdown<T>({ + name, + expand, + placeholder, + tooltip, + label, + help, + readonly, + withForever, + withoutClear, + focus, + side, + useProtocolDuration, +}: Props<keyof T>): VNode { + const { i18n } = useTranslationContext(); + const { error, required, value: anyValue, onChange } = useField<T>(name); + const value: Duration | undefined = + anyValue && anyValue.d_us !== undefined + ? Duration.fromTalerProtocolDuration(anyValue) + : anyValue; + + const initialUnit = bestInitialUnit(value); + + const [unit, setUnit] = useState<PosibleUnits>(initialUnit); + const [numStr, setNumStr] = useState<string>(); + + // const strValue = durationToString(i18n, value); + + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {required && ( + <span class="has-text-danger" style={{ marginLeft: 5 }}> + * + </span> + )} + {tooltip && ( + <Tooltip text={tooltip}> + <i class="icon mdi mdi-information" /> + </Tooltip> + )} + </label> + </div> + + <div class="field-body is-flex-grow-3"> + <div class="field" style={{ flexGrow: "initial" }}> + <InternalTextInputSwitch + class={error ? "input is-danger" : "input"} + label={undefined} + inputType={"numeric"} + placeholder={placeholder} + min={0} + cols={2} + readonly={readonly} + style={{ + textAlign: "right", + width: "6em", + }} + fromStr={(v) => (!v ? undefined : parseInt(v, 10))} + toStr={(v) => `${v}`} + disabled={readonly} + focus={focus} + name={String(name)} + value={defaultToInputNumberString(value, unit)} + onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void => { + const d = e.currentTarget.value; + setNumStr(d); + const duration = defaultFromInputToDuration(d, unit); + onChange( + (useProtocolDuration && duration + ? Duration.toTalerProtocolDuration(duration) + : duration) as any, + ); + }} + /> + </div> + <div class="field"> + <p class={expand ? "control is-expanded select" : "control select "}> + <select + class={error ? "select is-danger" : "select"} + name={String(name)} + disabled={readonly} + readonly={readonly} + onChange={(e) => { + const u = e.currentTarget.value as PosibleUnits; + setUnit(u); + const duration = defaultFromInputToDuration(numStr, u); + onChange( + (useProtocolDuration && duration + ? Duration.toTalerProtocolDuration(duration) + : duration) as any, + ); + }} + > + {placeholder && <option>{placeholder}</option>} + {UNIT_VALUES.map((v, i) => { + return ( + <option key={i} value={v} selected={unit === v}> + {translateUnits(v, i18n)} + </option> + ); + })} + </select> + </p> + + <span class="has-text-grey">{help}</span> + {error && ( + <p class="help is-danger" style={{ fontSize: 16 }}> + {error} + </p> + )} + </div> + + {withForever && ( + <Tooltip text={i18n.str`Change the value to never`}> + <button + type="button" + class="button is-info mr-3" + onClick={() => { + if (useProtocolDuration) { + onChange({ d_us: "forever" } as any); + } else { + onChange({ d_ms: "forever" } as any); + } + }} + > + <i18n.Translate>Forever</i18n.Translate> + </button> + </Tooltip> + )} + {!readonly && !withoutClear && ( + <Tooltip text={i18n.str`Change the value to empty`}> + <button + type="button" + class="button is-info mr-3" + onClick={() => onChange(undefined as any)} + > + <i18n.Translate>Clear</i18n.Translate> + </button> + </Tooltip> + )} + {side} + </div> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDurationSelector.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDurationSelector.tsx @@ -0,0 +1,212 @@ +/* + 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 { + Duration, + InternationalizationAPI, + TalerProtocolDuration, + TranslatedString, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { formatDuration, intervalToDuration } from "date-fns"; +import { ComponentChildren, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { SimpleModal } from "../modal/index.js"; +import { DurationPicker } from "../picker/DurationPicker.js"; +import { InputProps, useField } from "./useField.js"; +import { Tooltip } from "../Tooltip.js"; + +const TALER_SCREEN_ID = 8; + +export interface Props<T> extends InputProps<T> { + expand?: boolean; + readonly?: boolean; + withForever?: boolean; + side?: ComponentChildren; + withoutClear?: boolean; + useProtocolDuration?: boolean; +} + +export function durationToString( + i18n: InternationalizationAPI, + d: Duration | undefined, +): TranslatedString { + if (!d) return "" as TranslatedString; + if (d.d_ms === "forever") return i18n.str`Forever`; + if (d.d_ms === undefined) { + throw Error( + `assertion error: duration should have a d_ms but got '${JSON.stringify( + d, + )}'`, + ); + } + return formatDuration(intervalToDuration({ start: 0, end: d.d_ms }), { + locale: { + formatDistance: (name, value) => { + switch (name) { + case "xMonths": + return i18n.str`${value}M`; + case "xYears": + return i18n.str`${value}Y`; + case "xDays": + return i18n.str`${value}d`; + case "xHours": + return i18n.str`${value}h`; + case "xMinutes": + return i18n.str`${value}min`; + case "xSeconds": + return i18n.str`${value}sec`; + default: + throw Error("not reached"); + } + }, + localize: { + day: () => "s", + month: () => "m", + ordinalNumber: () => "th", + dayPeriod: () => "p", + quarter: () => "w", + era: () => "e", + }, + }, + }) as TranslatedString; +} + +export function InputDurationSelector<T>({ + name, + expand, + placeholder, + tooltip, + label, + help, + readonly, + withForever, + withoutClear, + side, + useProtocolDuration, +}: Props<keyof T>): VNode { + const [opened, setOpened] = useState(false); + const { i18n } = useTranslationContext(); + + const { error, required, value: anyValue, onChange } = useField<T>(name); + const value: Duration | undefined = + anyValue && anyValue.d_us !== undefined + ? Duration.fromTalerProtocolDuration(anyValue) + : anyValue; + const strValue = durationToString(i18n, value); + + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {required && ( + <span class="has-text-danger" style={{ marginLeft: 5 }}> + * + </span> + )} + {tooltip && ( + <Tooltip text={tooltip}> + <i class="icon mdi mdi-information" /> + </Tooltip> + )} + </label> + </div> + + <div class="field-body is-flex-grow-3"> + <div class="field"> + <div class="field has-addons"> + <p class={expand ? "control is-expanded " : "control "}> + <input + class="input" + type="text" + readonly + value={strValue} + placeholder={placeholder} + onClick={() => { + if (!readonly) setOpened(true); + }} + /> + </p> + <div + class="control" + onClick={() => { + if (!readonly) setOpened(true); + }} + > + <a class="button is-static"> + <i class="icon mdi mdi-clock" /> + </a> + </div> + </div> + {error && ( + <p class="help is-danger" style={{ fontSize: 16 }}> + {error} + </p> + )} + <span class="has-text-grey">{help}</span> + </div> + + {withForever && ( + <Tooltip text={i18n.str`Change the value to never`}> + <button + type="button" + class="button is-info mr-3" + onClick={() => onChange({ d_ms: "forever" } as any)} + > + <i18n.Translate>Forever</i18n.Translate> + </button> + </Tooltip> + )} + {!readonly && !withoutClear && ( + <Tooltip text={i18n.str`Change the value to empty`}> + <button + type="button" + class="button is-info mr-3" + onClick={() => onChange(undefined as any)} + > + <i18n.Translate>Clear</i18n.Translate> + </button> + </Tooltip> + )} + {side} + </div> + + {opened && ( + <SimpleModal onCancel={() => setOpened(false)}> + <DurationPicker + days + hours + minutes + value={!value || value.d_ms === "forever" ? 0 : value.d_ms} + onChange={(v) => { + let duration: any = { d_ms: v }; + if (useProtocolDuration === true) { + onChange(Duration.toTalerProtocolDuration(duration) as any); + } else { + onChange(duration); + } + }} + /> + </SimpleModal> + )} + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx @@ -25,7 +25,7 @@ import { useSessionContext } from "../../context/session.js"; import { UIElement } from "../../hooks/preference.js"; import { Entity } from "../../paths/admin/create/CreatePage.js"; import { Input } from "../form/Input.js"; -import { InputDuration } from "../form/InputDuration.js"; +import { InputDurationSelector } from "../form/InputDurationSelector.js"; import { InputGroup } from "../form/InputGroup.js"; import { InputImage } from "../form/InputImage.js"; import { InputLocation } from "../form/InputLocation.js"; @@ -35,6 +35,7 @@ import { FragmentPersonaFlag } from "../menu/SideBar.js"; import { RoundingInterval } from "@gnu-taler/taler-util"; import { InputSelector } from "../form/InputSelector.js"; import { Tooltip } from "../Tooltip.js"; +import { InputDurationDropdown } from "../form/InputDurationDropdown.js"; const TALER_SCREEN_ID = 16; @@ -127,7 +128,7 @@ export function DefaultInstanceFormFields({ tooltip={i18n.str`These values will be used if they are not overridden during order creation.`} name="" > - <InputDuration<Entity> + <InputDurationDropdown<Entity> name="default_pay_delay" label={i18n.str`Payment delay`} tooltip={i18n.str`Time customers have to pay an order before the offer expires by default.`} @@ -141,7 +142,6 @@ export function DefaultInstanceFormFields({ type="button" class="button is-info mr-3" onClick={() => { - console.log("dale"); setDefaultPayDelay(); }} > @@ -151,7 +151,7 @@ export function DefaultInstanceFormFields({ } /> - <InputDuration<Entity> + <InputDurationDropdown<Entity> name="default_refund_delay" label={i18n.str`Refund delay`} tooltip={i18n.str`Time merchants have to refund an order.`} @@ -165,7 +165,6 @@ export function DefaultInstanceFormFields({ type="button" class="button is-info mr-3" onClick={() => { - console.log("dale"); setDefaultRefundDelay(); }} > @@ -175,7 +174,7 @@ export function DefaultInstanceFormFields({ } /> - <InputDuration<Entity> + <InputDurationDropdown<Entity> name="default_wire_transfer_delay" label={i18n.str`Wire transfer delay`} tooltip={i18n.str`Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.`} @@ -189,7 +188,6 @@ export function DefaultInstanceFormFields({ type="button" class="button is-info mr-3" onClick={() => { - console.log("dale"); setDefaultWireDelay(); }} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/CreatePage.tsx @@ -41,7 +41,7 @@ import { TalerForm, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputDurationSelector } from "../../../../components/form/InputDurationSelector.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; import { SolveMFAChallenges } from "../../../../components/SolveMFA.js"; @@ -51,6 +51,7 @@ import { getAvailableForPersona } from "../../../../components/menu/SideBar.js"; import { UIElement, usePreference } from "../../../../hooks/preference.js"; import { Tooltip } from "../../../../components/Tooltip.js"; import { maybeTryFirstMFA } from "../../accounts/create/CreatePage.js"; +import { InputDurationDropdown } from "../../../../components/form/InputDurationDropdown.js"; const TALER_SCREEN_ID = 29; @@ -189,7 +190,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { help={i18n.str`Helps you remember where this access token is being used before deleting it.`} /> - <InputDuration<Entity> + <InputDurationDropdown<Entity> name="duration" label={i18n.str`Duration`} withForever diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx @@ -50,7 +50,7 @@ import { import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDate } from "../../../../components/form/InputDate.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputDurationSelector } from "../../../../components/form/InputDurationSelector.js"; import { InputGroup } from "../../../../components/form/InputGroup.js"; import { InputLocation } from "../../../../components/form/InputLocation.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; @@ -66,6 +66,7 @@ import { undefinedIfEmpty } from "../../../../utils/table.js"; import { FragmentPersonaFlag } from "../../../../components/menu/SideBar.js"; import { Tooltip } from "../../../../components/Tooltip.js"; import { MissingBankAccountsWarning } from "../../accounts/list/index.js"; +import { InputDurationDropdown } from "../../../../components/form/InputDurationDropdown.js"; const TALER_SCREEN_ID = 42; @@ -597,7 +598,7 @@ export function CreatePage({ errors?.payments?.pay_delay !== undefined } > - <InputDuration + <InputDurationDropdown name="payments.pay_delay" label={i18n.str`Payment time`} help={<DeadlineHelp duration={payDelay} />} @@ -629,7 +630,7 @@ export function CreatePage({ point={UIElement.option_advanceOrderCreation} showAnywayIf={errors?.payments?.refund_delay !== undefined} > - <InputDuration + <InputDurationDropdown name="payments.refund_delay" label={i18n.str`Refund time`} help={<DeadlineHelp duration={refundDelay} />} @@ -664,7 +665,7 @@ export function CreatePage({ errors?.payments?.wire_transfer_delay !== undefined } > - <InputDuration + <InputDurationDropdown name="payments.wire_transfer_delay" label={i18n.str`Wire transfer time`} help={<DeadlineHelp duration={wireDelay} />} @@ -698,7 +699,7 @@ export function CreatePage({ errors?.payments?.auto_refund_delay !== undefined } > - <InputDuration + <InputDurationDropdown name="payments.auto_refund_delay" label={i18n.str`Auto-refund time`} help={<DeadlineHelp duration={autoRefundDelay} />} 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 @@ -42,7 +42,7 @@ import { FormProvider } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDate } from "../../../../components/form/InputDate.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputDurationSelector } from "../../../../components/form/InputDurationSelector.js"; import { InputGroup } from "../../../../components/form/InputGroup.js"; import { InputLocation } from "../../../../components/form/InputLocation.js"; import { TextField } from "../../../../components/form/TextField.js"; @@ -59,6 +59,7 @@ import { Event, Timeline } from "./Timeline.js"; import { ConfirmModal } from "../../../../components/modal/index.js"; import { Tooltip } from "../../../../components/Tooltip.js"; import { QR } from "../../../../components/exception/QR.js"; +import { InputDurationDropdown } from "../../../../components/form/InputDurationDropdown.js"; const TALER_SCREEN_ID = 44; @@ -151,7 +152,7 @@ function ContractTerms_V0({ value }: { value: CT0 }) { label={i18n.str`Wire transfer deadline`} tooltip={i18n.str`Transfer deadline for the exchange`} /> - <InputDuration<CT0> + <InputDurationDropdown<CT0> readonly name="auto_refund" label={i18n.str`Auto-refund delay`} @@ -250,7 +251,7 @@ function ContractTerms_V1({ value }: { value: CT1 }) { > <InputLocation name="delivery_location" /> </InputGroup> - <InputDuration<CT1> + <InputDurationDropdown<CT1> readonly name="auto_refund" label={i18n.str`Auto-refund delay`} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/create/CreatePage.tsx @@ -37,7 +37,7 @@ import { FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputDurationSelector } from "../../../../components/form/InputDurationSelector.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { useSessionContext } from "../../../../context/session.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; @@ -181,13 +181,13 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { values={config.report_generators} /> - <InputDuration<Entity> + <InputDurationSelector<Entity> name="report_frequency" label={i18n.str`Report frequency`} useProtocolDuration /> - <InputDuration<Entity> + <InputDurationSelector<Entity> name="report_frequency_shift" label={i18n.str`Report frequency shift`} useProtocolDuration 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 @@ -35,7 +35,7 @@ import { import { Fragment, h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; import { useSessionContext } from "../../../../context/session.js"; -import { durationToString } from "../../../../components/form/InputDuration.js"; +import { durationToString } from "../../../../components/form/InputDurationSelector.js"; import { Tooltip } from "../../../../components/Tooltip.js"; const TALER_SCREEN_ID = 38; 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 @@ -42,8 +42,9 @@ import { WithId } from "../../../../declaration.js"; import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputDurationSelector } from "../../../../components/form/InputDurationSelector.js"; import { Tooltip } from "../../../../components/Tooltip.js"; +import { InputDurationDropdown } from "../../../../components/form/InputDurationDropdown.js"; const TALER_SCREEN_ID = 39; @@ -229,13 +230,13 @@ export function UpdatePage({ report, onUpdated, onBack }: Props): VNode { values={config.report_generators} /> - <InputDuration<Entity> + <InputDurationDropdown<Entity> name="report_frequency" label={i18n.str`Report frequency`} useProtocolDuration /> - <InputDuration<Entity> + <InputDurationDropdown<Entity> name="report_frequency_shift" label={i18n.str`Report frequency shift`} useProtocolDuration 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 @@ -43,7 +43,7 @@ import { } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputDurationSelector } from "../../../../components/form/InputDurationSelector.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputToggle } from "../../../../components/form/InputToggle.js"; @@ -57,6 +57,7 @@ import { useSessionContext } from "../../../../context/session.js"; import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; import { UIElement } from "../../../../hooks/preference.js"; import { Tooltip } from "../../../../components/Tooltip.js"; +import { InputDurationDropdown } from "../../../../components/form/InputDurationDropdown.js"; const TALER_SCREEN_ID = 61; @@ -288,7 +289,7 @@ export function CreatePage({ <FragmentPersonaFlag point={UIElement.option_paymentTimeoutOnTemplate} > - <InputDuration<Entity> + <InputDurationDropdown<Entity> name="pay_duration" label={i18n.str`Payment timeout`} help="" 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 @@ -44,7 +44,7 @@ import { FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputDurationSelector } from "../../../../components/form/InputDurationSelector.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputToggle } from "../../../../components/form/InputToggle.js"; @@ -60,6 +60,7 @@ import { FragmentPersonaFlag, } from "../../../../components/menu/SideBar.js"; import { Tooltip } from "../../../../components/Tooltip.js"; +import { InputDurationDropdown } from "../../../../components/form/InputDurationDropdown.js"; const TALER_SCREEN_ID = 65; @@ -348,7 +349,7 @@ export function UpdatePage({ template, onUpdated, onBack }: Props): VNode { <FragmentPersonaFlag point={UIElement.option_paymentTimeoutOnTemplate} > - <InputDuration<Entity> + <InputDurationDropdown<Entity> name="pay_duration" label={i18n.str`Payment timeout`} help="" 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 @@ -43,7 +43,7 @@ import { FormProvider, } from "../../../../components/form/FormProvider.js"; import { InputDate } from "../../../../components/form/InputDate.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputDurationSelector } from "../../../../components/form/InputDurationSelector.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; import { useSessionContext } from "../../../../context/session.js"; @@ -53,6 +53,7 @@ import { } from "../../../../hooks/preference.js"; import { Input } from "../../../../components/form/Input.js"; import { Tooltip } from "../../../../components/Tooltip.js"; +import { InputDurationDropdown } from "../../../../components/form/InputDurationDropdown.js"; const TALER_SCREEN_ID = 67; @@ -204,7 +205,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { withoutClear useProtocolTimestamp /> - <InputDuration<Entity> + <InputDurationDropdown<Entity> name="duration" label={i18n.str`Duration`} tooltip={i18n.str`How long the coupon/subscription remains valid after being activated.`} @@ -219,7 +220,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { useProtocolDuration /> - <InputDuration<Entity> + <InputDurationDropdown<Entity> name="validity_granularity" label={i18n.str`Validity Granularity`} tooltip={i18n.str`Rounds the validity to a specific unit of time (like day, hour, minute).`} 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 @@ -40,7 +40,7 @@ import { } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputDate } from "../../../../components/form/InputDate.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputDurationSelector } from "../../../../components/form/InputDurationSelector.js"; import { useSessionContext } from "../../../../context/session.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { @@ -49,6 +49,7 @@ import { } from "../../../../hooks/preference.js"; import { addDays, addMonths, endOfMonth, format, startOfMonth } from "date-fns"; import { Tooltip } from "../../../../components/Tooltip.js"; +import { InputDurationDropdown } from "../../../../components/form/InputDurationDropdown.js"; const TALER_SCREEN_ID = 70; @@ -192,7 +193,7 @@ export function UpdatePage({ onUpdated, onBack, tokenFamily }: Props) { withNever useProtocolTimestamp /> - <InputDuration<Entity> + <InputDurationDropdown<Entity> name="duration" label={i18n.str`Duration`} tooltip={i18n.str`How long the coupon/subscription remains valid after being activated.`} diff --git a/packages/merchant-backoffice-ui/src/scss/_aside.scss b/packages/merchant-backoffice-ui/src/scss/_aside.scss @@ -141,7 +141,7 @@ aside.aside { } html.has-aside-mobile-transition { body { - overflow-x: hidden; + // overflow-x: hidden; } body, nav.navbar { diff --git a/packages/merchant-backoffice-ui/src/scss/main.scss b/packages/merchant-backoffice-ui/src/scss/main.scss @@ -201,3 +201,11 @@ input.mfa-code:focus { input.mfa-code[type=number] { -moz-appearance: textfield; } +.modal-card-body { + -webkit-overflow-scrolling: touch; + background-color: white; + flex-grow: 1; + flex-shrink: 1; + overflow: initial; + padding: 20px; +}