taler-typescript-core

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

commit b5217432044d03224f905d5894058297fd354d8b
parent c20ed1ff554fbaa3ab0b6de0f9c965f5bd996b71
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed,  5 Feb 2025 12:10:09 -0300

input iso time

Diffstat:
Mpackages/web-util/src/forms/Calendar.tsx | 49++++++++++++++++++++++++++++++++++++++++++-------
Mpackages/web-util/src/forms/field-types.ts | 8++++++++
Apackages/web-util/src/forms/fields/InputIsoTime.stories.tsx | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/web-util/src/forms/fields/InputIsoTime.tsx | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/web-util/src/forms/fields/InputLine.tsx | 10+++++++---
Mpackages/web-util/src/forms/forms-types.ts | 17+++++++++++++++++
Mpackages/web-util/src/forms/forms-utils.ts | 17+++++++++++++----
Mpackages/web-util/src/forms/index.stories.ts | 1+
8 files changed, 242 insertions(+), 14 deletions(-)

diff --git a/packages/web-util/src/forms/Calendar.tsx b/packages/web-util/src/forms/Calendar.tsx @@ -10,13 +10,21 @@ import { getYear, isSameDay, isSameMonth, + isValid, + setYear, startOfDay, startOfMonth, startOfWeek, + subYears, } from "date-fns"; import { VNode, h } from "preact"; -import { useState } from "preact/hooks"; +import { useEffect, useRef, useState } from "preact/hooks"; import { useTranslationContext } from "../index.browser.js"; +import { composeRef, saveRef } from "../components/utils.js"; + +const THIS_MONTH = getMonth(new Date()); +const THIS_YEAR = getYear(new Date()); +const TODAY = startOfDay(new Date()); export function Calendar({ value, @@ -25,11 +33,21 @@ export function Calendar({ value: AbsoluteTime | undefined; onChange: (v: AbsoluteTime) => void; }): VNode { - const today = startOfDay(new Date()); - const selected = !value ? today : new Date(AbsoluteTime.toStampMs(value)); + const selectedMaybeInvalid = !value + ? TODAY + : new Date(AbsoluteTime.toStampMs(value)); + const selected = isValid(selectedMaybeInvalid) ? selectedMaybeInvalid : TODAY; const [showingDate, setShowingDate] = useState(selected); - const month = getMonth(showingDate); - const year = getYear(showingDate); + const m = getMonth(showingDate); + const y = getYear(showingDate); + const month = Number.isNaN(m) ? THIS_MONTH : m; + const year = Number.isNaN(y) ? THIS_YEAR : y; + const input = useRef<HTMLInputElement>(); + useEffect(() => { + if (!input.current) return; + if (input.current === document.activeElement) return; + input.current.value = !year ? "" : String(year); + }, [year]); const start = startOfWeek(startOfMonth(showingDate)); const end = endOfWeek(endOfMonth(showingDate)); @@ -73,7 +91,24 @@ export function Calendar({ /> </svg> </button> - <div class="flex-auto text-sm font-semibold">{year}</div> + <div class="flex-auto text-sm font-semibold"> + <input + type="text" + ref={composeRef(saveRef(input))} + onChange={(e) => { + const text = e.currentTarget.value; + const num = Number.parseInt(text, 10); + + if (Number.isSafeInteger(num) && num > 0) { + const nextYear = setYear(showingDate, num); + + if (isValid(nextYear)) { + setShowingDate(nextYear); + } + } + }} + /> + </div> <button type="button" class="flex px-4 flex-none items-center justify-center p-1.5 text-gray-400 hover:text-gray-500 ring-2 round-sm" @@ -157,7 +192,7 @@ export function Calendar({ type="button" key={idx} data-month={isSameMonth(current, showingDate)} - data-today={isSameDay(current, today)} + data-today={isSameDay(current, TODAY)} data-selected={isSameDay(current, selected)} onClick={() => { onChange(AbsoluteTime.fromStampMs(current.getTime())); diff --git a/packages/web-util/src/forms/field-types.ts b/packages/web-util/src/forms/field-types.ts @@ -19,6 +19,7 @@ import { Group } from "./Group.js"; import { HtmlIframe } from "./HtmlIframe.js"; import { InputDurationText } from "./fields/InputDurationText.js"; import { ExternalLink } from "./ExternalLink.js"; +import { InputIsoTime } from "./fields/InputIsoTime.js"; /** * Constrain the type with the ui props */ @@ -37,6 +38,7 @@ type FieldType<T extends object = any, K extends keyof T = any> = { choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0]; choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0]; absoluteTimeText: Parameters<typeof InputAbsoluteTime<T, K>>[0]; + isoTimeText: Parameters<typeof InputIsoTime<T, K>>[0]; integer: Parameters<typeof InputInteger<T, K>>[0]; secret: Parameters<typeof InputSecret<T, K>>[0]; toggle: Parameters<typeof InputToggle<T, K>>[0]; @@ -80,6 +82,10 @@ export type UIFormField = properties: FieldType["absoluteTimeText"]; } | { + type: "isoTimeText"; + properties: FieldType["isoTimeText"]; + } + | { type: "duration"; properties: FieldType["duration"]; } @@ -114,6 +120,8 @@ export const UIFormConfiguration: UIFormFieldMap = { //@ts-ignore absoluteTimeText: InputAbsoluteTime, //@ts-ignore + isoTimeText: InputIsoTime, + //@ts-ignore choiceStacked: InputChoiceStacked, //@ts-ignore choiceHorizontal: InputChoiceHorizontal, diff --git a/packages/web-util/src/forms/fields/InputIsoTime.stories.tsx b/packages/web-util/src/forms/fields/InputIsoTime.stories.tsx @@ -0,0 +1,63 @@ +/* + 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 { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; +import * as tests from "../../tests/hook.js"; +import { FormDesign, UIHandlerId } from "../forms-types.js"; +import { DefaultForm as TestedComponent } from "../forms-ui.js"; +export default { + title: "Input Iso Time", +}; + +export namespace Simplest { + export interface Form { + comment: string; + } +} + +type TargetObject = { + today: string; +}; +const initial: TargetObject = { + today: "1/12/3333344", +}; + +const design: FormDesign = { + type: "double-column", + sections: [ + { + title: "this is a simple form" as TranslatedString, + fields: [ + { + type: "isoTimeText", + label: "label of the field" as TranslatedString, + id: "today" as UIHandlerId, + pattern: "dd/MM/yyyy", + }, + ], + }, + ], +}; + +export const SimpleComment = tests.createExample(TestedComponent, { + initial, + design, +}); diff --git a/packages/web-util/src/forms/fields/InputIsoTime.tsx b/packages/web-util/src/forms/fields/InputIsoTime.tsx @@ -0,0 +1,91 @@ +import { AbsoluteTime } from "@gnu-taler/taler-util"; +import { format, parse } from "date-fns"; +import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { Calendar } from "../Calendar.js"; +import { Dialog } from "../Dialog.js"; +import { UIFormProps } from "../FormProvider.js"; +import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; +import { InputLine } from "./InputLine.js"; + +export function InputIsoTime<T extends object, K extends keyof T>( + properties: { pattern?: string } & UIFormProps<T, K>, +): VNode { + const pattern = properties.pattern ?? "dd/MM/yyyy"; + const [open, setOpen] = useState(false); + + const { value, onChange } = + properties.handler ?? noHandlerPropsAndNoContextForField(properties.name); + + const time = parse(value, pattern, Date.now()).getTime(); + // const strTime = format(time, pattern); + return ( + <Fragment> + <InputLine<T, K> + type="text" + {...properties} + after={{ + type: "button", + onClick: () => { + setOpen(true); + }, + // icon: <CalendarIcon class="h-6 w-6" />, + children: ( + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-6 h-6" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" + /> + </svg> + ), + }} + converter={{ + //@ts-ignore + fromStringUI: (v): string | undefined => { + if (!v) return undefined; + try { + const t_ms = parse(v, pattern, Date.now()).getTime(); + return format(t_ms, pattern); + } catch (e) { + return undefined; + } + }, + }} + /> + {open && ( + <Dialog onClose={() => setOpen(false)}> + <Calendar + value={AbsoluteTime.fromMilliseconds(time)} + onChange={(v) => { + onChange( + v.t_ms === "never" ? undefined : format(v.t_ms, pattern), + ); + setOpen(false); + }} + /> + </Dialog> + )} + {/* {open && ( + <Dialog onClose={() => setOpen(false)}> + <TimePicker + value={AbsoluteTime.fromMilliseconds(time)} + onChange={(v) => { + onChange(v as any); + }} + onConfirm={() => { + setOpen(false); + }} + /> + </Dialog> + )} */} + </Fragment> + ); +} diff --git a/packages/web-util/src/forms/fields/InputLine.tsx b/packages/web-util/src/forms/fields/InputLine.tsx @@ -72,14 +72,16 @@ export function LabelWithTooltipMaybeRequired({ export function RenderAddon({ disabled, addon, + reverse, }: { disabled?: boolean; + reverse?: boolean; addon: Addon; }): VNode { switch (addon.type) { case "text": { return ( - <span class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm"> + <span class="inline-flex items-center data-[right=true]:rounded-r-md data-[left=true]:rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm"> {addon.text} </span> ); @@ -97,7 +99,9 @@ export function RenderAddon({ type="button" disabled={disabled} onClick={addon.onClick} - class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-l-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50" + data-left={!reverse} + data-right={reverse} + class="relative -ml-px inline-flex items-center gap-x-1.5 data-[right=true]:rounded-r-md data-[left=true]:rounded-l-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50" > {addon.children} </button> @@ -133,7 +137,7 @@ export function InputWrapper<T extends object, K extends keyof T>({ {children} - {after && <RenderAddon disabled={disabled} addon={after} />} + {after && <RenderAddon disabled={disabled} addon={after} reverse />} </div> {error && ( <p class="mt-2 text-sm text-red-600" id="email-error"> diff --git a/packages/web-util/src/forms/forms-types.ts b/packages/web-util/src/forms/forms-types.ts @@ -51,6 +51,7 @@ export type UIFormElementConfig = | UIFormElementExternalLink | UIFormElementHtmlIframe | UIFormFieldAbsoluteTime + | UIFormFieldIsoTime | UIFormFieldAmount | UIFormFieldArray | UIFormFieldChoiseHorizontal @@ -78,6 +79,13 @@ type UIFormFieldAbsoluteTime = { pattern: string; } & UIFormFieldBaseConfig; +type UIFormFieldIsoTime = { + type: "isoTimeText"; + max?: TalerProtocolTimestamp; + min?: TalerProtocolTimestamp; + pattern: string; +} & UIFormFieldBaseConfig; + type UIFormFieldAmount = { type: "amount"; max?: Integer; @@ -258,6 +266,14 @@ const codecForUiFormFieldAbsoluteTime = (): Codec<UIFormFieldAbsoluteTime> => .property("min", codecOptional(codecForTimestamp)) .build("UIFormFieldAbsoluteTime"); +const codecForUiFormFieldIsoTime = (): Codec<UIFormFieldIsoTime> => + codecForUIFormFieldBaseConfigTemplate<UIFormFieldIsoTime>() + .property("type", codecForConstString("isoTimeText")) + .property("pattern", codecForString()) + .property("max", codecOptional(codecForTimestamp)) + .property("min", codecOptional(codecForTimestamp)) + .build("UIFormFieldIsoTime"); + const codecForUiFormFieldAmount = (): Codec<UIFormFieldAmount> => codecForUIFormFieldBaseConfigTemplate<UIFormFieldAmount>() .property("type", codecForConstString("amount")) @@ -411,6 +427,7 @@ const codecForUiFormField = (): Codec<UIFormElementConfig> => .alternative("download-link", codecForUIFormElementLink()) .alternative("external-link", codecForUIFormElementExternalLink()) .alternative("absoluteTimeText", codecForUiFormFieldAbsoluteTime()) + .alternative("isoTimeText", codecForUiFormFieldIsoTime()) .alternative("amount", codecForUiFormFieldAmount()) .alternative("caption", codecForUiFormFieldCaption()) .alternative("htmlIframe", codecForUiFormFieldHtmlIFrame()) diff --git a/packages/web-util/src/forms/forms-utils.ts b/packages/web-util/src/forms/forms-utils.ts @@ -120,6 +120,19 @@ export function convertFormConfigToUiField( }, } as UIFormField; } + case "isoTimeText": { + return { + type: "isoTimeText", + properties: { + ...converBaseFieldsProps(i18n_, config), + ...converInputFieldsProps( + form, + config, + getConverterByFieldType(config.type, config), + ), + }, + } as UIFormField; + } case "amount": { return { type: "amount", @@ -322,10 +335,6 @@ function getConverterByFieldType( // @ts-expect-error check this return amountConverter(config); } - if (fieldType === "TalerExchangeApi.AmlState") { - // @ts-expect-error check this - return amlStateConverter; - } return nullConverter as StringConverter<unknown>; } diff --git a/packages/web-util/src/forms/index.stories.ts b/packages/web-util/src/forms/index.stories.ts @@ -14,3 +14,4 @@ export * as a14 from "./fields/InputSecret.stories.js"; export * as a15 from "./fields/InputDuration.stories.js"; export * as a16 from "./fields/InputDurationText.stories.js"; export * as a17 from "./gana/GLS_Onboarding.stories.js"; +export * as a18 from "./fields/InputIsoTime.stories.js";