From fdbe623e1060efc4b074d213a96e8f5a2ab7498b Mon Sep 17 00:00:00 2001
From: Sebastian
Date: Wed, 20 Sep 2023 15:16:28 -0300
Subject: more ui stuff, moved forms to util
---
packages/web-util/src/forms/Caption.tsx | 32 +++
packages/web-util/src/forms/DefaultForm.tsx | 65 +++++
packages/web-util/src/forms/FormProvider.tsx | 99 ++++++++
packages/web-util/src/forms/Group.tsx | 41 +++
packages/web-util/src/forms/InputAmount.tsx | 34 +++
packages/web-util/src/forms/InputArray.tsx | 183 +++++++++++++
.../web-util/src/forms/InputChoiceHorizontal.tsx | 82 ++++++
packages/web-util/src/forms/InputChoiceStacked.tsx | 111 ++++++++
packages/web-util/src/forms/InputDate.tsx | 37 +++
packages/web-util/src/forms/InputFile.tsx | 101 ++++++++
packages/web-util/src/forms/InputInteger.tsx | 23 ++
packages/web-util/src/forms/InputLine.tsx | 282 +++++++++++++++++++++
.../web-util/src/forms/InputSelectMultiple.tsx | 151 +++++++++++
packages/web-util/src/forms/InputSelectOne.tsx | 134 ++++++++++
packages/web-util/src/forms/InputText.tsx | 8 +
packages/web-util/src/forms/InputTextArea.tsx | 8 +
packages/web-util/src/forms/forms.ts | 135 ++++++++++
packages/web-util/src/forms/index.ts | 19 ++
packages/web-util/src/forms/useField.ts | 93 +++++++
packages/web-util/src/hooks/index.ts | 3 +
packages/web-util/src/hooks/useNotifications.ts | 29 +--
packages/web-util/src/index.browser.ts | 1 +
22 files changed, 1656 insertions(+), 15 deletions(-)
create mode 100644 packages/web-util/src/forms/Caption.tsx
create mode 100644 packages/web-util/src/forms/DefaultForm.tsx
create mode 100644 packages/web-util/src/forms/FormProvider.tsx
create mode 100644 packages/web-util/src/forms/Group.tsx
create mode 100644 packages/web-util/src/forms/InputAmount.tsx
create mode 100644 packages/web-util/src/forms/InputArray.tsx
create mode 100644 packages/web-util/src/forms/InputChoiceHorizontal.tsx
create mode 100644 packages/web-util/src/forms/InputChoiceStacked.tsx
create mode 100644 packages/web-util/src/forms/InputDate.tsx
create mode 100644 packages/web-util/src/forms/InputFile.tsx
create mode 100644 packages/web-util/src/forms/InputInteger.tsx
create mode 100644 packages/web-util/src/forms/InputLine.tsx
create mode 100644 packages/web-util/src/forms/InputSelectMultiple.tsx
create mode 100644 packages/web-util/src/forms/InputSelectOne.tsx
create mode 100644 packages/web-util/src/forms/InputText.tsx
create mode 100644 packages/web-util/src/forms/InputTextArea.tsx
create mode 100644 packages/web-util/src/forms/forms.ts
create mode 100644 packages/web-util/src/forms/index.ts
create mode 100644 packages/web-util/src/forms/useField.ts
(limited to 'packages/web-util/src')
diff --git a/packages/web-util/src/forms/Caption.tsx b/packages/web-util/src/forms/Caption.tsx
new file mode 100644
index 000000000..8facddec3
--- /dev/null
+++ b/packages/web-util/src/forms/Caption.tsx
@@ -0,0 +1,32 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { VNode, h } from "preact";
+import {
+ LabelWithTooltipMaybeRequired
+} from "./InputLine.js";
+
+interface Props {
+ label: TranslatedString;
+ tooltip?: TranslatedString;
+ help?: TranslatedString;
+ before?: VNode;
+ after?: VNode;
+}
+
+export function Caption({ before, after, label, tooltip, help }: Props): VNode {
+ return (
+
+ {before !== undefined && (
+
{before}
+ )}
+
+ {after !== undefined && (
+
{after}
+ )}
+ {help && (
+
+ {help}
+
+ )}
+
+ );
+}
diff --git a/packages/web-util/src/forms/DefaultForm.tsx b/packages/web-util/src/forms/DefaultForm.tsx
new file mode 100644
index 000000000..92c379459
--- /dev/null
+++ b/packages/web-util/src/forms/DefaultForm.tsx
@@ -0,0 +1,65 @@
+
+import { ComponentChildren, Fragment, h } from "preact";
+import { FormProvider, FormState } from "./FormProvider.js";
+import { DoubleColumnForm, RenderAllFieldsByUiConfig } from "./forms.js";
+
+
+export interface FlexibleForm {
+ versionId: string;
+ design: DoubleColumnForm;
+ behavior: (form: Partial) => FormState;
+}
+
+export function DefaultForm({
+ initial,
+ onUpdate,
+ form,
+ onSubmit,
+ children,
+}: {
+ children?: ComponentChildren;
+ initial: Partial;
+ onSubmit?: (v: Partial) => void;
+ form: FlexibleForm;
+ onUpdate?: (d: Partial) => void;
+}) {
+ return (
+
+
+ {form.design.map((section, i) => {
+ if (!section) return
;
+ return (
+
+
+
+ {section.title}
+
+ {section.description && (
+
+ {section.description}
+
+ )}
+
+
+
+ );
+ })}
+
+ {children}
+
+ );
+}
diff --git a/packages/web-util/src/forms/FormProvider.tsx b/packages/web-util/src/forms/FormProvider.tsx
new file mode 100644
index 000000000..3da2a4f07
--- /dev/null
+++ b/packages/web-util/src/forms/FormProvider.tsx
@@ -0,0 +1,99 @@
+import {
+ AbsoluteTime,
+ AmountJson,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import { ComponentChildren, VNode, createContext, h } from "preact";
+import {
+ MutableRef,
+ StateUpdater,
+ useEffect,
+ useRef,
+ useState,
+} from "preact/hooks";
+
+export interface FormType {
+ value: MutableRef>;
+ initialValue?: Partial;
+ onUpdate?: StateUpdater;
+ computeFormState?: (v: T) => FormState;
+}
+
+//@ts-ignore
+export const FormContext = createContext>({});
+
+export type FormState = {
+ [field in keyof T]?: T[field] extends AbsoluteTime
+ ? Partial
+ : T[field] extends AmountJson
+ ? Partial
+ : T[field] extends Array
+ ? Partial>
+ : T[field] extends (object | undefined)
+ ? FormState
+ : Partial;
+};
+
+export interface InputFieldState {
+ /* should show the error */
+ error?: TranslatedString;
+ /* should not allow to edit */
+ readonly: boolean;
+ /* should show as disable */
+ disabled: boolean;
+ /* should not show */
+ hidden: boolean;
+}
+
+export interface InputArrayFieldState extends InputFieldState {
+ elements: FormState[];
+}
+
+export function FormProvider({
+ children,
+ initialValue,
+ onUpdate: notify,
+ onSubmit,
+ computeFormState,
+}: {
+ initialValue?: Partial;
+ onUpdate?: (v: Partial) => void;
+ onSubmit?: (v: Partial, s: FormState | undefined) => void;
+ computeFormState?: (v: Partial) => FormState;
+ children: ComponentChildren;
+}): VNode {
+ // const value = useRef(initialValue ?? {});
+ // useEffect(() => {
+ // return function onUnload() {
+ // value.current = initialValue ?? {};
+ // };
+ // });
+ // const onUpdate = notify
+ const [state, setState] = useState>(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 (
+
+
+
+ );
+}
diff --git a/packages/web-util/src/forms/Group.tsx b/packages/web-util/src/forms/Group.tsx
new file mode 100644
index 000000000..0645f6d97
--- /dev/null
+++ b/packages/web-util/src/forms/Group.tsx
@@ -0,0 +1,41 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { VNode, h } from "preact";
+import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
+import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
+
+interface Props {
+ before?: TranslatedString;
+ after?: TranslatedString;
+ tooltipBefore?: TranslatedString;
+ tooltipAfter?: TranslatedString;
+ fields: UIFormField[];
+}
+
+export function Group({
+ before,
+ after,
+ tooltipAfter,
+ tooltipBefore,
+ fields,
+}: Props): VNode {
+ return (
+
+
+ {before && (
+
+ )}
+
+
+
+
+
+ {after && (
+
+ )}
+
+
+ );
+}
diff --git a/packages/web-util/src/forms/InputAmount.tsx b/packages/web-util/src/forms/InputAmount.tsx
new file mode 100644
index 000000000..9be9dd4d0
--- /dev/null
+++ b/packages/web-util/src/forms/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(
+ props: { currency?: string } & UIFormProps,
+): VNode {
+ const { value } = useField(props.name);
+ const currency =
+ !value || !(value as any).currency
+ ? props.currency
+ : (value as any).currency;
+ return (
+
+ 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/web-util/src/forms/InputArray.tsx b/packages/web-util/src/forms/InputArray.tsx
new file mode 100644
index 000000000..00379bed6
--- /dev/null
+++ b/packages/web-util/src/forms/InputArray.tsx
@@ -0,0 +1,183 @@
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { FormProvider, InputArrayFieldState } from "./FormProvider.js";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
+import { useField } from "./useField.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
+
+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}
+
+ {/* */}
+ {/*
+ This project would be available to anyone who has the link
+ */}
+
+
+ );
+}
+
+export function InputArray(
+ props: {
+ fields: UIFormField[];
+ labelField: string;
+ } & UIFormProps,
+): VNode {
+ const { fields, labelField, name, label, required, tooltip } = props;
+ const { value, onChange, state } = useField(name);
+ const list = (value ?? []) as Array>;
+ const [selectedIndex, setSelected] = useState(undefined);
+ const selected =
+ selectedIndex === undefined ? undefined : list[selectedIndex];
+
+ return (
+
+
+
+
+ {list.map((v, idx) => {
+ return (
+
{
+ setSelected(selectedIndex === idx ? undefined : idx);
+ }}
+ />
+ );
+ })}
+
+ {
+ setSelected(
+ selectedIndex === list.length ? undefined : list.length,
+ );
+ }}
+ />
+
+
+ {selectedIndex !== undefined && (
+ /**
+ * This form provider act as a substate of the parent form
+ * Consider creating an InnerFormProvider since not every feature is expected
+ */
+
{
+ // 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];
+ }}
+ onSubmit={(v) => {
+ const newValue = [...list];
+ newValue.splice(selectedIndex, 1, v);
+ onChange(newValue as T[K]);
+ setSelected(undefined);
+ }}
+ onUpdate={(v) => {
+ const newValue = [...list];
+ newValue.splice(selectedIndex, 1, v);
+ onChange(newValue as T[K]);
+ }}
+ >
+
+
+ )}
+ {selectedIndex !== undefined && (
+
+
+ {selected !== undefined && (
+ {
+ const newValue = [...list];
+ newValue.splice(selectedIndex, 1);
+ onChange(newValue as T[K]);
+ setSelected(undefined);
+ }}
+ class="block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
+ >
+ Remove
+
+ )}
+
+
+ )}
+
+ );
+}
diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.tsx b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
new file mode 100644
index 000000000..5c909b5d7
--- /dev/null
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.tsx
@@ -0,0 +1,82 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { Fragment, VNode, h } from "preact";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { useField } from "./useField.js";
+import { Choice } from "./InputChoiceStacked.js";
+
+export function InputChoiceHorizontal(
+ props: {
+ choices: Choice[];
+ } & UIFormProps,
+): VNode {
+ const {
+ choices,
+ name,
+ label,
+ tooltip,
+ help,
+ placeholder,
+ required,
+ before,
+ after,
+ converter,
+ } = props;
+ const { value, onChange, state, isDirty } = useField(name);
+ if (state.hidden) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {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 (
+ {
+ onChange(
+ (value === choice.value ? undefined : choice.value) as T[K],
+ );
+ }}
+ >
+ {(!converter
+ ? (choice.value as string)
+ : converter?.toStringUI(choice.value)) ?? ""}
+
+ );
+ })}
+
+
+ {help && (
+
+ {help}
+
+ )}
+
+ );
+}
diff --git a/packages/web-util/src/forms/InputChoiceStacked.tsx b/packages/web-util/src/forms/InputChoiceStacked.tsx
new file mode 100644
index 000000000..c37984368
--- /dev/null
+++ b/packages/web-util/src/forms/InputChoiceStacked.tsx
@@ -0,0 +1,111 @@
+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 {
+ label: TranslatedString;
+ description?: TranslatedString;
+ value: V;
+}
+
+export function InputChoiceStacked(
+ props: {
+ choices: Choice[];
+ } & UIFormProps,
+): VNode {
+ const {
+ choices,
+ name,
+ label,
+ tooltip,
+ help,
+ placeholder,
+ required,
+ before,
+ after,
+ converter,
+ } = props;
+ const { value, onChange, state, isDirty } = useField(name);
+ if (state.hidden) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {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) {
+ clazz +=
+ " border-transparent border-indigo-600 ring-2 ring-indigo-600";
+ } else {
+ clazz += " border-gray-300";
+ }
+
+ return (
+
+ {
+ onChange(
+ (value === choice.value
+ ? undefined
+ : choice.value) as T[K],
+ );
+ }}
+ class="sr-only"
+ aria-labelledby="server-size-0-label"
+ aria-describedby="server-size-0-description-0 server-size-0-description-1"
+ />
+
+
+
+ {choice.label}
+
+ {choice.description !== undefined && (
+
+
+ {choice.description}
+
+
+ )}
+
+
+
+ );
+ })}
+
+
+ {help && (
+
+ {help}
+
+ )}
+
+ );
+}
diff --git a/packages/web-util/src/forms/InputDate.tsx b/packages/web-util/src/forms/InputDate.tsx
new file mode 100644
index 000000000..1fd81aad9
--- /dev/null
+++ b/packages/web-util/src/forms/InputDate.tsx
@@ -0,0 +1,37 @@
+import { AbsoluteTime } from "@gnu-taler/taler-util";
+import { InputLine, UIFormProps } from "./InputLine.js";
+import { CalendarIcon } from "@heroicons/react/24/outline";
+import { VNode, h } from "preact";
+import { format, parse } from "date-fns";
+
+export function InputDate(
+ props: { pattern?: string } & UIFormProps,
+): VNode {
+ const pattern = props.pattern ?? "dd/MM/yyyy";
+ return (
+
+ type="text"
+ after={{
+ type: "icon",
+ icon: ,
+ }}
+ converter={{
+ //@ts-ignore
+ fromStringUI: (v): AbsoluteTime => {
+ if (!v) return AbsoluteTime.never();
+ const t_ms = parse(v, pattern, Date.now()).getTime();
+ return AbsoluteTime.fromMilliseconds(t_ms);
+ },
+ //@ts-ignore
+ toStringUI: (v: AbsoluteTime) => {
+ return !v || !v.t_ms
+ ? ""
+ : v.t_ms === "never"
+ ? "never"
+ : format(v.t_ms, pattern);
+ },
+ }}
+ {...props}
+ />
+ );
+}
diff --git a/packages/web-util/src/forms/InputFile.tsx b/packages/web-util/src/forms/InputFile.tsx
new file mode 100644
index 000000000..0d89a98a3
--- /dev/null
+++ b/packages/web-util/src/forms/InputFile.tsx
@@ -0,0 +1,101 @@
+import { Fragment, VNode, h } from "preact";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export function InputFile(
+ props: { maxBites: number; accept?: string } & UIFormProps,
+): VNode {
+ const {
+ name,
+ label,
+ placeholder,
+ tooltip,
+ required,
+ help,
+ maxBites,
+ accept,
+ } = props;
+ const { value, onChange, state } = useField(name);
+
+ if (state.hidden) {
+ return
;
+ }
+ return (
+
+
+ {!value || !(value as string).startsWith("data:image/") ? (
+
+
+
+
+
+
+
+ Upload a file
+ {
+ const f: FileList | null = e.currentTarget.files;
+ if (!f || f.length != 1) {
+ return onChange(undefined!);
+ }
+ if (f[0].size > maxBites) {
+ return onChange(undefined!);
+ }
+ return f[0].arrayBuffer().then((b) => {
+ const b64 = window.btoa(
+ new Uint8Array(b).reduce(
+ (data, byte) => data + String.fromCharCode(byte),
+ "",
+ ),
+ );
+ return onChange(`data:${f[0].type};base64,${b64}` as any);
+ });
+ }}
+ />
+
+ {/*
or drag and drop
*/}
+
+
+
+ ) : (
+
+
+
+
{
+ onChange(undefined!);
+ }}
+ >
+ Clear
+
+
+ )}
+ {help &&
{help}
}
+
+ );
+}
diff --git a/packages/web-util/src/forms/InputInteger.tsx b/packages/web-util/src/forms/InputInteger.tsx
new file mode 100644
index 000000000..fb04e3852
--- /dev/null
+++ b/packages/web-util/src/forms/InputInteger.tsx
@@ -0,0 +1,23 @@
+import { VNode, h } from "preact";
+import { InputLine, UIFormProps } from "./InputLine.js";
+
+export function InputInteger(
+ props: UIFormProps,
+): VNode {
+ return (
+ {
+ return !v ? 0 : Number.parseInt(v, 10);
+ },
+ //@ts-ignore
+ toStringUI: (v?: number): string => {
+ return v === undefined ? "" : String(v);
+ },
+ }}
+ {...props}
+ />
+ );
+}
diff --git a/packages/web-util/src/forms/InputLine.tsx b/packages/web-util/src/forms/InputLine.tsx
new file mode 100644
index 000000000..9448ef5e4
--- /dev/null
+++ b/packages/web-util/src/forms/InputLine.tsx
@@ -0,0 +1,282 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useField } from "./useField.js";
+
+export interface IconAddon {
+ type: "icon";
+ icon: VNode;
+}
+interface ButtonAddon {
+ type: "button";
+ onClick: () => void;
+ children: ComponentChildren;
+}
+interface TextAddon {
+ type: "text";
+ text: TranslatedString;
+}
+type Addon = IconAddon | ButtonAddon | TextAddon;
+
+interface StringConverter {
+ toStringUI: (v?: T) => string;
+ fromStringUI: (v?: string) => T;
+}
+
+export interface UIFormProps {
+ name: K;
+ label: TranslatedString;
+ placeholder?: TranslatedString;
+ tooltip?: TranslatedString;
+ help?: TranslatedString;
+ before?: Addon;
+ after?: Addon;
+ required?: boolean;
+ converter?: StringConverter;
+}
+
+export type FormErrors = {
+ [P in keyof T]?: string | FormErrors;
+};
+
+//@ts-ignore
+const TooltipIcon = (
+
+
+
+);
+
+export function LabelWithTooltipMaybeRequired({
+ label,
+ required,
+ tooltip,
+}: {
+ label: TranslatedString;
+ required?: boolean;
+ tooltip?: TranslatedString;
+}): VNode {
+ const Label = (
+
+
+
+ {label}
+
+
+
+ );
+ const WithTooltip = tooltip ? (
+
+ {Label}
+
+ {TooltipIcon}
+
+
+
+ ) : (
+ Label
+ );
+ if (required) {
+ return (
+
+ {WithTooltip}
+ *
+
+ );
+ }
+ return WithTooltip;
+}
+
+function InputWrapper({
+ children,
+ label,
+ tooltip,
+ before,
+ after,
+ help,
+ error,
+ required,
+}: { error?: string; children: ComponentChildren } & UIFormProps): VNode {
+ return (
+
+
+
+ {before &&
+ (before.type === "text" ? (
+
+ {before.text}
+
+ ) : before.type === "icon" ? (
+
+ {before.icon}
+
+ ) : before.type === "button" ? (
+
+ {before.children}
+
+ ) : undefined)}
+
+ {children}
+
+ {after &&
+ (after.type === "text" ? (
+
+ {after.text}
+
+ ) : after.type === "icon" ? (
+
+ {after.icon}
+
+ ) : after.type === "button" ? (
+
+ {after.children}
+
+ ) : undefined)}
+
+ {error && (
+
+ {error}
+
+ )}
+ {help && (
+
+ {help}
+
+ )}
+
+ );
+}
+
+function defaultToString(v: unknown) {
+ return v === undefined ? "" : typeof v !== "object" ? String(v) : "";
+}
+function defaultFromString(v: string) {
+ return v;
+}
+
+type InputType = "text" | "text-area" | "password" | "email" | "number";
+
+export function InputLine(
+ props: { type: InputType } & UIFormProps,
+): VNode {
+ const { name, placeholder, before, after, converter, type } = props;
+ const { value, onChange, state, isDirty } = useField(name);
+
+ if (state.hidden) return
;
+
+ let clazz =
+ "block w-full rounded-md border-0 py-1.5 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200";
+ if (before) {
+ switch (before.type) {
+ case "icon": {
+ clazz += " pl-10";
+ break;
+ }
+ case "button": {
+ clazz += " rounded-none rounded-r-md ";
+ break;
+ }
+ case "text": {
+ clazz += " min-w-0 flex-1 rounded-r-md rounded-none ";
+ break;
+ }
+ }
+ }
+ if (after) {
+ switch (after.type) {
+ case "icon": {
+ clazz += " pr-10";
+ break;
+ }
+ case "button": {
+ clazz += " rounded-none rounded-l-md";
+ break;
+ }
+ case "text": {
+ clazz += " min-w-0 flex-1 rounded-l-md rounded-none ";
+ break;
+ }
+ }
+ }
+ const showError = isDirty && state.error;
+ if (showError) {
+ clazz +=
+ " text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500";
+ } else {
+ clazz +=
+ " text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-indigo-600";
+ }
+ const fromString: (s: string) => any =
+ converter?.fromStringUI ?? defaultFromString;
+ const toString: (s: any) => string = converter?.toStringUI ?? defaultToString;
+
+ if (type === "text-area") {
+ return (
+
+ {...props}
+ error={showError ? state.error : undefined}
+ >
+
+ );
+ }
+
+ return (
+ {...props} error={showError ? state.error : undefined}>
+ {
+ onChange(fromString(e.currentTarget.value));
+ }}
+ placeholder={placeholder ? placeholder : undefined}
+ value={toString(value) ?? ""}
+ // defaultValue={toString(value)}
+ disabled={state.disabled}
+ aria-invalid={showError}
+ // aria-describedby="email-error"
+ class={clazz}
+ />
+
+ );
+}
diff --git a/packages/web-util/src/forms/InputSelectMultiple.tsx b/packages/web-util/src/forms/InputSelectMultiple.tsx
new file mode 100644
index 000000000..8116bdc03
--- /dev/null
+++ b/packages/web-util/src/forms/InputSelectMultiple.tsx
@@ -0,0 +1,151 @@
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Choice } from "./InputChoiceStacked.js";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export function InputSelectMultiple(
+ props: {
+ choices: Choice[];
+ unique?: boolean;
+ max?: number;
+ } & UIFormProps,
+): VNode {
+ const { name, label, choices, placeholder, tooltip, required, unique, max } =
+ props;
+ const { value, onChange } = useField(name);
+
+ const [filter, setFilter] = useState(undefined);
+ const regex = new RegExp(`.*${filter}.*`, "i");
+ const choiceMap = choices.reduce((prev, curr) => {
+ return { ...prev, [curr.value as string]: curr.label };
+ }, {} as Record);
+
+ const list = (value ?? []) as string[];
+ const filteredChoices =
+ filter === undefined
+ ? undefined
+ : choices.filter((v) => {
+ return regex.test(v.label);
+ });
+ return (
+
+
+ {list.map((v, idx) => {
+ return (
+
+ {choiceMap[v]}
+ {
+ const newValue = [...list];
+ newValue.splice(idx, 1);
+ onChange(newValue as T[K]);
+ setFilter(undefined);
+ }}
+ class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20"
+ >
+ Remove
+
+
+
+
+
+
+ );
+ })}
+
+
+
{
+ setFilter(e.currentTarget.value);
+ }}
+ placeholder={placeholder}
+ class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ role="combobox"
+ aria-controls="options"
+ aria-expanded="false"
+ />
+
{
+ setFilter(filter === undefined ? "" : undefined);
+ }}
+ class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
+ >
+
+
+
+
+
+ {filteredChoices !== undefined && (
+
+ {filteredChoices.map((v, idx) => {
+ return (
+ {
+ setFilter(undefined);
+ if (unique && list.indexOf(v.value as string) !== -1) {
+ return;
+ }
+ if (max !== undefined && list.length >= max) {
+ return;
+ }
+ const newValue = [...list];
+ newValue.splice(0, 0, v.value as string);
+ onChange(newValue as T[K]);
+ }}
+
+ // tabindex="-1"
+ >
+ {/* */}
+ {v.label}
+
+ {/* */}
+
+ );
+ })}
+
+ {/* */}
+
+ {/* */}
+
+ )}
+
+
+ );
+}
diff --git a/packages/web-util/src/forms/InputSelectOne.tsx b/packages/web-util/src/forms/InputSelectOne.tsx
new file mode 100644
index 000000000..7bef1058b
--- /dev/null
+++ b/packages/web-util/src/forms/InputSelectOne.tsx
@@ -0,0 +1,134 @@
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { Choice } from "./InputChoiceStacked.js";
+import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js";
+import { useField } from "./useField.js";
+
+export function InputSelectOne(
+ props: {
+ choices: Choice[];
+ } & UIFormProps,
+): VNode {
+ const { name, label, choices, placeholder, tooltip, required } = props;
+ const { value, onChange } = useField(name);
+
+ const [filter, setFilter] = useState(undefined);
+ const regex = new RegExp(`.*${filter}.*`, "i");
+ const choiceMap = choices.reduce((prev, curr) => {
+ return { ...prev, [curr.value as string]: curr.label };
+ }, {} as Record);
+
+ const filteredChoices =
+ filter === undefined
+ ? undefined
+ : choices.filter((v) => {
+ return regex.test(v.label);
+ });
+ return (
+
+
+ {value ? (
+
+ {choiceMap[value as string]}
+ {
+ onChange(undefined!);
+ }}
+ class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20"
+ >
+ Remove
+
+
+
+
+
+
+ ) : (
+
+
{
+ setFilter(e.currentTarget.value);
+ }}
+ placeholder={placeholder}
+ class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ role="combobox"
+ aria-controls="options"
+ aria-expanded="false"
+ />
+
{
+ setFilter(filter === undefined ? "" : undefined);
+ }}
+ class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none"
+ >
+
+
+
+
+
+ {filteredChoices !== undefined && (
+
+ {filteredChoices.map((v, idx) => {
+ return (
+ {
+ setFilter(undefined);
+ onChange(v.value as T[K]);
+ }}
+
+ // tabindex="-1"
+ >
+ {/* */}
+ {v.label}
+
+ {/* */}
+
+ );
+ })}
+
+ {/* */}
+
+ {/* */}
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/packages/web-util/src/forms/InputText.tsx b/packages/web-util/src/forms/InputText.tsx
new file mode 100644
index 000000000..1b37ee6fb
--- /dev/null
+++ b/packages/web-util/src/forms/InputText.tsx
@@ -0,0 +1,8 @@
+import { VNode, h } from "preact";
+import { InputLine, UIFormProps } from "./InputLine.js";
+
+export function InputText(
+ props: UIFormProps,
+): VNode {
+ return ;
+}
diff --git a/packages/web-util/src/forms/InputTextArea.tsx b/packages/web-util/src/forms/InputTextArea.tsx
new file mode 100644
index 000000000..45229951e
--- /dev/null
+++ b/packages/web-util/src/forms/InputTextArea.tsx
@@ -0,0 +1,8 @@
+import { VNode, h } from "preact";
+import { InputLine, UIFormProps } from "./InputLine.js";
+
+export function InputTextArea(
+ props: UIFormProps,
+): VNode {
+ return ;
+}
diff --git a/packages/web-util/src/forms/forms.ts b/packages/web-util/src/forms/forms.ts
new file mode 100644
index 000000000..2c90a69ed
--- /dev/null
+++ b/packages/web-util/src/forms/forms.ts
@@ -0,0 +1,135 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { InputText } from "./InputText.js";
+import { InputDate } from "./InputDate.js";
+import { InputInteger } from "./InputInteger.js";
+import { h as create, Fragment, VNode } from "preact";
+import { InputChoiceStacked } from "./InputChoiceStacked.js";
+import { InputArray } from "./InputArray.js";
+import { InputSelectMultiple } from "./InputSelectMultiple.js";
+import { InputTextArea } from "./InputTextArea.js";
+import { InputFile } from "./InputFile.js";
+import { Caption } from "./Caption.js";
+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 = Array;
+
+export type DoubleColumnFormSection = {
+ title: TranslatedString;
+ description?: TranslatedString;
+ fields: UIFormField[];
+};
+
+/**
+ * Constrain the type with the ui props
+ */
+type FieldType = {
+ group: Parameters[0];
+ caption: Parameters[0];
+ array: Parameters>[0];
+ file: Parameters>[0];
+ selectOne: Parameters>[0];
+ selectMultiple: Parameters>[0];
+ text: Parameters>[0];
+ textArea: Parameters>[0];
+ choiceStacked: Parameters>[0];
+ choiceHorizontal: Parameters>[0];
+ date: Parameters>[0];
+ integer: Parameters>[0];
+ amount: Parameters>[0];
+};
+
+/**
+ * List all the form fields so typescript can type-check the form instance
+ */
+export type UIFormField =
+ | { type: "group"; props: FieldType["group"] }
+ | { 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"] };
+
+type FieldComponentFunction = (
+ props: FieldType[key],
+) => VNode;
+
+type UIFormFieldMap = {
+ [key in keyof FieldType]: FieldComponentFunction;
+};
+
+/**
+ * Maps input type with component implementation
+ */
+const UIFormConfiguration: UIFormFieldMap = {
+ group: Group,
+ caption: Caption,
+ //@ts-ignore
+ array: InputArray,
+ text: InputText,
+ //@ts-ignore
+ file: InputFile,
+ textArea: InputTextArea,
+ //@ts-ignore
+ 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({
+ fields,
+}: {
+ fields: UIFormField[];
+}): VNode {
+ return create(
+ Fragment,
+ {},
+ fields.map((field, i) => {
+ const Component = UIFormConfiguration[
+ field.type
+ ] as FieldComponentFunction;
+ return Component(field.props);
+ }),
+ );
+}
+
+type FormSet = {
+ Provider: typeof FormProvider;
+ InputLine: () => typeof InputLine;
+ InputChoiceHorizontal: () => typeof InputChoiceHorizontal<
+ T,
+ K
+ >;
+};
+export function createNewForm() {
+ const res: FormSet = {
+ Provider: FormProvider,
+ InputLine: () => InputLine,
+ InputChoiceHorizontal: () => InputChoiceHorizontal,
+ };
+ return {
+ Provider: res.Provider,
+ InputLine: res.InputLine(),
+ InputChoiceHorizontal: res.InputChoiceHorizontal(),
+ };
+}
diff --git a/packages/web-util/src/forms/index.ts b/packages/web-util/src/forms/index.ts
new file mode 100644
index 000000000..08bb9ee77
--- /dev/null
+++ b/packages/web-util/src/forms/index.ts
@@ -0,0 +1,19 @@
+export * from "./Caption.js"
+export * from "./FormProvider.js"
+export * from "./forms.js"
+export * from "./Group.js"
+export * from "./index.js"
+export * from "./InputAmount.js"
+export * from "./InputArray.js"
+export * from "./InputChoiceHorizontal.js"
+export * from "./InputChoiceStacked.js"
+export * from "./InputDate.js"
+export * from "./InputFile.js"
+export * from "./InputInteger.js"
+export * from "./InputLine.js"
+export * from "./InputSelectMultiple.js"
+export * from "./InputSelectOne.js"
+export * from "./InputTextArea.js"
+export * from "./InputText.js"
+export * from "./useField.js"
+export * from "./DefaultForm.js"
diff --git a/packages/web-util/src/forms/useField.ts b/packages/web-util/src/forms/useField.ts
new file mode 100644
index 000000000..bf94d2f5d
--- /dev/null
+++ b/packages/web-util/src/forms/useField.ts
@@ -0,0 +1,93 @@
+import { useContext, useState } from "preact/compat";
+import { FormContext, InputFieldState } from "./FormProvider.js";
+
+export interface InputFieldHandler {
+ value: Type;
+ onChange: (s: Type) => void;
+ state: InputFieldState;
+ isDirty: boolean;
+}
+
+export function useField(
+ name: K,
+): InputFieldHandler {
+ const {
+ initialValue,
+ value: formValue,
+ 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(fieldValue);
+ const fieldState =
+ readField>(formState, String(name)) ?? {};
+
+ //compute default state
+ const state = {
+ disabled: fieldState.disabled ?? false,
+ readonly: fieldState.readonly ?? false,
+ hidden: fieldState.hidden ?? false,
+ error: fieldState.error,
+ elements: "elements" in fieldState ? fieldState.elements ?? [] : [],
+ };
+
+ function onChange(value: V): void {
+ setCurrentValue(value);
+ formValue.current = setValueDeeper(
+ formValue.current,
+ String(name).split("."),
+ value,
+ );
+ if (notifyUpdate) {
+ notifyUpdate(formValue.current);
+ }
+ }
+
+ return {
+ value: fieldValue,
+ onChange,
+ isDirty: currentValue !== undefined,
+ state,
+ };
+}
+
+/**
+ * read the field of an object an support accessing it using '.'
+ *
+ * @param object
+ * @param name
+ * @returns
+ */
+function readField(
+ 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 {
+ if (names.length === 0) return value;
+ const [head, ...rest] = names;
+ if (object === undefined) {
+ return { [head]: setValueDeeper({}, rest, value) };
+ }
+ return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) };
+}
diff --git a/packages/web-util/src/hooks/index.ts b/packages/web-util/src/hooks/index.ts
index a3a2053e6..c29de9023 100644
--- a/packages/web-util/src/hooks/index.ts
+++ b/packages/web-util/src/hooks/index.ts
@@ -5,6 +5,9 @@ export {
useNotifications,
notifyError,
notifyInfo,
+ notify,
+ ErrorNotification,
+ InfoNotification
} from "./useNotifications.js";
export {
useAsyncAsHook,
diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts
index 733950592..52e626b38 100644
--- a/packages/web-util/src/hooks/useNotifications.ts
+++ b/packages/web-util/src/hooks/useNotifications.ts
@@ -4,13 +4,13 @@ import { memoryMap } from "../index.browser.js";
export type NotificationMessage = ErrorNotification | InfoNotification;
-interface ErrorNotification {
+export interface ErrorNotification {
type: "error";
title: TranslatedString;
description?: TranslatedString;
debug?: string;
}
-interface InfoNotification {
+export interface InfoNotification {
type: "info";
title: TranslatedString;
}
@@ -18,30 +18,29 @@ interface InfoNotification {
const storage = memoryMap>();
const NOTIFICATION_KEY = "notification";
+export function notify(notif: NotificationMessage): void {
+ const currentState: Map =
+ storage.get(NOTIFICATION_KEY) ?? new Map();
+ const newState = currentState.set(hash(notif), notif);
+ storage.set(NOTIFICATION_KEY, newState);
+}
export function notifyError(
title: TranslatedString,
description: TranslatedString | undefined,
debug?: any,
) {
- const currentState: Map =
- storage.get(NOTIFICATION_KEY) ?? new Map();
-
- const notif = {
+ notify({
type: "error" as const,
title,
description,
debug,
- };
- const newState = currentState.set(hash(notif), notif);
- storage.set(NOTIFICATION_KEY, newState);
+ });
}
export function notifyInfo(title: TranslatedString) {
- const currentState: Map =
- storage.get(NOTIFICATION_KEY) ?? new Map();
-
- const notif = { type: "info" as const, title };
- const newState = currentState.set(hash(notif), notif);
- storage.set(NOTIFICATION_KEY, newState);
+ notify({
+ type: "info" as const,
+ title,
+ });
}
type Notification = {
diff --git a/packages/web-util/src/index.browser.ts b/packages/web-util/src/index.browser.ts
index 2a537b405..82c399bfd 100644
--- a/packages/web-util/src/index.browser.ts
+++ b/packages/web-util/src/index.browser.ts
@@ -5,4 +5,5 @@ export * from "./utils/http-impl.sw.js";
export * from "./utils/observable.js";
export * from "./context/index.js";
export * from "./components/index.js";
+export * from "./forms/index.js";
export { renderStories, parseGroupImport } from "./stories.js";
--
cgit v1.2.3
From 5640f0a67dcc31fa2b5fa0992abca8a55bc00dd8 Mon Sep 17 00:00:00 2001
From: Sebastian
Date: Fri, 22 Sep 2023 18:34:35 -0300
Subject: default to content type json
---
packages/web-util/src/utils/request.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'packages/web-util/src')
diff --git a/packages/web-util/src/utils/request.ts b/packages/web-util/src/utils/request.ts
index 8ce21b0e1..ef4d8e847 100644
--- a/packages/web-util/src/utils/request.ts
+++ b/packages/web-util/src/utils/request.ts
@@ -48,7 +48,7 @@ export async function defaultRequestHandler(
)}`;
}
requestHeaders["Content-Type"] =
- options.contentType === "json" ? "application/json" : "text/plain";
+ !options.contentType || options.contentType === "json" ? "application/json" : "text/plain";
if (options.talerAmlOfficerSignature) {
requestHeaders["Taler-AML-Officer-Signature"] =
--
cgit v1.2.3
From fd9ed97fdc60baaa3f74a3f7b02c6831c6d6948b Mon Sep 17 00:00:00 2001
From: Sebastian
Date: Mon, 25 Sep 2023 09:31:04 -0300
Subject: do not reuse the same map instance
---
packages/web-util/src/hooks/useNotifications.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'packages/web-util/src')
diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts
index 52e626b38..2f9df24f9 100644
--- a/packages/web-util/src/hooks/useNotifications.ts
+++ b/packages/web-util/src/hooks/useNotifications.ts
@@ -53,7 +53,7 @@ export function useNotifications(): Notification[] {
useEffect(() => {
return storage.onUpdate(NOTIFICATION_KEY, () => {
const mem = storage.get(NOTIFICATION_KEY) ?? new Map();
- setter(mem);
+ setter(structuredClone(mem));
});
});
--
cgit v1.2.3
From ea0738ccd585445d7e2080d9009025dde9cf22c5 Mon Sep 17 00:00:00 2001
From: Sebastian
Date: Mon, 25 Sep 2023 14:49:47 -0300
Subject: better /config error
---
.../demobank-ui/src/components/ErrorLoading.tsx | 3 ++
.../src/components/Transactions/state.ts | 2 +-
packages/demobank-ui/src/components/app.tsx | 18 +++++++---
packages/demobank-ui/src/declaration.d.ts | 3 +-
packages/demobank-ui/src/hooks/config.ts | 40 +++++++++-------------
.../demobank-ui/src/pages/AccountPage/state.ts | 4 +--
packages/demobank-ui/src/pages/BankFrame.tsx | 14 ++++++--
.../demobank-ui/src/pages/WithdrawalQRCode.tsx | 1 -
packages/web-util/src/hooks/index.ts | 1 +
packages/web-util/src/hooks/useNotifications.ts | 11 ++++++
10 files changed, 60 insertions(+), 37 deletions(-)
(limited to 'packages/web-util/src')
diff --git a/packages/demobank-ui/src/components/ErrorLoading.tsx b/packages/demobank-ui/src/components/ErrorLoading.tsx
index a4faa4d5d..f83b61234 100644
--- a/packages/demobank-ui/src/components/ErrorLoading.tsx
+++ b/packages/demobank-ui/src/components/ErrorLoading.tsx
@@ -32,6 +32,9 @@ export function ErrorLoading({ error }: { error: HttpError{error.message}
+
+
Got status "{error.info.status}" on {error.info.url}
+
);
diff --git a/packages/demobank-ui/src/components/Transactions/state.ts b/packages/demobank-ui/src/components/Transactions/state.ts
index 30c48aa45..4b62b005e 100644
--- a/packages/demobank-ui/src/components/Transactions/state.ts
+++ b/packages/demobank-ui/src/components/Transactions/state.ts
@@ -44,7 +44,7 @@ export function useComponentState({ account }: Props): State {
cp.targetType === "bitcoin" ? `${cp.targetPath.substring(0, 6)}...` : undefined) ??
"unkown";
- const when = AbsoluteTime.fromMilliseconds(tx.date / 1000);
+ const when = AbsoluteTime.fromProtocolTimestamp(tx.date);
const amount = Amounts.parse(tx.amount);
const subject = tx.subject;
return {
diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx
index ebda31035..a587c6f1e 100644
--- a/packages/demobank-ui/src/components/app.tsx
+++ b/packages/demobank-ui/src/components/app.tsx
@@ -29,6 +29,8 @@ import { useEffect, useState } from "preact/hooks";
import { Loading } from "./Loading.js";
import { getInitialBackendBaseURL } from "../hooks/backend.js";
import { BANK_INTEGRATION_PROTOCOL_VERSION, useConfigState } from "../hooks/config.js";
+import { ErrorLoading } from "./ErrorLoading.js";
+import { BankFrame } from "../pages/BankFrame.js";
const WITH_LOCAL_STORAGE_CACHE = false;
/**
@@ -76,12 +78,18 @@ function VersionCheck({ children }: { children: ComponentChildren }): VNode {
if (checked === undefined) {
return
}
- if (checked === false) {
- return
- the bank backend is not supported. supported version "{BANK_INTEGRATION_PROTOCOL_VERSION}"
-
+ if (typeof checked === "string") {
+ return
+ the bank backend is not supported. supported version "{BANK_INTEGRATION_PROTOCOL_VERSION}", server version "{checked}"
+
}
- return {children}
+ if (checked === true) {
+ return {children}
+ }
+
+ return
+
+
}
function localStorageProvider(): Map {
diff --git a/packages/demobank-ui/src/declaration.d.ts b/packages/demobank-ui/src/declaration.d.ts
index 8d729c1f7..d3d9e02ef 100644
--- a/packages/demobank-ui/src/declaration.d.ts
+++ b/packages/demobank-ui/src/declaration.d.ts
@@ -205,8 +205,7 @@ namespace SandboxBackend {
// Transaction unique ID. Matches
// $transaction_id from the URI.
row_id: number;
- date: number;
- // date: Timestamp;
+ date: Timestamp;
}
interface CreateBankAccountTransactionCreate {
diff --git a/packages/demobank-ui/src/hooks/config.ts b/packages/demobank-ui/src/hooks/config.ts
index 4b22e8ad3..4cf677d35 100644
--- a/packages/demobank-ui/src/hooks/config.ts
+++ b/packages/demobank-ui/src/hooks/config.ts
@@ -1,5 +1,5 @@
import { LibtoolVersion } from "@gnu-taler/taler-util";
-import { useApiContext } from "@gnu-taler/web-util/browser";
+import { ErrorType, HttpError, HttpResponseServerError, RequestError, useApiContext } from "@gnu-taler/web-util/browser";
import { useEffect, useState } from "preact/hooks";
import { getInitialBackendBaseURL } from "./backend.js";
@@ -12,38 +12,32 @@ export const BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0";
async function getConfigState(
request: ReturnType["request"],
-): Promise {
- try {
- const url = getInitialBackendBaseURL();
- const result = await request(
- url,
- `config`,
- );
- return result.data;
- } catch (error) {
- return undefined;
- }
+): Promise {
+ const url = getInitialBackendBaseURL();
+ const result = await request(url, `config`);
+ return result.data;
}
-export function useConfigState(): boolean | undefined {
- const [checked, setChecked] = useState()
+export function useConfigState(): undefined | true | string | HttpError {
+ const [checked, setChecked] = useState>()
const { request } = useApiContext();
useEffect(() => {
-
getConfigState(request)
- .then((result) => {
- if (!result) {
- setChecked(false)
+ .then((s) => {
+ const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, s.version)
+ if (r?.compatible) {
+ setChecked(true);
} else {
- const r = LibtoolVersion.compare(BANK_INTEGRATION_PROTOCOL_VERSION, result.version)
- setChecked(r?.compatible);
+ setChecked(s.version)
}
})
- .catch((error) => {
- setChecked(false);
+ .catch((error: unknown) => {
+ if (error instanceof RequestError) {
+ setChecked(error.cause);
+ }
});
- });
+ }, []);
return checked;
}
diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts
index c212e7484..ca7e1d447 100644
--- a/packages/demobank-ui/src/pages/AccountPage/state.ts
+++ b/packages/demobank-ui/src/pages/AccountPage/state.ts
@@ -75,9 +75,7 @@ export function useComponentState({ account, goToBusinessAccount, goToConfirmOpe
};
}
- // FIXME: balance
- const balanceIsDebit = true;
- // data.balance.credit_debit_indicator == "debit";
+ const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
const limit = balanceIsDebit
? Amounts.sub(debitThreshold, balance).amount
: Amounts.add(balance, debitThreshold).amount;
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx
index c4f872679..5c43d2c3e 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -15,7 +15,7 @@
*/
import { Amounts, Logger, PaytoUriIBAN, TranslatedString, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
-import { notifyError, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { notifyError, notifyException, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { StateUpdater, useEffect, useErrorBoundary, useState } from "preact/hooks";
import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js";
@@ -54,7 +54,12 @@ export function BankFrame({
useEffect(() => {
if (error) {
- notifyError(i18n.str`Internal error, please report.`, (error instanceof Error ? error.message : String(error)) as TranslatedString)
+ const desc = (error instanceof Error ? error.stack : String(error)) as TranslatedString
+ if (error instanceof Error) {
+ notifyException(i18n.str`Internal error, please report.`, error)
+ } else {
+ notifyError(i18n.str`Internal error, please report.`, String(error) as TranslatedString)
+ }
resetError()
}
}, [error])
@@ -386,6 +391,11 @@ function StatusBanner(): VNode {
{n.message.description}
}
+ {n.message.debug &&
+
+ {n.message.debug}
+
+ }
case "info":
return
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index 25c571e28..8f4e175f6 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -45,7 +45,6 @@ export function WithdrawalQRCode({
withdrawUri,
onClose,
}: Props): VNode {
- const [settings, updateSettings] = useSettings();
const { i18n } = useTranslationContext();
const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId);
diff --git a/packages/web-util/src/hooks/index.ts b/packages/web-util/src/hooks/index.ts
index c29de9023..cc3267dbd 100644
--- a/packages/web-util/src/hooks/index.ts
+++ b/packages/web-util/src/hooks/index.ts
@@ -4,6 +4,7 @@ export { useMemoryStorage } from "./useMemoryStorage.js";
export {
useNotifications,
notifyError,
+ notifyException,
notifyInfo,
notify,
ErrorNotification,
diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts
index 2f9df24f9..792095b06 100644
--- a/packages/web-util/src/hooks/useNotifications.ts
+++ b/packages/web-util/src/hooks/useNotifications.ts
@@ -36,6 +36,17 @@ export function notifyError(
debug,
});
}
+export function notifyException(
+ title: TranslatedString,
+ ex: Error,
+) {
+ notify({
+ type: "error" as const,
+ title,
+ description: ex.message as TranslatedString,
+ debug: ex.stack,
+ });
+}
export function notifyInfo(title: TranslatedString) {
notify({
type: "info" as const,
--
cgit v1.2.3
From c10f3f3aded637111176487f95403294b1164633 Mon Sep 17 00:00:00 2001
From: Sebastian
Date: Fri, 29 Sep 2023 16:01:59 -0300
Subject: notifications exposed
---
packages/web-util/src/hooks/index.ts | 10 +---------
packages/web-util/src/hooks/useNotifications.ts | 2 +-
2 files changed, 2 insertions(+), 10 deletions(-)
(limited to 'packages/web-util/src')
diff --git a/packages/web-util/src/hooks/index.ts b/packages/web-util/src/hooks/index.ts
index cc3267dbd..f6c74ff22 100644
--- a/packages/web-util/src/hooks/index.ts
+++ b/packages/web-util/src/hooks/index.ts
@@ -1,15 +1,7 @@
export { useLang } from "./useLang.js";
export { useLocalStorage, buildStorageKey } from "./useLocalStorage.js";
export { useMemoryStorage } from "./useMemoryStorage.js";
-export {
- useNotifications,
- notifyError,
- notifyException,
- notifyInfo,
- notify,
- ErrorNotification,
- InfoNotification
-} from "./useNotifications.js";
+export * from "./useNotifications.js";
export {
useAsyncAsHook,
HookError,
diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts
index 792095b06..e9e8a240b 100644
--- a/packages/web-util/src/hooks/useNotifications.ts
+++ b/packages/web-util/src/hooks/useNotifications.ts
@@ -54,7 +54,7 @@ export function notifyInfo(title: TranslatedString) {
});
}
-type Notification = {
+export type Notification = {
message: NotificationMessage;
remove: () => void;
};
--
cgit v1.2.3
From 851b2da39c3297ede3d267f3d2534cac213261c1 Mon Sep 17 00:00:00 2001
From: Sebastian
Date: Wed, 4 Oct 2023 14:36:03 -0300
Subject: fixing issues reported by Christian, wip
---
.../src/pages/PaytoWireTransferForm.tsx | 4 +-
.../demobank-ui/src/pages/WithdrawalQRCode.tsx | 2 +-
.../merchant-backoffice-ui/src/Application.tsx | 4 +-
.../merchant-backoffice-ui/src/InstanceRoutes.tsx | 30 ++++++-------
.../src/components/form/InputStock.tsx | 5 +--
.../src/components/menu/SideBar.tsx | 29 ++++++++----
.../src/components/menu/index.tsx | 8 ++--
.../src/components/product/ProductForm.tsx | 6 +--
.../merchant-backoffice-ui/src/context/backend.ts | 26 +++++++++--
.../merchant-backoffice-ui/src/declaration.d.ts | 4 +-
packages/merchant-backoffice-ui/src/hooks/index.ts | 3 +-
packages/merchant-backoffice-ui/src/hooks/otp.ts | 4 +-
.../merchant-backoffice-ui/src/hooks/product.ts | 17 ++++++-
.../src/paths/instance/products/list/Table.tsx | 13 +++---
.../src/paths/instance/products/list/index.tsx | 52 +++++++++++++++++++++-
.../paths/instance/reserves/create/CreatePage.tsx | 27 +++++++----
.../instance/validators/create/CreatePage.tsx | 6 +--
.../validators/create/CreatedSuccessfully.tsx | 2 +-
.../instance/validators/update/UpdatePage.tsx | 2 +-
.../src/paths/instance/validators/update/index.tsx | 2 +-
.../src/paths/login/index.tsx | 17 +++----
.../merchant-backoffice-ui/src/schemas/index.ts | 3 +-
.../merchant-backoffice-ui/src/utils/constants.ts | 2 +-
packages/web-util/src/index.build.ts | 2 +-
24 files changed, 185 insertions(+), 85 deletions(-)
(limited to 'packages/web-util/src')
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index 69a9a07b2..52dbd4ff6 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -363,14 +363,14 @@ export function PaytoWireTransferForm({
*/
export function doAutoFocus(element: HTMLElement | null) {
if (element) {
- window.requestIdleCallback(() => {
+ setTimeout(() => {
element.focus()
element.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "center"
})
- })
+ }, 100)
}
}
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index 2d90b410c..91c5da718 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -150,7 +150,7 @@ export function WithdrawalQRCode({
details={{
account,
reserve: data.selected_reserve_pub,
- amount: Amounts.parseOrThrow("usd:10.00")
+ amount: Amounts.parseOrThrow(data.amount)
}}
onAborted={() => {
notifyInfo(i18n.str`Operation canceled`);
diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx
index 1a7617643..f0a7de53b 100644
--- a/packages/merchant-backoffice-ui/src/Application.tsx
+++ b/packages/merchant-backoffice-ui/src/Application.tsx
@@ -60,7 +60,7 @@ export function Application(): VNode {
* @returns
*/
function ApplicationStatusRoutes(): VNode {
- const { url: backendURL, updateToken, changeBackend } = useBackendContext();
+ const { changeBackend, selected: backendSelected } = useBackendContext();
const result = useBackendConfig();
const { i18n } = useTranslationContext();
@@ -69,7 +69,7 @@ function ApplicationStatusRoutes(): VNode {
: { currency: "unknown", version: "unknown" };
const ctx = useMemo(() => ({ currency, version }), [currency, version]);
- if (!backendURL) {
+ if (!backendSelected) {
return (
diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
index c2a9d3b18..f5372db8d 100644
--- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
+++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
@@ -87,9 +87,9 @@ export enum InstancePaths {
bank_update = "/bank/:bid/update",
bank_new = "/bank/new",
- product_list = "/products",
- product_update = "/product/:pid/update",
- product_new = "/product/new",
+ inventory_list = "/inventory",
+ inventory_update = "/inventory/:pid/update",
+ inventory_new = "/inventory/new",
order_list = "/orders",
order_new = "/order/new",
@@ -347,42 +347,42 @@ export function InstanceRoutes({
onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
/>
{/**
- * Product pages
+ * Inventory pages
*/}
{
- route(InstancePaths.product_new);
+ route(InstancePaths.inventory_new);
}}
onSelect={(id: string) => {
- route(InstancePaths.product_update.replace(":pid", id));
+ route(InstancePaths.inventory_update.replace(":pid", id));
}}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
/>
{
- route(InstancePaths.product_list);
+ route(InstancePaths.inventory_list);
}}
onBack={() => {
- route(InstancePaths.product_list);
+ route(InstancePaths.inventory_list);
}}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
/>
{
- route(InstancePaths.product_list);
+ route(InstancePaths.inventory_list);
}}
onBack={() => {
- route(InstancePaths.product_list);
+ route(InstancePaths.inventory_list);
}}
/>
{/**
@@ -405,7 +405,7 @@ export function InstanceRoutes({
path={InstancePaths.bank_update}
component={BankAccountUpdatePage}
onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.product_list)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)}
onConfirm={() => {
route(InstancePaths.bank_list);
}}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx
index 012d14977..1d18685c5 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx
@@ -212,10 +212,7 @@ export function InputStock({
withTimestampSupport
/>
-
- name="address"
- label={i18n.str`Delivery address`}
- >
+ name="address" label={i18n.str`Warehouse address`}>
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
index 3d5f20c85..402134096 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -49,7 +49,7 @@ export function Sidebar({
isPasswordOk
}: Props): VNode {
const config = useConfigContext();
- const { url: backendURL } = useBackendContext()
+ const { url: backendURL, resetBackend } = useBackendContext()
const { i18n } = useTranslationContext();
const kycStatus = useInstanceKYCDetails();
const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
@@ -80,7 +80,7 @@ export function Sidebar({
- {isPasswordOk && instance ? (
+ {instance ? (
- {isPasswordOk && admin && !mimic && (
+ {admin && !mimic && (
)}
- {isPasswordOk &&
+ {isPasswordOk ?
Log out
-
- }
+ :
+
+ resetBackend()}
+ >
+
+
+
+
+
+
+ }
diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
index cb318906f..b8ac2c9ab 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
@@ -30,11 +30,11 @@ function getInstanceTitle(path: string, id: string): string {
return `${id}: Orders`;
case InstancePaths.order_new:
return `${id}: New order`;
- case InstancePaths.product_list:
- return `${id}: Products`;
- case InstancePaths.product_new:
+ case InstancePaths.inventory_list:
+ return `${id}: Inventory`;
+ case InstancePaths.inventory_new:
return `${id}: New product`;
- case InstancePaths.product_update:
+ case InstancePaths.inventory_update:
return `${id}: Update product`;
case InstancePaths.reserves_new:
return `${id}: New reserve`;
diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
index 8bebbd298..e91e8c876 100644
--- a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
@@ -146,9 +146,9 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
/>
name="minimum_age"
- label={i18n.str`Age restricted`}
+ label={i18n.str`Age restriction`}
tooltip={i18n.str`is this product restricted for customer below certain age?`}
- help={i18n.str`can be overridden by the order configuration`}
+ help={i18n.str`minimum age of the buyer`}
/>
name="unit"
@@ -165,7 +165,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
name="stock"
label={i18n.str`Stock`}
alreadyExist={alreadyExist}
- tooltip={i18n.str`product inventory for products with finite supply (for internal use only)`}
+ tooltip={i18n.str`inventory for products with finite supply (for internal use only)`}
/>
name="taxes"
diff --git a/packages/merchant-backoffice-ui/src/context/backend.ts b/packages/merchant-backoffice-ui/src/context/backend.ts
index 056f9a192..d4a9abd5f 100644
--- a/packages/merchant-backoffice-ui/src/context/backend.ts
+++ b/packages/merchant-backoffice-ui/src/context/backend.ts
@@ -20,35 +20,55 @@
*/
import { createContext, h, VNode } from "preact";
-import { useContext } from "preact/hooks";
+import { useContext, useState } from "preact/hooks";
import { LoginToken } from "../declaration.js";
import { useBackendDefaultToken, useBackendURL } from "../hooks/index.js";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import { codecForBoolean } from "@gnu-taler/taler-util";
interface BackendContextType {
url: string,
+ selected: boolean;
token?: LoginToken;
updateToken: (token: LoginToken | undefined) => void;
changeBackend: (url: string) => void;
+ resetBackend: () => void;
}
const BackendContext = createContext({
url: "",
+ selected: false,
token: undefined,
updateToken: () => null,
changeBackend: () => null,
+ resetBackend: () => null,
});
+const BACKEND_SELECTED = buildStorageKey("backend-selected", codecForBoolean());
+
function useBackendContextState(
defaultUrl?: string,
): BackendContextType {
- const [url, changeBackend] = useBackendURL(defaultUrl);
+ const [url, changeBackend2] = useBackendURL(defaultUrl);
const [token, updateToken] = useBackendDefaultToken();
+ const {value, update} = useLocalStorage(BACKEND_SELECTED)
+
+ function changeBackend(s:string) {
+ changeBackend2(s)
+ update(true)
+ }
+
+ function resetBackend() {
+ update(false)
+ }
return {
url,
token,
+ selected: value ?? false,
updateToken,
- changeBackend
+ changeBackend,
+ resetBackend
};
}
diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts
index c3e6ea3da..dc53e3e83 100644
--- a/packages/merchant-backoffice-ui/src/declaration.d.ts
+++ b/packages/merchant-backoffice-ui/src/declaration.d.ts
@@ -1327,7 +1327,7 @@ export namespace MerchantBackend {
otp_device_id: string;
// Human-readable description for the device.
- otp_description: string;
+ otp_device_description: string;
// A base64-encoded key
otp_key: string;
@@ -1341,7 +1341,7 @@ export namespace MerchantBackend {
interface OtpDevicePatchDetails {
// Human-readable description for the device.
- otp_description: string;
+ otp_device_description: string;
// A base64-encoded key
otp_key: string | undefined;
diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts
index ee696779f..498e4eb78 100644
--- a/packages/merchant-backoffice-ui/src/hooks/index.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/index.ts
@@ -31,7 +31,8 @@ const calculateRootPath = () => {
typeof window !== undefined
? window.location.origin + window.location.pathname
: "/";
- return rootPath;
+
+ return rootPath.replace("webui/","");
};
const loginTokenCodec = buildCodecForObject()
diff --git a/packages/merchant-backoffice-ui/src/hooks/otp.ts b/packages/merchant-backoffice-ui/src/hooks/otp.ts
index 3544b4881..93eefeea5 100644
--- a/packages/merchant-backoffice-ui/src/hooks/otp.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/otp.ts
@@ -30,13 +30,13 @@ const useSWR = _useSWR as unknown as SWRHook;
const MOCKED_DEVICES: Record = {
"1": {
- otp_description: "first device",
+ otp_device_description: "first device",
otp_algorithm: 1,
otp_device_id: "1",
otp_key: "123",
},
"2": {
- otp_description: "second device",
+ otp_device_description: "second device",
otp_algorithm: 0,
otp_device_id: "2",
otp_key: "456",
diff --git a/packages/merchant-backoffice-ui/src/hooks/product.ts b/packages/merchant-backoffice-ui/src/hooks/product.ts
index 8ecaefaa6..e06ea8ed8 100644
--- a/packages/merchant-backoffice-ui/src/hooks/product.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/product.ts
@@ -26,6 +26,9 @@ import _useSWR, { SWRHook, useSWRConfig } from "swr";
const useSWR = _useSWR as unknown as SWRHook;
export interface ProductAPI {
+ getProduct: (
+ id: string,
+ ) => Promise;
createProduct: (
data: MerchantBackend.Products.ProductAddDetail,
) => Promise;
@@ -66,7 +69,7 @@ export function useProductAPI(): ProductAPI {
data,
});
- return await mutateAll(/.*"\/private\/products.*/);
+ return await mutateAll(/.*\/private\/products.*/);
};
const deleteProduct = async (productId: string): Promise => {
@@ -88,7 +91,17 @@ export function useProductAPI(): ProductAPI {
return await mutateAll(/.*"\/private\/products.*/);
};
- return { createProduct, updateProduct, deleteProduct, lockProduct };
+ const getProduct = async (
+ productId: string,
+ ): Promise => {
+ await request(`/private/products/${productId}`, {
+ method: "GET",
+ });
+
+ return
+ };
+
+ return { createProduct, updateProduct, deleteProduct, lockProduct, getProduct };
}
export function useInstanceProducts(): HttpResponse<
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx
index cbfe1d573..db73217ed 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx
@@ -66,7 +66,7 @@ export function CardTable({
- Products
+ Inventory