aboutsummaryrefslogtreecommitdiff
path: root/packages/exchange-backoffice-ui/src/handlers
diff options
context:
space:
mode:
Diffstat (limited to 'packages/exchange-backoffice-ui/src/handlers')
-rw-r--r--packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx43
-rw-r--r--packages/exchange-backoffice-ui/src/handlers/InputAmount.tsx34
-rw-r--r--packages/exchange-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx86
-rw-r--r--packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx18
-rw-r--r--packages/exchange-backoffice-ui/src/handlers/InputLine.tsx6
-rw-r--r--packages/exchange-backoffice-ui/src/handlers/forms.ts32
-rw-r--r--packages/exchange-backoffice-ui/src/handlers/useField.ts31
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 {