taler-typescript-core

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

commit f586af103f68c67f763c01c105fe640ce0d34aae
parent 696482d3bd5bf032ff002dd35e95ad24187f08d2
Author: Sebastian <sebasjm@gmail.com>
Date:   Fri, 24 Jan 2025 12:18:46 -0300

input duration

Diffstat:
Mpackages/aml-backoffice-ui/src/pages/Dashboard.tsx | 8++++----
Mpackages/taler-util/src/time.ts | 29+++++++++++++++--------------
Mpackages/web-util/src/components/index.ts | 2+-
Mpackages/web-util/src/components/utils.ts | 24++++++++++++++++++++++++
Mpackages/web-util/src/forms/field-types.ts | 7+++++++
Mpackages/web-util/src/forms/fields/InputDuration.stories.tsx | 8++++----
Mpackages/web-util/src/forms/fields/InputDuration.tsx | 176++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Apackages/web-util/src/forms/fields/InputDurationText.stories.tsx | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/web-util/src/forms/fields/InputDurationText.tsx | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/web-util/src/forms/fields/InputInteger.stories.tsx | 27+++++++++++++++++++++++++++
Mpackages/web-util/src/forms/fields/InputLine.tsx | 16+++++++++++++---
Mpackages/web-util/src/forms/forms-types.ts | 11+++++++++++
Mpackages/web-util/src/forms/forms-utils.ts | 13+++++++++++++
Mpackages/web-util/src/forms/index.stories.ts | 1+
14 files changed, 435 insertions(+), 50 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/pages/Dashboard.tsx b/packages/aml-backoffice-ui/src/pages/Dashboard.tsx @@ -95,7 +95,7 @@ function EventMetrics({ <div class="sm:flex sm:items-center mb-4"> <div class="sm:flex-auto"> <h1 class="text-base font-semibold leading-6 text-gray-900"> - <i18n.Translate>Events</i18n.Translate> + <i18n.Translate>Statistics</i18n.Translate> </h1> </div> </div> @@ -104,7 +104,7 @@ function EventMetrics({ <div class="w-full flex justify-between"> <h1 class="text-base text-gray-900 mt-5"> - {i18n.str`Trading volume from ${getDateStringForTimeframe( + {i18n.str`Events from ${getDateStringForTimeframe( params.current.start, metricType, dateLocale, @@ -258,13 +258,13 @@ function MetricValueNumber({ <Fragment> <dd class="mt-1 block "> <div class="flex justify-start text-2xl items-baseline font-semibold text-indigo-600"> - {!current ? "-" : current} + {!current ? 0 : current} </div> <div class="flex flex-col"> <div class="flex justify-end items-baseline text-2xl font-semibold text-indigo-600"> <small class="ml-2 text-sm font-medium text-gray-500"> <i18n.Translate>previous</i18n.Translate>{" "} - {!previous ? "-" : previous} + {!previous ? 0 : previous} </small> </div> {!!rate && ( diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts @@ -304,25 +304,26 @@ export namespace Duration { minutes: number; hours: number; days: number; + month: number; + years: number; } | undefined { if (d_ms === "forever") return undefined; - let time = d_ms; - const millis = d_ms % SECONDS; - time -= millis; - const s = time % MINUTES; - time -= s; - const m = time % HOURS; - time -= m; - const h = time % DAYS; - time -= h; - const d = time; + const ms = d_ms > 0 ? d_ms : 0; + const Y_rest = ms % YEARS; + const M_rest = Y_rest % MONTHS; + const D_rest = M_rest % DAYS; + const h_rest = D_rest % HOURS; + const m_rest = h_rest % MINUTES; + const millis = m_rest % SECONDS; return { - seconds: s / SECONDS, - minutes: m / MINUTES, - hours: h / HOURS, - days: d / DAYS, + years: (ms - Y_rest) / YEARS, + month: (Y_rest - M_rest) / MONTHS, + days: (M_rest - D_rest) / DAYS, + hours: (D_rest - h_rest) / HOURS, + minutes: (h_rest - m_rest) / MINUTES, + seconds: (m_rest - millis) / SECONDS, }; } diff --git a/packages/web-util/src/components/index.ts b/packages/web-util/src/components/index.ts @@ -1,5 +1,5 @@ export * as utils from "./utils.js"; -export { onComponentUnload } from "./utils.js"; +export { onComponentUnload, preconnectAs, Preconnect } from "./utils.js"; export * from "./Attention.js"; export * from "./CopyButton.js"; export * from "./ErrorLoading.js"; diff --git a/packages/web-util/src/components/utils.ts b/packages/web-util/src/components/utils.ts @@ -83,6 +83,30 @@ export function onComponentUnload(callback: () => void) { }, []); } +const ownerDocument = typeof document === "undefined" ? null : document; +const preconnectsSet: Set<string> = new Set(); + +export type Preconnect = { + rel: "preconnect" | "dns-prefetch"; + href: string; + crossOrigin: string; +}; + +export function preconnectAs(pre: Preconnect[]) { + if (ownerDocument) { + pre.forEach(({ rel, href, crossOrigin }) => { + const key = `${rel}${href}${crossOrigin}`; + if (preconnectsSet.has(key)) return; + preconnectsSet.add(key); + const instance = ownerDocument.createElement("link"); + instance.setAttribute("rel", rel); + instance.setAttribute("crossOrigin", crossOrigin); + instance.setAttribute("href", href); + ownerDocument.head.appendChild(instance); + }); + } +} + /** * * @param obj VNode diff --git a/packages/web-util/src/forms/field-types.ts b/packages/web-util/src/forms/field-types.ts @@ -17,6 +17,7 @@ import { InputTextArea } from "./fields/InputTextArea.js"; import { InputToggle } from "./fields/InputToggle.js"; import { Group } from "./Group.js"; import { HtmlIframe } from "./HtmlIframe.js"; +import { InputDurationText } from "./fields/InputDurationText.js"; /** * Constrain the type with the ui props */ @@ -39,6 +40,7 @@ type FieldType<T extends object = any, K extends keyof T = any> = { toggle: Parameters<typeof InputToggle<T, K>>[0]; amount: Parameters<typeof InputAmount<T, K>>[0]; duration: Parameters<typeof InputDuration<T, K>>[0]; + durationText: Parameters<typeof InputDurationText<T, K>>[0]; }; /** @@ -77,6 +79,10 @@ export type UIFormField = | { type: "duration"; properties: FieldType["duration"]; + } + | { + type: "durationText"; + properties: FieldType["durationText"]; }; export type FieldComponentFunction<key extends keyof FieldType> = ( @@ -118,4 +124,5 @@ export const UIFormConfiguration: UIFormFieldMap = { //@ts-ignore amount: InputAmount, duration: InputDuration, + durationText: InputDurationText, }; diff --git a/packages/web-util/src/forms/fields/InputDuration.stories.tsx b/packages/web-util/src/forms/fields/InputDuration.stories.tsx @@ -33,10 +33,10 @@ type TargetObject = { }; const initial: TargetObject = { time: Duration.fromSpec({ - days: 1, - hours: 2, - minutes: 3, - seconds: 4, + days: 29, + hours: 23, + minutes: 59, + seconds: 59, }), }; diff --git a/packages/web-util/src/forms/fields/InputDuration.tsx b/packages/web-util/src/forms/fields/InputDuration.tsx @@ -4,7 +4,7 @@ import { UIFormProps } from "../FormProvider.js"; import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; import { InputWrapper } from "./InputLine.js"; import { Duration } from "@gnu-taler/taler-util"; -import { useEffect, useState } from "preact/hooks"; +import { useEffect, useRef, useState } from "preact/hooks"; export function InputDuration<T extends object, K extends keyof T>( props: UIFormProps<T, K>, @@ -14,26 +14,75 @@ export function InputDuration<T extends object, K extends keyof T>( const { value, onChange, state, error } = props.handler ?? noHandlerPropsAndNoContextForField(props.name); - const sd = !value ? undefined : Duration.toSpec(value as Duration); - const [days, setDays] = useState(sd?.days ?? 0); - const [hours, setHours] = useState(sd?.hours ?? 0); - const [minutes, setMinutes] = useState(sd?.minutes ?? 0); - const [seconds, setSeconds] = useState(sd?.seconds ?? 0); + const specDuration = !value ? undefined : Duration.toSpec(value as Duration); + // const [seconds, setSeconds] = useState(sd?.seconds ?? 0); + // const [hours, setHours] = useState(sd?.hours ?? 0); + // const [minutes, setMinutes] = useState(sd?.minutes ?? 0); + // const [days, setDays] = useState(sd?.days ?? 0); + // const [months, setMonths] = useState(sd?.month ?? 0); + // const [years, setYears] = useState(sd?.years ?? 0); - useEffect(() => { - onChange( - Duration.fromSpec({ - days, - hours, - minutes, - seconds, - }), - ); - }, [days, hours, minutes, seconds]); + const secondsRef = useRef<HTMLInputElement>(null); + const hoursRef = useRef<HTMLInputElement>(null); + const minutesRef = useRef<HTMLInputElement>(null); + const daysRef = useRef<HTMLInputElement>(null); + const monthsRef = useRef<HTMLInputElement>(null); + const yearsRef = useRef<HTMLInputElement>(null); + + // useEffect(() => { + // onChange( + // Duration.fromSpec({ + // days, + // hours, + // minutes, + // seconds, + // months, + // years, + // }), + // ); + // }, [days, hours, minutes, seconds, months, years]); const fromString: (s: string) => any = converter?.fromStringUI ?? defaultFromString; const toString: (s: any) => string = converter?.toStringUI ?? defaultToString; + const strSeconds = toString(specDuration?.seconds ?? 0) ?? ""; + const strHours = toString(specDuration?.hours ?? 0) ?? ""; + const strMinutes = toString(specDuration?.minutes ?? 0) ?? ""; + const strDays = toString(specDuration?.days ?? 0) ?? ""; + const strMonths = toString(specDuration?.month ?? 0) ?? ""; + const strYears = toString(specDuration?.years ?? 0) ?? ""; + + useEffect(() => { + if (!secondsRef.current) return; + if (secondsRef.current === document.activeElement) return; + secondsRef.current.value = strSeconds; + }, [strSeconds]); + useEffect(() => { + if (!minutesRef.current) return; + if (minutesRef.current === document.activeElement) return; + minutesRef.current.value = strMinutes; + }, [strMinutes]); + useEffect(() => { + if (!hoursRef.current) return; + if (hoursRef.current === document.activeElement) return; + hoursRef.current.value = strHours; + }, [strHours]); + useEffect(() => { + if (!daysRef.current) return; + if (daysRef.current === document.activeElement) return; + daysRef.current.value = strDays; + }, [strDays]); + useEffect(() => { + if (!monthsRef.current) return; + if (monthsRef.current === document.activeElement) return; + monthsRef.current.value = strMonths; + }, [strMonths]); + useEffect(() => { + if (!yearsRef.current) return; + if (yearsRef.current === document.activeElement) return; + yearsRef.current.value = strYears; + }, [strYears]); + if (state.hidden) return <div />; let clazz = @@ -88,16 +137,76 @@ export function InputDuration<T extends object, K extends keyof T>( <div class="flex flex-col gap-1"> <div class="flex"> <span class="ml-2 inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm"> + <i18n.Translate>years</i18n.Translate> + </span> + <input + name={String(name)} + type="number" + onChange={(e) => { + onChange( + Duration.fromSpec({ + ...specDuration, + years: fromString(e.currentTarget.value), + }), + ); + }} + placeholder={placeholder ? placeholder : undefined} + ref={yearsRef} + // value={toString(sd?.years) ?? ""} + // onBlur={() => { + // onChange(fromString(value as any)); + // }} + // defaultValue={toString(value)} + disabled={disabled ?? false} + aria-invalid={showError} + // aria-describedby="email-error" + class={clazz} + /> + <span class="ml-2 inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm"> + <i18n.Translate>months</i18n.Translate> + </span> + <input + name={String(name)} + type="number" + ref={monthsRef} + onChange={(e) => { + onChange( + Duration.fromSpec({ + ...specDuration, + months: fromString(e.currentTarget.value), + }), + ); + }} + placeholder={placeholder ? placeholder : undefined} + // value={toString(specDuration?.month) ?? ""} + // onBlur={() => { + // onChange(fromString(value as any)); + // }} + // defaultValue={toString(value)} + disabled={disabled ?? false} + aria-invalid={showError} + // aria-describedby="email-error" + class={clazz} + /> + </div> + <div class="flex"> + <span class="ml-2 inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm"> <i18n.Translate>days</i18n.Translate> </span> <input name={String(name)} type="number" + ref={daysRef} onChange={(e) => { - setDays(fromString(e.currentTarget.value)); + onChange( + Duration.fromSpec({ + ...specDuration, + days: fromString(e.currentTarget.value), + }), + ); }} placeholder={placeholder ? placeholder : undefined} - value={toString(sd?.days) ?? ""} + // value={toString(specDuration?.days) ?? ""} // onBlur={() => { // onChange(fromString(value as any)); // }} @@ -113,11 +222,17 @@ export function InputDuration<T extends object, K extends keyof T>( <input name={String(name)} type="number" + ref={hoursRef} onChange={(e) => { - setHours(fromString(e.currentTarget.value)); + onChange( + Duration.fromSpec({ + ...specDuration, + hours: fromString(e.currentTarget.value), + }), + ); }} placeholder={placeholder ? placeholder : undefined} - value={toString(sd?.hours) ?? ""} + // value={toString(specDuration?.hours) ?? ""} // onBlur={() => { // onChange(fromString(value as any)); // }} @@ -135,11 +250,17 @@ export function InputDuration<T extends object, K extends keyof T>( <input name={String(name)} type="number" + ref={minutesRef} onChange={(e) => { - setMinutes(fromString(e.currentTarget.value)); + onChange( + Duration.fromSpec({ + ...specDuration, + minutes: fromString(e.currentTarget.value), + }), + ); }} placeholder={placeholder ? placeholder : undefined} - value={toString(sd?.minutes) ?? ""} + // value={toString(specDuration?.minutes) ?? ""} // onBlur={() => { // onChange(fromString(value as any)); // }} @@ -155,11 +276,18 @@ export function InputDuration<T extends object, K extends keyof T>( <input name={String(name)} type="number" + ref={secondsRef} onChange={(e) => { - setSeconds(fromString(e.currentTarget.value)); + // setSeconds(fromString(e.currentTarget.value)); + onChange( + Duration.fromSpec({ + ...specDuration, + seconds: fromString(e.currentTarget.value), + }), + ); }} placeholder={placeholder ? placeholder : undefined} - value={toString(sd?.seconds) ?? ""} + // value={toString(specDuration?.seconds) ?? ""} // onBlur={() => { // onChange(fromString(value as any)); // }} diff --git a/packages/web-util/src/forms/fields/InputDurationText.stories.tsx b/packages/web-util/src/forms/fields/InputDurationText.stories.tsx @@ -0,0 +1,58 @@ +/* + This file is part of GNU Taler + (C) 2022 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, TranslatedString } from "@gnu-taler/taler-util"; +import * as tests from "../../tests/hook.js"; +import { DefaultForm as TestedComponent } from "../forms-ui.js"; +import { FormDesign, UIHandlerId } from "../forms-types.js"; + +export default { + title: "Input Duration Text", +}; + +type TargetObject = { + age: Duration; +}; +const initial: TargetObject = { + age: { d_ms: 10000000 }, +}; + +const design: FormDesign = { + type: "double-column", + sections: [ + { + title: "this is a simple form" as TranslatedString, + fields: [ + { + type: "durationText", + label: "Age" as TranslatedString, + id: "age" as UIHandlerId, + tooltip: "just numbers" as TranslatedString, + }, + ], + }, + ], +}; + +export const SimpleComment = tests.createExample(TestedComponent, { + initial, + design, +}); diff --git a/packages/web-util/src/forms/fields/InputDurationText.tsx b/packages/web-util/src/forms/fields/InputDurationText.tsx @@ -0,0 +1,105 @@ +import { VNode, h } from "preact"; +import { InputLine } from "./InputLine.js"; +import { UIFormProps } from "../FormProvider.js"; +import { Duration } from "@gnu-taler/taler-util"; + +const PATTERN = /^(?<value>[0-9]+)(?<unit>[smhDMY])$/; +const UNIT_GROUP = "unit"; +const VALUE_GROUP = "value"; + +type DurationUnit = "s" | "m" | "h" | "D" | "M" | "Y"; +type DurationSpec = Parameters<typeof Duration.fromSpec>[0]; + +type DurationValue = { + unit: DurationUnit; + value: number; +}; + +function updateSpec(spec: DurationSpec, value: DurationValue): void { + switch (value.unit) { + case "s": { + spec.seconds = value.value; + break; + } + case "m": { + spec.minutes = value.value; + break; + } + case "h": { + spec.hours = value.value; + break; + } + case "D": { + spec.days = value.value; + break; + } + case "M": { + spec.months = value.value; + break; + } + case "Y": { + spec.years = value.value; + break; + } + } +} + +function parseDurationValue(str: string): DurationValue | undefined { + const r = PATTERN.exec(str); + if (!r) return undefined; + const value = Number.parseInt(r.groups![VALUE_GROUP], 10); + const unit = r.groups![UNIT_GROUP] as DurationUnit; + return { value, unit }; +} + +export function InputDurationText<T extends object, K extends keyof T>( + props: UIFormProps<T, K>, +): VNode { + return ( + // <div>s</div> + <InputLine + type="text" + {...props} + converter={{ + //@ts-ignore + fromStringUI: (v): Duration => { + if (!v) return Duration.getForever(); + const spec = v.split(" ").reduce((prev, cur) => { + const v = parseDurationValue(cur); + if (v) { + updateSpec(prev, v); + } + return prev; + }, {} as DurationSpec); + return Duration.fromSpec(spec); + }, + //@ts-ignore + toStringUI: (v?: Duration): string => { + if (v === undefined) return ""; + // return v! as any; + const spec = Duration.toSpec(v); + let result = ""; + if (spec?.years) { + result += `${spec.years}Y `; + } + if (spec?.month) { + result += `${spec.month}M `; + } + if (spec?.days) { + result += `${spec.days}D `; + } + if (spec?.hours) { + result += `${spec.hours}h `; + } + if (spec?.minutes) { + result += `${spec.minutes}m `; + } + if (spec?.seconds) { + result += `${spec.seconds}s `; + } + return result.trimEnd(); + }, + }} + /> + ); +} diff --git a/packages/web-util/src/forms/fields/InputInteger.stories.tsx b/packages/web-util/src/forms/fields/InputInteger.stories.tsx @@ -52,7 +52,34 @@ const design: FormDesign = { ], }; +const design2: FormDesign = { + type: "double-column", + sections: [ + { + title: "this is a simple form" as TranslatedString, + fields: [ + { + type: "integer", + label: "Age" as TranslatedString, + id: "age" as UIHandlerId, + tooltip: "just numbers" as TranslatedString, + }, + { + type: "integer", + label: "Age" as TranslatedString, + id: "age" as UIHandlerId, + tooltip: "just numbers" as TranslatedString, + }, + ], + }, + ], +}; export const SimpleComment = tests.createExample(TestedComponent, { initial, design, }); + +export const DoubleInput = tests.createExample(TestedComponent, { + initial, + design: design2, +}); diff --git a/packages/web-util/src/forms/fields/InputLine.tsx b/packages/web-util/src/forms/fields/InputLine.tsx @@ -2,6 +2,7 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { ComponentChildren, Fragment, VNode, h } from "preact"; import { Addon, UIFormProps } from "../FormProvider.js"; import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; +import { useEffect, useRef } from "preact/hooks"; //@ts-ignore const TooltipIcon = ( @@ -160,6 +161,7 @@ export function InputLine<T extends object, K extends keyof T>( props: { type: InputType } & UIFormProps<T, K>, ): VNode { const { name, placeholder, before, after, converter, type, disabled } = props; + const input = useRef<HTMLTextAreaElement & HTMLInputElement>(null); const { value, onChange, state, error } = props.handler ?? noHandlerPropsAndNoContextForField(props.name); @@ -168,6 +170,12 @@ export function InputLine<T extends object, K extends keyof T>( converter?.fromStringUI ?? defaultFromString; const toString: (s: any) => string = converter?.toStringUI ?? defaultToString; + useEffect(() => { + if (!input.current) return; + if (input.current === document.activeElement) return; + input.current.value = String(value); + }, [value]); + if (state.hidden) return <div />; let clazz = @@ -223,12 +231,13 @@ export function InputLine<T extends object, K extends keyof T>( > <textarea rows={4} + ref={input} name={String(name)} onChange={(e) => { onChange(fromString(e.currentTarget.value)); }} placeholder={placeholder ? placeholder : undefined} - value={toString(value) ?? ""} + // value={toString(value) ?? ""} // defaultValue={toString(value)} disabled={disabled ?? false} aria-invalid={showError} @@ -248,16 +257,17 @@ export function InputLine<T extends object, K extends keyof T>( > <input name={String(name)} + ref={input} type={type} onChange={(e) => { onChange(fromString(e.currentTarget.value)); }} placeholder={placeholder ? placeholder : undefined} - value={toString(value) ?? ""} + // value={toString(value) ?? ""} // onBlur={() => { // onChange(fromString(value as any)); // }} - // defaultValue={toString(value)} + defaultValue={toString(value)} disabled={disabled ?? false} aria-invalid={showError} // aria-describedby="email-error" diff --git a/packages/web-util/src/forms/forms-types.ts b/packages/web-util/src/forms/forms-types.ts @@ -59,6 +59,7 @@ export type UIFormElementConfig = | UIFormFieldSecret | UIFormFieldSelectMultiple | UIFormFieldDuration + | UIFormFieldDurationText | UIFormFieldSelectOne | UIFormFieldText | UIFormFieldTextArea @@ -151,6 +152,10 @@ type UIFormFieldDuration = { type: "duration"; } & UIFormFieldBaseConfig; +type UIFormFieldDurationText = { + type: "durationText"; +} & UIFormFieldBaseConfig; + type UIFormFieldSelectOne = { type: "selectOne"; choices: Array<SelectUiChoice>; @@ -332,6 +337,11 @@ const codecForUiFormFieldDuration = (): Codec<UIFormFieldDuration> => .property("type", codecForConstString("duration")) .build("UiFormFieldDuration"); +const codecForUiFormFieldDurationText = (): Codec<UIFormFieldDurationText> => + codecForUIFormFieldBaseConfigTemplate<UIFormFieldDurationText>() + .property("type", codecForConstString("durationText")) + .build("UiFormFieldDuration"); + const codecForUiFormFieldSelectMultiple = (): Codec<UIFormFieldSelectMultiple> => codecForUIFormFieldBaseConfigTemplate<UIFormFieldSelectMultiple>() @@ -383,6 +393,7 @@ const codecForUiFormField = (): Codec<UIFormElementConfig> => .alternative("secret", codecForUiFormFieldSecret()) .alternative("selectMultiple", codecForUiFormFieldSelectMultiple()) .alternative("duration", codecForUiFormFieldDuration()) + .alternative("durationText", codecForUiFormFieldDurationText()) .alternative("selectOne", codecForUiFormFieldSelectOne()) .alternative("text", codecForUiFormFieldText()) .alternative("textArea", codecForUiFormFieldTextArea()) diff --git a/packages/web-util/src/forms/forms-utils.ts b/packages/web-util/src/forms/forms-utils.ts @@ -258,6 +258,19 @@ export function convertFormConfigToUiField( }, } as UIFormField; } + case "durationText": { + return { + type: "durationText", + properties: { + ...converBaseFieldsProps(i18n_, config), + ...converInputFieldsProps( + form, + config, + getConverterByFieldType(config.type, config), + ), + }, + } as UIFormField; + } case "toggle": { return { type: "toggle", diff --git a/packages/web-util/src/forms/index.stories.ts b/packages/web-util/src/forms/index.stories.ts @@ -12,3 +12,4 @@ export * as a12 from "./fields/InputTextArea.stories.js"; export * as a13 from "./fields/InputToggle.stories.js"; export * as a14 from "./fields/InputSecret.stories.js"; export * as a15 from "./fields/InputDuration.stories.js"; +export * as a16 from "./fields/InputDurationText.stories.js";