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 {  | 
