diff options
Diffstat (limited to 'packages/exchange-backoffice-ui/src/handlers')
7 files changed, 217 insertions, 33 deletions
diff --git a/packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx b/packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx index 87c4c43fb..4ac90ad57 100644 --- a/packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx @@ -1,6 +1,16 @@ -import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; +import { + AbsoluteTime, + AmountJson, + TranslatedString, +} from "@gnu-taler/taler-util"; import { ComponentChildren, VNode, createContext, h } from "preact"; -import { MutableRef, StateUpdater, useEffect, useRef } from "preact/hooks"; +import { + MutableRef, + StateUpdater, + useEffect, + useRef, + useState, +} from "preact/hooks"; export interface FormType<T> { value: MutableRef<Partial<T>>; @@ -15,6 +25,8 @@ export const FormContext = createContext<FormType<any>>({}); export type FormState<T> = { [field in keyof T]?: T[field] extends AbsoluteTime ? Partial<InputFieldState> + : T[field] extends AmountJson + ? Partial<InputFieldState> : T[field] extends Array<infer P> ? Partial<InputArrayFieldState<P>> : T[field] extends object @@ -40,22 +52,31 @@ export interface InputArrayFieldState<T> extends InputFieldState { export function FormProvider<T>({ children, initialValue, - onUpdate, + onUpdate: notify, onSubmit, computeFormState, }: { initialValue?: Partial<T>; onUpdate?: (v: Partial<T>) => void; - onSubmit: (v: T) => void; + onSubmit?: (v: T) => void; computeFormState?: (v: T) => FormState<T>; children: ComponentChildren; }): VNode { - const value = useRef(initialValue ?? {}); - useEffect(() => { - return function onUnload() { - value.current = initialValue ?? {}; - }; - }); + // const value = useRef(initialValue ?? {}); + // useEffect(() => { + // return function onUnload() { + // value.current = initialValue ?? {}; + // }; + // }); + // const onUpdate = notify + const [state, setState] = useState<Partial<T>>(initialValue ?? {}); + const value = { current: state }; + // console.log("RENDER", initialValue, value); + const onUpdate = (v: typeof state) => { + // console.log("updated"); + setState(v); + if (notify) notify(v); + }; return ( <FormContext.Provider value={{ initialValue, value, onUpdate, computeFormState }} @@ -64,7 +85,7 @@ export function FormProvider<T>({ onSubmit={(e) => { e.preventDefault(); //@ts-ignore - onSubmit(value.current); + if (onSubmit) onSubmit(value.current); }} > {children} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputAmount.tsx b/packages/exchange-backoffice-ui/src/handlers/InputAmount.tsx new file mode 100644 index 000000000..9be9dd4d0 --- /dev/null +++ b/packages/exchange-backoffice-ui/src/handlers/InputAmount.tsx @@ -0,0 +1,34 @@ +import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util"; +import { VNode, h } from "preact"; +import { InputLine, UIFormProps } from "./InputLine.js"; +import { useField } from "./useField.js"; + +export function InputAmount<T extends object, K extends keyof T>( + props: { currency?: string } & UIFormProps<T, K>, +): VNode { + const { value } = useField<T, K>(props.name); + const currency = + !value || !(value as any).currency + ? props.currency + : (value as any).currency; + return ( + <InputLine<T, K> + type="text" + before={{ + type: "text", + text: currency as TranslatedString, + }} + converter={{ + //@ts-ignore + fromStringUI: (v): AmountJson => { + return Amounts.parseOrThrow(`${currency}:${v}`); + }, + //@ts-ignore + toStringUI: (v: AmountJson) => { + return v === undefined ? "" : Amounts.stringifyValue(v); + }, + }} + {...props} + /> + ); +} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx b/packages/exchange-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx new file mode 100644 index 000000000..fdee35447 --- /dev/null +++ b/packages/exchange-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx @@ -0,0 +1,86 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; +import { Fragment, VNode, h } from "preact"; +import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; +import { useField } from "./useField.js"; + +export interface Choice<V> { + label: TranslatedString; + value: V; +} + +export function InputChoiceHorizontal<T extends object, K extends keyof T>( + props: { + choices: Choice<T[K]>[]; + } & UIFormProps<T, K>, +): VNode { + const { + choices, + name, + label, + tooltip, + help, + placeholder, + required, + before, + after, + converter, + } = props; + const { value, onChange, state, isDirty } = useField<T, K>(name); + if (state.hidden) { + return <Fragment />; + } + + return ( + <div class="sm:col-span-6"> + <LabelWithTooltipMaybeRequired + label={label} + required={required} + tooltip={tooltip} + /> + <fieldset class="mt-2"> + <div class="isolate inline-flex rounded-md shadow-sm"> + {choices.map((choice, idx) => { + const isFirst = idx === 0; + const isLast = idx === choices.length - 1; + let clazz = + "relative inline-flex items-center px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 focus:z-10"; + if (choice.value === value) { + clazz += + " text-white bg-indigo-600 hover:bg-indigo-500 ring-2 ring-indigo-600 hover:ring-indigo-500"; + } else { + clazz += " hover:bg-gray-100 border-gray-300"; + } + if (isFirst) { + clazz += " rounded-l-md"; + } else { + clazz += " -ml-px"; + } + if (isLast) { + clazz += " rounded-r-md"; + } + return ( + <button + type="button" + class={clazz} + onClick={(e) => { + onChange( + (value === choice.value ? undefined : choice.value) as T[K], + ); + }} + > + {(!converter + ? (choice.value as string) + : converter?.toStringUI(choice.value)) ?? ""} + </button> + ); + })} + </div> + </fieldset> + {help && ( + <p class="mt-2 text-sm text-gray-500" id="email-description"> + {help} + </p> + )} + </div> + ); +} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx b/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx index 3bce0123f..c37984368 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx @@ -3,15 +3,15 @@ import { Fragment, VNode, h } from "preact"; import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; import { useField } from "./useField.js"; -export interface Choice { +export interface Choice<V> { label: TranslatedString; description?: TranslatedString; - value: string; + value: V; } export function InputChoiceStacked<T extends object, K extends keyof T>( props: { - choices: Choice[]; + choices: Choice<T[K]>[]; } & UIFormProps<T, K>, ): VNode { const { @@ -41,6 +41,10 @@ export function InputChoiceStacked<T extends object, K extends keyof T>( <fieldset class="mt-2"> <div class="space-y-4"> {choices.map((choice) => { + // const currentValue = !converter + // ? choice.value + // : converter.fromStringUI(choice.value) ?? ""; + let clazz = "border relative block cursor-pointer rounded-lg bg-white px-6 py-4 shadow-sm focus:outline-none sm:flex sm:justify-between"; if (choice.value === value) { @@ -49,12 +53,18 @@ export function InputChoiceStacked<T extends object, K extends keyof T>( } else { clazz += " border-gray-300"; } + return ( <label class={clazz}> <input type="radio" name="server-size" - defaultValue={choice.value} + // defaultValue={choice.value} + value={ + (!converter + ? (choice.value as string) + : converter?.toStringUI(choice.value)) ?? "" + } onClick={(e) => { onChange( (value === choice.value diff --git a/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx b/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx index 8e847a273..9448ef5e4 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx @@ -250,7 +250,8 @@ export function InputLine<T extends object, K extends keyof T>( onChange(fromString(e.currentTarget.value)); }} placeholder={placeholder ? placeholder : undefined} - defaultValue={toString(value)} + value={toString(value) ?? ""} + // defaultValue={toString(value)} disabled={state.disabled} aria-invalid={showError} // aria-describedby="email-error" @@ -269,7 +270,8 @@ export function InputLine<T extends object, K extends keyof T>( onChange(fromString(e.currentTarget.value)); }} placeholder={placeholder ? placeholder : undefined} - defaultValue={toString(value)} + value={toString(value) ?? ""} + // defaultValue={toString(value)} disabled={state.disabled} aria-invalid={showError} // aria-describedby="email-error" diff --git a/packages/exchange-backoffice-ui/src/handlers/forms.ts b/packages/exchange-backoffice-ui/src/handlers/forms.ts index 115127cc3..4eb188a09 100644 --- a/packages/exchange-backoffice-ui/src/handlers/forms.ts +++ b/packages/exchange-backoffice-ui/src/handlers/forms.ts @@ -13,8 +13,10 @@ import { Group } from "./Group.js"; import { InputSelectOne } from "./InputSelectOne.js"; import { FormProvider } from "./FormProvider.js"; import { InputLine } from "./InputLine.js"; +import { InputAmount } from "./InputAmount.js"; +import { InputChoiceHorizontal } from "./InputChoiceHorizontal.js"; -export type DoubleColumnForm = DoubleColumnFormSection[]; +export type DoubleColumnForm = Array<DoubleColumnFormSection | undefined>; type DoubleColumnFormSection = { title: TranslatedString; @@ -35,8 +37,10 @@ type FieldType<T extends object = any, K extends keyof T = any> = { text: Parameters<typeof InputText<T, K>>[0]; textArea: Parameters<typeof InputTextArea<T, K>>[0]; choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0]; + choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0]; date: Parameters<typeof InputDate<T, K>>[0]; integer: Parameters<typeof InputInteger<T, K>>[0]; + amount: Parameters<typeof InputAmount<T, K>>[0]; }; /** @@ -47,11 +51,13 @@ export type UIFormField = | { type: "caption"; props: FieldType["caption"] } | { type: "array"; props: FieldType["array"] } | { type: "file"; props: FieldType["file"] } + | { type: "amount"; props: FieldType["amount"] } | { type: "selectOne"; props: FieldType["selectOne"] } | { type: "selectMultiple"; props: FieldType["selectMultiple"] } | { type: "text"; props: FieldType["text"] } | { type: "textArea"; props: FieldType["textArea"] } | { type: "choiceStacked"; props: FieldType["choiceStacked"] } + | { type: "choiceHorizontal"; props: FieldType["choiceHorizontal"] } | { type: "integer"; props: FieldType["integer"] } | { type: "date"; props: FieldType["date"] }; @@ -79,11 +85,15 @@ const UIFormConfiguration: UIFormFieldMap = { date: InputDate, //@ts-ignore choiceStacked: InputChoiceStacked, + //@ts-ignore + choiceHorizontal: InputChoiceHorizontal, integer: InputInteger, //@ts-ignore selectOne: InputSelectOne, //@ts-ignore selectMultiple: InputSelectMultiple, + //@ts-ignore + amount: InputAmount, }; export function RenderAllFieldsByUiConfig({ @@ -103,13 +113,23 @@ export function RenderAllFieldsByUiConfig({ ); } -type FormSet<T extends object, K extends keyof T = any> = { +type FormSet<T extends object> = { Provider: typeof FormProvider<T>; - InputLine: typeof InputLine<T, K>; + InputLine: <K extends keyof T>() => typeof InputLine<T, K>; + InputChoiceHorizontal: <K extends keyof T>() => typeof InputChoiceHorizontal< + T, + K + >; }; -export function createNewForm<T extends object>(): FormSet<T> { - return { +export function createNewForm<T extends object>() { + const res: FormSet<T> = { Provider: FormProvider, - InputLine: InputLine, + InputLine: () => InputLine, + InputChoiceHorizontal: () => InputChoiceHorizontal, + }; + return { + Provider: res.Provider, + InputLine: res.InputLine(), + InputChoiceHorizontal: res.InputChoiceHorizontal(), }; } diff --git a/packages/exchange-backoffice-ui/src/handlers/useField.ts b/packages/exchange-backoffice-ui/src/handlers/useField.ts index 60e65f435..bf94d2f5d 100644 --- a/packages/exchange-backoffice-ui/src/handlers/useField.ts +++ b/packages/exchange-backoffice-ui/src/handlers/useField.ts @@ -1,9 +1,5 @@ -import { TargetedEvent, useContext, useState } from "preact/compat"; -import { - FormContext, - InputArrayFieldState, - InputFieldState, -} from "./FormProvider.js"; +import { useContext, useState } from "preact/compat"; +import { FormContext, InputFieldState } from "./FormProvider.js"; export interface InputFieldHandler<Type> { value: Type; @@ -21,11 +17,13 @@ export function useField<T extends object, K extends keyof T>( computeFormState, onUpdate: notifyUpdate, } = useContext(FormContext); + type P = typeof name; type V = T[P]; const formState = computeFormState ? computeFormState(formValue.current) : {}; const fieldValue = readField(formValue.current, String(name)) as V; + // console.log("USE FIELD", String(name), formValue.current, fieldValue); const [currentValue, setCurrentValue] = useState<any | undefined>(fieldValue); const fieldState = readField<Partial<InputFieldState>>(formState, String(name)) ?? {}; @@ -66,10 +64,23 @@ export function useField<T extends object, K extends keyof T>( * @param name * @returns */ -function readField<T>(object: any, name: string): T | undefined { - return name - .split(".") - .reduce((prev, current) => prev && prev[current], object); +function readField<T>( + object: any, + name: string, + debug?: boolean, +): T | undefined { + return name.split(".").reduce((prev, current) => { + if (debug) { + console.log( + "READ", + name, + prev, + current, + prev ? prev[current] : undefined, + ); + } + return prev ? prev[current] : undefined; + }, object); } function setValueDeeper(object: any, names: string[], value: any): any { |