taler-typescript-core

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

commit c875ab428e09992c5d010aee3b0ac2a2b640dfed
parent 346f2d8cfd4248524f41b2ad93301c98c91b5834
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Wed, 25 Mar 2026 16:13:00 -0300

fix input duration

Diffstat:
Mpackages/merchant-backoffice-ui/src/components/form/InputDurationDropdown.tsx | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
1 file changed, 55 insertions(+), 24 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDurationDropdown.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDurationDropdown.tsx @@ -26,7 +26,7 @@ import { } 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 { useEffect, useState } from "preact/hooks"; import { Tooltip } from "../Tooltip.js"; import { InternalTextInputSwitch } from "./Input.js"; import { InputProps, useField } from "./useField.js"; @@ -45,11 +45,13 @@ export interface Props<T> extends InputProps<T> { function defaultToInputNumberString( d: Duration | undefined, - unit: PosibleUnits, + unit: PosibleUnits | undefined, ): string | undefined { - if (!d) return undefined; - if (d.d_ms === "forever") return undefined; + if (!d) return ""; + if (d.d_ms === "forever") return "Forever"; switch (unit) { + case undefined: + return ""; case "days": return String(Math.trunc(d.d_ms / (1000 * 60 * 60 * 24))); case "hours": @@ -58,19 +60,23 @@ function defaultToInputNumberString( return String(Math.trunc(d.d_ms / (1000 * 60))); case "seconds": return String(Math.trunc(d.d_ms / 1000)); + case "forever": + return ""; default: assertUnreachable(unit); } } function defaultFromInputToDuration( value: string | undefined, - unit: PosibleUnits, + unit: PosibleUnits | undefined, ): Duration | undefined { if (!value) return undefined; const num = Number.parseInt(value, 10); if (Number.isNaN(num)) return undefined; switch (unit) { + case undefined: + return undefined; case "days": return Duration.fromSpec({ days: num }); case "hours": @@ -79,24 +85,26 @@ function defaultFromInputToDuration( return Duration.fromSpec({ minutes: num }); case "seconds": return Duration.fromSpec({ seconds: num }); + case "forever": + return { d_ms: "forever" }; default: assertUnreachable(unit); } } -const UNIT_VALUES = ["days", "hours", "minutes", "seconds"] as const; +const UNIT_VALUES = ["forever", "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 { +function bestInitialUnit(v: Duration | undefined): PosibleUnits | undefined { if (v === undefined) { - return "seconds"; + return undefined; } if (v.d_ms === "forever") { - return "seconds"; + return "forever"; } if (isDivisible(v.d_ms, 1000 * 60 * 60 * 24)) { return "days"; @@ -114,10 +122,12 @@ function bestInitialUnit(v: Duration | undefined): PosibleUnits { } function translateUnits( - unit: PosibleUnits, + unit: PosibleUnits | undefined, i18n: InternationalizationAPI, ): TranslatedString { switch (unit) { + case undefined: + return i18n.str`Select one...`; case "days": return i18n.str`Days`; case "hours": @@ -126,6 +136,8 @@ function translateUnits( return i18n.str`Minutes`; case "seconds": return i18n.str`Seconds`; + case "forever": + return i18n.str`Forever`; default: assertUnreachable(unit); } @@ -154,9 +166,17 @@ export function InputDurationDropdown<T>({ const initialUnit = bestInitialUnit(value); - const [unit, setUnit] = useState<PosibleUnits>(initialUnit); - const [numStr, setNumStr] = useState<string | undefined>(defaultToInputNumberString(value, unit)); + const [unit, setUnit] = useState<PosibleUnits | undefined>(initialUnit); + const [numStr, setNumStr] = useState<string | undefined>( + defaultToInputNumberString(value, unit), + ); + useEffect(() => { + if (value === undefined && !numStr) return + const nu = bestInitialUnit(value) + setUnit(nu) + setNumStr(defaultToInputNumberString(value, nu)) + },[value]) // const strValue = durationToString(i18n, value); return ( @@ -193,14 +213,14 @@ export function InputDurationDropdown<T>({ }} fromStr={(v) => (!v ? undefined : parseInt(v, 10))} toStr={(v) => `${v}`} - disabled={readonly} + disabled={readonly || unit === "forever"} focus={focus} name={String(name)} value={numStr} onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void => { - const d = e.currentTarget.value; - setNumStr(d); - const duration = defaultFromInputToDuration(d, unit); + const v = e.currentTarget.value; + setNumStr(v); + const duration = defaultFromInputToDuration(v, unit); onChange( (useProtocolDuration && duration ? Duration.toTalerProtocolDuration(duration) @@ -208,8 +228,8 @@ export function InputDurationDropdown<T>({ ); }} /> - </div> - <div class="field"> + {/* </div> + <div class="field"> */} <p class={expand ? "control is-expanded select" : "control select "}> <select class={error ? "select is-danger" : "select"} @@ -220,6 +240,9 @@ export function InputDurationDropdown<T>({ const u = e.currentTarget.value as PosibleUnits; setUnit(u); const duration = defaultFromInputToDuration(numStr, u); + if (!duration || duration.d_ms === "forever") { + setNumStr("") + } onChange( (useProtocolDuration && duration ? Duration.toTalerProtocolDuration(duration) @@ -229,21 +252,29 @@ export function InputDurationDropdown<T>({ > {placeholder && <option>{placeholder}</option>} {UNIT_VALUES.map((v, i) => { + if (!withForever && v === "forever") { + return undefined; + } return ( <option key={i} value={v} selected={unit === v}> {translateUnits(v, i18n)} </option> ); })} + <option key="unslected" value="" selected={unit === undefined}> + {translateUnits(undefined, i18n)} + </option> </select> </p> - <span class="has-text-grey">{help}</span> - {error && ( - <p class="help is-danger" style={{ fontSize: 16 }}> - {error} - </p> - )} + <div> + <span class="has-text-grey">{help}</span> + {error && ( + <p class="help is-danger" style={{ fontSize: 16 }}> + {error} + </p> + )} + </div> </div> {withForever && (