taler-typescript-core

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

commit 987c556dcf4fe5fd118f144cf01d172890fc4426
parent 94246810ebbc4e8f97d89bfce9af22fb719e7b69
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 29 Jan 2025 15:24:27 -0300

update gls form

Diffstat:
Mpackages/web-util/src/components/utils.ts | 50++++++++++++++++++++++++++++++++++++++++++++++++--
Mpackages/web-util/src/forms/FormProvider.tsx | 3++-
Mpackages/web-util/src/forms/fields/InputAbsoluteTime.tsx | 1+
Mpackages/web-util/src/forms/fields/InputArray.tsx | 4----
Mpackages/web-util/src/forms/fields/InputLine.tsx | 7++++---
Mpackages/web-util/src/forms/fields/InputSelectOne.stories.tsx | 2+-
Mpackages/web-util/src/forms/fields/InputSelectOne.tsx | 24++++++------------------
Mpackages/web-util/src/forms/forms-ui.tsx | 25++++++++++++++++++++++---
Mpackages/web-util/src/forms/gana/GLS_Onboarding.ts | 27++++-----------------------
9 files changed, 88 insertions(+), 55 deletions(-)

diff --git a/packages/web-util/src/components/utils.ts b/packages/web-util/src/components/utils.ts @@ -1,5 +1,5 @@ -import { createElement, VNode } from "preact"; -import { useEffect, useRef } from "preact/hooks"; +import { createElement, Ref, VNode } from "preact"; +import { MutableRef, useEffect, useRef } from "preact/hooks"; export type StateFunc<S> = (p: S) => VNode; @@ -107,6 +107,52 @@ export function preconnectAs(pre: Preconnect[]) { } } +export function composeRef<T>(...fn: ((e: T | null) => void)[]) { + return (element: T | null) => { + fn.forEach((handler) => { + handler(element); + }); + }; +} + +export function saveRef<T>(ref: MutableRef<T>) { + return (element: T | null) => { + if (element) { + ref.current = element; + } + }; +} +/** + * Show the element when the load ended + * @param element + */ +export function doAutoFocus<T extends HTMLElement>( + element: T | null | undefined, +) { + if (element) { + setTimeout(() => { + element.focus({ preventScroll: true }); + }, 100); + } +} + +/** + * Show the element when the load ended + * @param element + */ +export function doAutoFocusWithScroll(element: HTMLElement | null) { + if (element) { + setTimeout(() => { + element.focus({ preventScroll: true }); + element.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "center", + }); + }, 100); + } +} + /** * * @param obj VNode diff --git a/packages/web-util/src/forms/FormProvider.tsx b/packages/web-util/src/forms/FormProvider.tsx @@ -54,7 +54,8 @@ export type FieldUIOptions = { disabled?: boolean; /* should not show */ hidden?: boolean; - + /* should start with focus */ + focus?: boolean; /* show a mark as required*/ required?: boolean; }; diff --git a/packages/web-util/src/forms/fields/InputAbsoluteTime.tsx b/packages/web-util/src/forms/fields/InputAbsoluteTime.tsx @@ -20,6 +20,7 @@ export function InputAbsoluteTime<T extends object, K extends keyof T>( <Fragment> <InputLine<T, K> type="text" + focus={properties.focus} after={{ type: "button", onClick: () => { diff --git a/packages/web-util/src/forms/fields/InputArray.tsx b/packages/web-util/src/forms/fields/InputArray.tsx @@ -46,10 +46,6 @@ function ArrayForm({ selected ?? {}, ); - // useEffect(() => { - // onChange(form.status.result); - // }, [form.status.result]); - return ( <div class="px-4 py-6"> <div class="grid grid-cols-1 gap-y-8 "> diff --git a/packages/web-util/src/forms/fields/InputLine.tsx b/packages/web-util/src/forms/fields/InputLine.tsx @@ -3,6 +3,7 @@ import { ComponentChildren, Fragment, VNode, h } from "preact"; import { Addon, UIFormProps } from "../FormProvider.js"; import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; import { useEffect, useRef } from "preact/hooks"; +import { composeRef, doAutoFocus, saveRef } from "../../components/utils.js"; //@ts-ignore const TooltipIcon = ( @@ -161,7 +162,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 input = useRef<HTMLTextAreaElement | HTMLInputElement>(); const { value, onChange, state, error } = props.handler ?? noHandlerPropsAndNoContextForField(props.name); @@ -231,7 +232,7 @@ export function InputLine<T extends object, K extends keyof T>( > <textarea rows={4} - ref={input} + ref={composeRef(saveRef(input), doAutoFocus)} name={String(name)} onChange={(e) => { onChange(fromString(e.currentTarget.value)); @@ -257,7 +258,7 @@ export function InputLine<T extends object, K extends keyof T>( > <input name={String(name)} - ref={input} + ref={composeRef(saveRef(input), doAutoFocus)} type={type} onChange={(e) => { onChange(fromString(e.currentTarget.value)); diff --git a/packages/web-util/src/forms/fields/InputSelectOne.stories.tsx b/packages/web-util/src/forms/fields/InputSelectOne.stories.tsx @@ -110,6 +110,6 @@ const design2: FormDesign = { }; export const SimpleRequired = tests.createExample(TestedComponent, { - initial, + initial: {}, design: design2, }); diff --git a/packages/web-util/src/forms/fields/InputSelectOne.tsx b/packages/web-util/src/forms/fields/InputSelectOne.tsx @@ -15,6 +15,7 @@ export function InputSelectOne<T extends object, K extends keyof T>( props.handler ?? noHandlerPropsAndNoContextForField(props.name); const [filter, setFilter] = useState<string | undefined>(undefined); + const [dirty, setDirty] = useState<boolean>(); const regex = new RegExp(`.*${filter}.*`, "i"); const choiceMap = choices.reduce( (prev, curr) => { @@ -43,6 +44,7 @@ export function InputSelectOne<T extends object, K extends keyof T>( type="button" onClick={() => { onChange(undefined!); + setDirty(true); }} class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20" > @@ -63,6 +65,7 @@ export function InputSelectOne<T extends object, K extends keyof T>( value={filter ?? ""} onChange={(e) => { setFilter(e.currentTarget.value); + setDirty(true); }} placeholder={placeholder} class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" @@ -74,6 +77,7 @@ export function InputSelectOne<T extends object, K extends keyof T>( type="button" onClick={() => { setFilter(filter === undefined ? "" : undefined); + setDirty(true); }} class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none" > @@ -107,34 +111,18 @@ export function InputSelectOne<T extends object, K extends keyof T>( onClick={() => { setFilter(undefined); onChange(v.value as any); + setDirty(true); }} - - // tabindex="-1" > - {/* <!-- Selected: "font-semibold" --> */} <span class="block truncate">{v.label}</span> - - {/* <!-- - Checkmark, only display for selected option. - - Active: "text-white", Not Active: "text-indigo-600" - --> */} </li> ); })} - - {/* <!-- - Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation. - - Active: "text-white bg-indigo-600", Not Active: "text-gray-900" - --> */} - - {/* <!-- More items... --> */} </ul> )} </div> )} - {error && ( + {dirty !== undefined && error && ( <p class="mt-2 text-sm text-red-600" id="email-error"> {error} </p> diff --git a/packages/web-util/src/forms/forms-ui.tsx b/packages/web-util/src/forms/forms-ui.tsx @@ -50,23 +50,33 @@ export function DefaultForm<T>({ export function FormUI<T>({ design, handler, + focus, }: { design: FormDesign; handler: FormHandler<T>; + focus?: boolean; }): VNode { switch (design.type) { case "double-column": { const ui = design.sections.map((section, i) => { if (!section) return <Fragment />; return ( - <DoubleColumnFormSectionUI section={section} handler={handler} /> + <DoubleColumnFormSectionUI + section={section} + handler={handler} + focus={focus} + /> ); }); return <Fragment>{ui}</Fragment>; } case "single-column": { return ( - <SingleColumnFormSectionUI fields={design.fields} handler={handler} /> + <SingleColumnFormSectionUI + fields={design.fields} + handler={handler} + focus={focus} + /> ); } } @@ -74,10 +84,12 @@ export function FormUI<T>({ export function DoubleColumnFormSectionUI<T>({ section, + focus, handler, }: { handler: FormHandler<T>; section: DoubleColumnFormSection; + focus?: boolean; }): VNode { const { i18n } = useTranslationContext(); return ( @@ -97,6 +109,7 @@ export function DoubleColumnFormSectionUI<T>({ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> <RenderAllFieldsByUiConfig fields={convertFormConfigToUiField(i18n, section.fields, handler)} + focus={focus} /> </div> </div> @@ -107,9 +120,11 @@ export function DoubleColumnFormSectionUI<T>({ export function SingleColumnFormSectionUI<T>({ fields, handler, + focus, }: { handler: FormHandler<T>; fields: UIFormElementConfig[]; + focus?: boolean; }): VNode { const { i18n } = useTranslationContext(); return ( @@ -118,6 +133,7 @@ export function SingleColumnFormSectionUI<T>({ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> <RenderAllFieldsByUiConfig fields={convertFormConfigToUiField(i18n, fields, handler)} + focus={focus} /> </div> </div> @@ -126,9 +142,11 @@ export function SingleColumnFormSectionUI<T>({ } export function RenderAllFieldsByUiConfig({ + focus, fields, }: { fields: UIFormField[]; + focus?: boolean; }): VNode { return create( Fragment, @@ -137,7 +155,8 @@ export function RenderAllFieldsByUiConfig({ const Component = UIFormConfiguration[ field.type ] as FieldComponentFunction<any>; - return Component(field.properties); + const p = { ...field.properties, focus: focus }; + return Component(p); }), ); } diff --git a/packages/web-util/src/forms/gana/GLS_Onboarding.ts b/packages/web-util/src/forms/gana/GLS_Onboarding.ts @@ -14,10 +14,12 @@ export function GLS_Onboarding( sections: [ { title: i18n.str`Personal individual information`, + description: i18n.str`Contact information of the company representative`, fields: [ { id: "PERSON_FULL_NAME" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, - label: i18n.str`Full name`, + label: i18n.str`First name(s)`, + help: i18n.str`As on your ID document`, // gana_type: "String", type: "text", }, @@ -29,20 +31,6 @@ export function GLS_Onboarding( required: true, }, { - id: "PERSON_NATIONAL_ID" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, - label: i18n.str`National ID Number`, - // gana_type: "String", - type: "text", - required: true, - }, - { - id: "PERSON_NATIONAL_ID_SCAN" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, - label: i18n.str`National ID Photo`, - // gana_type: "File", - type: "file", - required: true, - }, - { id: "PERSON_DATE_OF_BIRTH" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, label: i18n.str`Date of birth`, // gana_type: "AbsoluteDate", @@ -172,16 +160,9 @@ export function GLS_Onboarding( ], }, { - title: i18n.str`Contact information`, + title: i18n.str`Contact information of company headquarters`, fields: [ { - id: "CONTACT_DNS_DOMAIN" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, - label: i18n.str`Hostname`, - // gana_type: "Hostname", - type: "text", - required: true, - }, - { id: "CONTACT_WEB_DOMAIN" satisfies keyof TalerFormAttributes.GLS_Onboarding as UIHandlerId, label: i18n.str`Web site`, // gana_type: "HttpHostnamePath",