taler-typescript-core

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

commit 5c17476815c55585c3176de23d33294b8aea7a59
parent 9e347c2f4a59be7b66d0f77e8d072a867b1e2fd3
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 29 Jan 2025 11:58:33 -0300

better array form

Diffstat:
Mpackages/web-util/src/forms/fields/InputArray.stories.tsx | 2+-
Mpackages/web-util/src/forms/fields/InputArray.tsx | 229++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mpackages/web-util/src/hooks/useForm.ts | 4++--
3 files changed, 132 insertions(+), 103 deletions(-)

diff --git a/packages/web-util/src/forms/fields/InputArray.stories.tsx b/packages/web-util/src/forms/fields/InputArray.stories.tsx @@ -96,7 +96,7 @@ const design2: FormDesign = { fields: [ { type: "text", - id: "PERSON_FULL_NAME" as UIHandlerId, + id: "AGE" as UIHandlerId, label: "Full name", required: true, }, diff --git a/packages/web-util/src/forms/fields/InputArray.tsx b/packages/web-util/src/forms/fields/InputArray.tsx @@ -14,71 +14,6 @@ import { UIFormProps } from "../FormProvider.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; import { UIFormElementConfig, UIHandlerId } from "../forms-types.js"; -function Option({ - label, - disabled, - isFirst, - isLast, - isSelected, - onClick, -}: { - label: TranslatedString; - isFirst?: boolean; - isLast?: boolean; - isSelected?: boolean; - disabled?: boolean; - onClick: () => void; -}): VNode { - let clazz = "relative flex border p-4 focus:outline-none disabled:text-grey"; - if (isFirst) { - clazz += " rounded-tl-md rounded-tr-md "; - } - if (isLast) { - clazz += " rounded-bl-md rounded-br-md "; - } - if (isSelected) { - clazz += " z-10 border-indigo-200 bg-indigo-50 "; - } else { - clazz += " border-gray-200"; - } - if (disabled) { - clazz += - " cursor-not-allowed bg-gray-50 text-gray-500 ring-gray-200 text-gray"; - } else { - clazz += " cursor-pointer"; - } - return ( - <label class={clazz}> - <input - type="radio" - name="privacy-setting" - checked={isSelected} - disabled={disabled} - onClick={onClick} - class="mt-0.5 h-4 w-4 shrink-0 text-indigo-600 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200 focus:ring-indigo-600" - aria-labelledby="privacy-setting-0-label" - aria-describedby="privacy-setting-0-description" - /> - <span class="ml-3 flex flex-col"> - <span - id="privacy-setting-0-label" - disabled - class="block text-sm font-medium" - > - {label} - </span> - {/* <!-- Checked: "text-indigo-700", Not Checked: "text-gray-500" --> */} - {/* <span - id="privacy-setting-0-description" - class="block text-sm" - > - This project would be available to anyone who has the link - </span> */} - </span> - </label> - ); -} - export function noHandlerPropsAndNoContextForField( field: string | number | symbol, ): never { @@ -92,12 +27,17 @@ type FormType = {}; function ArrayForm({ fields, selected, - onChange, + onClose, + onRemove, + onConfirm, }: { fields: UIFormElementConfig[]; selected: Record<string, string | undefined> | undefined; - onChange: (r: RecursivePartial<FormType>) => void; + onClose: () => void; + onRemove: () => void; + onConfirm: (r: RecursivePartial<FormType>) => void; }): VNode { + const { i18n } = useTranslationContext(); const form = useForm<FormType>( { type: "single-column", @@ -106,15 +46,54 @@ function ArrayForm({ selected ?? {}, ); - useEffect(() => { - onChange(form.status.result); - }, [form.status.result]); + // useEffect(() => { + // onChange(form.status.result); + // }, [form.status.result]); return ( <div class="px-4 py-6"> <div class="grid grid-cols-1 gap-y-8 "> <SingleColumnFormSectionUI fields={fields} handler={form.handler} /> </div> + {/* <pre>{JSON.stringify(form.status, undefined, 2)}</pre> */} + + <div class="flex items-center justify-end gap-x-6 mt-4"> + <button + type="button" + onClick={onClose} + class="block px-3 py-2 text-sm font-semibold leading-6 text-gray-900" + > + <i18n.Translate>Cancel</i18n.Translate> + </button> + + <button + type="button" + disabled={selected === undefined} + onClick={() => { + onRemove(); + }} + // onClick={() => { + // const newValue = [...list]; + // newValue.splice(selectedIndex, 1); + // onChange(newValue as any); + // // setSelectedIndex(undefined); + // }} + class="block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200" + > + <i18n.Translate>Remove</i18n.Translate> + </button> + + <button + type="button" + disabled={form.status.status !== "ok"} + onClick={() => { + onConfirm(form.status.result); + }} + class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-indigo-500 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200" + > + <i18n.Translate>Confirm</i18n.Translate> + </button> + </div> </div> ); } @@ -132,7 +111,8 @@ export function InputArray<T extends object, K extends keyof T>( const list = (value ?? []) as Array<Record<string, string | undefined>>; const [selectedIndex, setSelectedIndex] = useState<number | undefined>( - undefined, + // undefined, + 0, ); const selected = selectedIndex === undefined ? undefined : list[selectedIndex]; @@ -172,7 +152,7 @@ export function InputArray<T extends object, K extends keyof T>( {!state.disabled && ( <div class="pt-2"> <Option - label={"Add new..." as TranslatedString} + label={i18n.str`Add new...`} isSelected={selectedIndex === list.length} isLast isFirst @@ -191,41 +171,90 @@ export function InputArray<T extends object, K extends keyof T>( {selectedIndex !== undefined && ( <ArrayForm fields={fields} - onChange={(result) => { + onRemove={() => { const newValue = [...list]; - newValue.splice(selectedIndex, 1, result); + newValue.splice(selectedIndex, 1); onChange(newValue as any); + setSelectedIndex(undefined); + }} + onClose={() => { + setSelectedIndex(undefined); + }} + onConfirm={(value) => { + const newValue = [...list]; + newValue.splice(selectedIndex, 1, value); + onChange(newValue as any); + setSelectedIndex(undefined); }} selected={selected} /> )} - {selectedIndex !== undefined && ( - <div class="flex items-center justify-end gap-x-6"> - <button - type="button" - onClick={() => { - setSelectedIndex(undefined); - }} - class="block px-3 py-2 text-sm font-semibold leading-6 text-gray-900" - > - <i18n.Translate>Close</i18n.Translate> - </button> - - <button - type="button" - onClick={() => { - const newValue = [...list]; - newValue.splice(selectedIndex, 1); - onChange(newValue as any); - setSelectedIndex(undefined); - }} - class="block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 " - > - <i18n.Translate>Remove</i18n.Translate> - </button> - </div> - )} </div> </div> ); } + +function Option({ + label, + disabled, + isFirst, + isLast, + isSelected, + onClick, +}: { + label: TranslatedString; + isFirst?: boolean; + isLast?: boolean; + isSelected?: boolean; + disabled?: boolean; + onClick: () => void; +}): VNode { + let clazz = "relative flex border p-4 focus:outline-none disabled:text-grey"; + if (isFirst) { + clazz += " rounded-tl-md rounded-tr-md "; + } + if (isLast) { + clazz += " rounded-bl-md rounded-br-md "; + } + if (isSelected) { + clazz += " z-10 border-indigo-200 bg-indigo-50 "; + } else { + clazz += " border-gray-200"; + } + if (disabled) { + clazz += + " cursor-not-allowed bg-gray-50 text-gray-500 ring-gray-200 text-gray"; + } else { + clazz += " cursor-pointer"; + } + return ( + <label class={clazz}> + <input + type="radio" + name="privacy-setting" + checked={isSelected} + disabled={disabled} + onClick={onClick} + class="mt-0.5 h-4 w-4 shrink-0 text-indigo-600 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200 focus:ring-indigo-600" + aria-labelledby="privacy-setting-0-label" + aria-describedby="privacy-setting-0-description" + /> + <span class="ml-3 flex flex-col"> + <span + id="privacy-setting-0-label" + disabled + class="block text-sm font-medium" + > + {label} + </span> + {/* <!-- Checked: "text-indigo-700", Not Checked: "text-gray-500" --> */} + {/* <span + id="privacy-setting-0-description" + class="block text-sm" + > + This project would be available to anyone who has the link + </span> */} + </span> + </label> + ); +} diff --git a/packages/web-util/src/hooks/useForm.ts b/packages/web-util/src/hooks/useForm.ts @@ -205,7 +205,7 @@ export function validateRequiredFields<FormType>( function checkIfRequiredFieldHasValue(formElement: UIFormElementConfig) { if ("fields" in formElement) { - formElement.fields.forEach(checkIfRequiredFieldHasValue); + // formElement.fields.forEach(checkIfRequiredFieldHasValue); } if (!("id" in formElement)) { return; @@ -249,7 +249,7 @@ function constructFormHandler<T>( function notifyUpdateOnFieldChange(formElement: UIFormElementConfig): void { if ("fields" in formElement) { - formElement.fields.forEach(notifyUpdateOnFieldChange); + // formElement.fields.forEach(notifyUpdateOnFieldChange); } if (!("id" in formElement)) { return;