taler-typescript-core

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

commit e1aae7f0f0f0c30d91f1f2ea3bcefeb1e4eb7cf3
parent 411dcd6b32501c1c1c91727b428ae0e46c76460b
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 15 Jan 2025 11:38:19 -0300

fix some input array problems

Diffstat:
Mpackages/web-util/src/forms/fields/InputArray.stories.tsx | 36++++++++++++++++++++++++++++++++++++
Mpackages/web-util/src/forms/fields/InputArray.tsx | 191++++++++++++++++++++++---------------------------------------------------------
Mpackages/web-util/src/forms/fields/InputDuration.tsx | 2+-
Mpackages/web-util/src/forms/fields/InputSelectMultiple.tsx | 41+++++++++++++++++++++--------------------
Mpackages/web-util/src/forms/fields/InputSelectOne.stories.tsx | 37+++++++++++++++++++++++++++++++++++++
Mpackages/web-util/src/forms/fields/InputText.stories.tsx | 1+
Mpackages/web-util/src/forms/fields/InputToggle.stories.tsx | 1+
Mpackages/web-util/src/forms/fields/InputToggle.tsx | 1-
Mpackages/web-util/src/forms/forms-ui.tsx | 15++++++++++++---
Mpackages/web-util/src/hooks/useForm.ts | 2+-
10 files changed, 163 insertions(+), 164 deletions(-)

diff --git a/packages/web-util/src/forms/fields/InputArray.stories.tsx b/packages/web-util/src/forms/fields/InputArray.stories.tsx @@ -135,3 +135,39 @@ export const NonMixingProperties = tests.createExample(TestedComponent, { initial: initial2, design: design2, }); + +const initial3: any = { + list: [{ steps: ["asd"] }], +}; + +const design3: FormDesign = { + type: "single-column", + fields: [ + { + type: "array", + id: "list" as UIHandlerId, + label: `Paths`, + help: `For every entry the customer will have a different path to satify checks.`, + labelFieldId: "steps" as UIHandlerId, + fields: [ + { + type: "selectMultiple", + choices: ["asd", "qwe", "zxc"].map((m) => { + return { + value: m, + label: m, + }; + }), + id: "steps" as UIHandlerId, + label: `Steps`, + help: `The checks that the customer will need to satisfy for this path.`, + }, + ], + }, + ], +}; + +export const ArrayOfSelect = tests.createExample(TestedComponent, { + initial: initial3, + design: design3, +}); diff --git a/packages/web-util/src/forms/fields/InputArray.tsx b/packages/web-util/src/forms/fields/InputArray.tsx @@ -1,7 +1,11 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; -import { getValueFromPath, useForm } from "../../hooks/useForm.js"; +import { + getValueFromPath, + RecursivePartial, + useForm, +} from "../../hooks/useForm.js"; import { SingleColumnFormSectionUI, useTranslationContext, @@ -83,88 +87,38 @@ export function noHandlerPropsAndNoContextForField( ); } -// function getRequiredFields(fields: UIFormField[]): Array<UIHandlerId> { -// const shape: Array<UIHandlerId> = []; -// fields.forEach((field) => { -// if ("name" in field.properties) { -// // FIXME: this should be a validation when loading the form -// // consistency check -// if (shape.indexOf(field.properties.name) !== -1) { -// throw Error(`already present: ${field.properties.name}`); -// } -// if (!field.properties.required) { -// return; -// } -// shape.push(field.properties.name); -// } else if (field.type === "group") { -// Array.prototype.push.apply( -// shape, -// getRequiredFields(field.properties.fields), -// ); -// } -// }); -// return shape; -// } +type FormType = {}; -function getRequiredFields(fields: UIFormElementConfig[]): Array<UIHandlerId> { - const shape: Array<UIHandlerId> = []; - fields.forEach((field) => { - if ("id" in field) { - // FIXME: this should be a validation when loading the form - // consistency check - if (shape.indexOf(field.id) !== -1) { - throw Error(`already present: ${field.id}`); - } - if (!field.required) { - return; - } - shape.push(field.id); - } else if (field.type === "group") { - Array.prototype.push.apply(shape, getRequiredFields(field.fields)); - } - }); - return shape; -} +function ArrayForm({ + fields, + selected, + onChange, +}: { + fields: UIFormElementConfig[]; + selected: Record<string, string | undefined> | undefined; + onChange: (r: RecursivePartial<FormType>) => void; +}): VNode { + const form = useForm<FormType>( + { + type: "single-column", + fields, + }, + selected ?? {}, + ); -// function getShapeFromFields(fields: UIFormField[]): Array<UIHandlerId> { -// const shape: Array<UIHandlerId> = []; -// fields.forEach((field) => { -// if ("name" in field.properties) { -// // FIXME: this should be a validation when loading the form -// // consistency check -// if (shape.indexOf(field.properties.name) !== -1) { -// throw Error(`already present: ${field.properties.name}`); -// } -// shape.push(field.properties.name); -// } else if (field.type === "group") { -// Array.prototype.push.apply( -// shape, -// getShapeFromFields(field.properties.fields), -// ); -// } -// }); -// return shape; -// } + useEffect(() => { + onChange(form.status.result); + }, [form.status.result]); -function getShapeFromFields(fields: UIFormElementConfig[]): Array<UIHandlerId> { - const shape: Array<UIHandlerId> = []; - fields.forEach((field) => { - if ("id" in field) { - // FIXME: this should be a validation when loading the form - // consistency check - if (shape.indexOf(field.id) !== -1) { - throw Error(`already present: ${field.id}`); - } - shape.push(field.id); - } else if (field.type === "group") { - Array.prototype.push.apply(shape, getShapeFromFields(field.fields)); - } - }); - return shape; + return ( + <div class="px-4 py-6"> + <div class="grid grid-cols-1 gap-y-8 "> + <SingleColumnFormSectionUI fields={fields} handler={form.handler} /> + </div> + </div> + ); } -type FormType = {}; - export function InputArray<T extends object, K extends keyof T>( props: { fields: UIFormElementConfig[]; @@ -183,20 +137,6 @@ export function InputArray<T extends object, K extends keyof T>( const selected = selectedIndex === undefined ? undefined : list[selectedIndex]; - const form = useForm<FormType>( - { - type: "single-column", - fields, - }, - selected ?? {}, - ); - - useEffect(() => { - if (selectedIndex === undefined) return; - const newValue = [...list]; - newValue.splice(selectedIndex, 1, form.status.result); - onChange(newValue as any); - }, [form.status.result, selectedIndex]); const { i18n } = useTranslationContext(); return ( @@ -210,8 +150,11 @@ export function InputArray<T extends object, K extends keyof T>( <div class="overflow-hidden ring-1 ring-gray-900/5 rounded-xl p-4"> <div class="-space-y-px rounded-md bg-white "> {list.map((v, idx) => { - const label = + const labelValue = getValueFromPath(v, labelField.split(".")) ?? "<<incomplete>>"; + const label = Array.isArray(labelValue) + ? labelValue.join(", ") + : labelValue; return ( <Option label={label as TranslatedString} @@ -246,51 +189,23 @@ export function InputArray<T extends object, K extends keyof T>( )} </div> {selectedIndex !== undefined && ( - /** - * This form provider act as a substate of the parent form - * Consider creating an InnerFormProvider since not every feature is expected - */ - // <FormProvider - // initial={selected ?? {}} - // readOnly={state.disabled} - // computeFormState={(v) => { - // // current state is ignored - // // the state is defined by the parent form - - // // elements should be present in the state object since this is expected to be an array - // //@ts-ignore - // // return state.elements[selectedIndex]; - // return {}; - // }} - // onSubmit={(v) => { - // const newValue = [...list]; - // newValue.splice(selectedIndex, 1, v); - // onChange(newValue as any); - // setSelectedIndex(undefined); - // }} - // onUpdate={(v) => { - // const newValue = [...list]; - // newValue.splice(selectedIndex, 1, v); - // onChange(newValue as any); - // }} - // > - <div class="px-4 py-6"> - <div class="grid grid-cols-1 gap-y-8 "> - <SingleColumnFormSectionUI - fields={fields} - handler={form.handler} - /> - {/* <RenderAllFieldsByUiConfig - fields={convertUiField( - i18n, - fields, - form.handler, - getConverterById, - )} - /> */} - </div> - </div> - // </FormProvider> + <ArrayForm + fields={fields} + onChange={(result) => { + const newValue = [...list]; + newValue.splice(selectedIndex, 1, result); + onChange(newValue as any); + }} + selected={selected} + /> + // <div class="px-4 py-6"> + // <div class="grid grid-cols-1 gap-y-8 "> + // <SingleColumnFormSectionUI + // fields={fields} + // handler={form.handler} + // /> + // </div> + // </div> )} {selectedIndex !== undefined && ( <div class="flex items-center justify-end gap-x-6"> diff --git a/packages/web-util/src/forms/fields/InputDuration.tsx b/packages/web-util/src/forms/fields/InputDuration.tsx @@ -14,7 +14,7 @@ export function InputDuration<T extends object, K extends keyof T>( const { value, onChange, state, error } = props.handler ?? noHandlerPropsAndNoContextForField(props.name); - const sd = Duration.toSpec(value as Duration); + 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); diff --git a/packages/web-util/src/forms/fields/InputSelectMultiple.tsx b/packages/web-util/src/forms/fields/InputSelectMultiple.tsx @@ -4,6 +4,7 @@ import { UIFormProps } from "../FormProvider.js"; import { noHandlerPropsAndNoContextForField } from "./InputArray.js"; import { ChoiceS } from "./InputChoiceStacked.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; +import { useTranslationContext } from "../../index.browser.js"; export function InputSelectMultiple<T extends object, K extends keyof T>( props: { @@ -22,6 +23,7 @@ export function InputSelectMultiple<T extends object, K extends keyof T>( unique, max, } = props; + const { i18n } = useTranslationContext(); const { value, onChange, state } = props.handler ?? noHandlerPropsAndNoContextForField(props.name); @@ -39,7 +41,9 @@ export function InputSelectMultiple<T extends object, K extends keyof T>( filter === undefined ? undefined : choices.filter((v) => { - return regex.test(v.label); + const match = regex.test(v.label); + if (!unique) return match; + return match && list.indexOf(v.value as string) === -1; }); return ( <div class="sm:col-span-6"> @@ -116,7 +120,20 @@ export function InputSelectMultiple<T extends object, K extends keyof T>( </svg> </button> - {filteredChoices !== undefined && ( + {!filter ? undefined : filteredChoices === undefined || + !filteredChoices.length ? ( + <ul + class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" + id="options" + role="listbox" + > + <li class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600"> + <span class="block truncate"> + <i18n.Translate>No element found</i18n.Translate> + </span> + </li> + </ul> + ) : ( <ul class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" id="options" @@ -138,31 +155,15 @@ export function InputSelectMultiple<T extends object, K extends keyof T>( return; } const newValue = [...list]; - newValue.splice(0, 0, v.value as string); + newValue.push(v.value as string); + // newValue.splice(0, 0, v.value as string); onChange(newValue as any); }} - - // 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> diff --git a/packages/web-util/src/forms/fields/InputSelectOne.stories.tsx b/packages/web-util/src/forms/fields/InputSelectOne.stories.tsx @@ -76,3 +76,40 @@ export const SimpleComment = tests.createExample(TestedComponent, { initial, design, }); + +const design2: FormDesign = { + type: "double-column", + sections: [ + { + title: "this is a simple form" as TranslatedString, + fields: [ + { + type: "selectOne", + label: "label of the field" as TranslatedString, + id: "things" as UIHandlerId, + required: true, + placeholder: "search..." as TranslatedString, + choices: [ + { + label: "one label" as TranslatedString, + value: "one", + }, + { + label: "two label" as TranslatedString, + value: "two", + }, + { + label: "five label" as TranslatedString, + value: "five", + }, + ], + }, + ], + }, + ], +}; + +export const SimpleRequired = tests.createExample(TestedComponent, { + initial, + design: design2, +}); diff --git a/packages/web-util/src/forms/fields/InputText.stories.tsx b/packages/web-util/src/forms/fields/InputText.stories.tsx @@ -51,6 +51,7 @@ const design: FormDesign = { type: "text", label: "label of the field" as TranslatedString, id: "comment" as UIHandlerId, + required: true, }, ], }, diff --git a/packages/web-util/src/forms/fields/InputToggle.stories.tsx b/packages/web-util/src/forms/fields/InputToggle.stories.tsx @@ -64,6 +64,7 @@ export const WithThreeState = tests.createExample(TestedComponent, { type: "toggle", label: "do you accept?" as TranslatedString, threeState: true, + required: true, id: "accept" as UIHandlerId, }, ], diff --git a/packages/web-util/src/forms/fields/InputToggle.tsx b/packages/web-util/src/forms/fields/InputToggle.tsx @@ -24,7 +24,6 @@ export function InputToggle<T extends object, K extends keyof T>( const isOn = !!value; return ( <div class="sm:col-span-6"> - v = {JSON.stringify({ value, isOn })} <div class="flex items-center justify-between"> <LabelWithTooltipMaybeRequired label={label} diff --git a/packages/web-util/src/forms/forms-ui.tsx b/packages/web-util/src/forms/forms-ui.tsx @@ -26,9 +26,18 @@ export function DefaultForm<T>({ return ( <div> <FormUI design={design} handler={handler} /> - <pre class="break-all whitespace-pre-wrap"> - {JSON.stringify({ status }, undefined, 2)} - </pre> + {status.status === "ok" ? ( + <pre class="break-all whitespace-pre-wrap"> + {JSON.stringify(status.result ?? {}, undefined, 2)} + </pre> + ) : ( + <Fragment> + <h1>form validation </h1> + <pre class="break-all whitespace-pre-wrap bg-red-200 border border-red-500 w-max p-4"> + {JSON.stringify(status.errors, undefined, 2)} + </pre> + </Fragment> + )} </div> ); } diff --git a/packages/web-util/src/hooks/useForm.ts b/packages/web-util/src/hooks/useForm.ts @@ -27,7 +27,7 @@ import { UIFormElementConfig, UIHandlerId, } from "@gnu-taler/web-util/browser"; -import { useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; export type FormHandler<T> = { [k in keyof T]?: T[k] extends string