aboutsummaryrefslogtreecommitdiff
path: root/packages/merchant-backoffice-ui/src/components/form
diff options
context:
space:
mode:
Diffstat (limited to 'packages/merchant-backoffice-ui/src/components/form')
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputDate.tsx11
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx305
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx (renamed from packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx)97
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx4
4 files changed, 136 insertions, 281 deletions
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
index 1f41c3564..a398629dc 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
@@ -20,16 +20,18 @@
*/
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
-import { h, VNode } from "preact";
+import { ComponentChildren, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { DatePicker } from "../picker/DatePicker.js";
import { InputProps, useField } from "./useField.js";
+import { dateFormatForSettings, useSettings } from "../../hooks/useSettings.js";
export interface Props<T> extends InputProps<T> {
readonly?: boolean;
expand?: boolean;
//FIXME: create separated components InputDate and InputTimestamp
withTimestampSupport?: boolean;
+ side?: ComponentChildren;
}
export function InputDate<T>({
@@ -41,9 +43,11 @@ export function InputDate<T>({
tooltip,
expand,
withTimestampSupport,
+ side,
}: Props<keyof T>): VNode {
const [opened, setOpened] = useState(false);
const { i18n } = useTranslationContext();
+ const [settings] = useSettings()
const { error, required, value, onChange } = useField<T>(name);
@@ -51,14 +55,14 @@ export function InputDate<T>({
if (!value) {
strValue = withTimestampSupport ? "unknown" : "";
} else if (value instanceof Date) {
- strValue = format(value, "yyyy/MM/dd");
+ strValue = format(value, dateFormatForSettings(settings));
} else if (value.t_s) {
strValue =
value.t_s === "never"
? withTimestampSupport
? "never"
: ""
- : format(new Date(value.t_s * 1000), "yyyy/MM/dd");
+ : format(new Date(value.t_s * 1000), dateFormatForSettings(settings));
}
return (
@@ -142,6 +146,7 @@ export function InputDate<T>({
</button>
</span>
)}
+ {side}
</div>
<DatePicker
opened={opened}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
index 8d324660e..5cd69a0b3 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
@@ -18,9 +18,9 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { parsePaytoUri, PaytoUriGeneric, stringifyPaytoUri } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
-import { useCallback, useState } from "preact/hooks";
import { COUNTRY_TABLE } from "../../utils/constants.js";
import { undefinedIfEmpty } from "../../utils/table.js";
import { FormErrors, FormProvider } from "./FormProvider.js";
@@ -28,23 +28,23 @@ import { Input } from "./Input.js";
import { InputGroup } from "./InputGroup.js";
import { InputSelector } from "./InputSelector.js";
import { InputProps, useField } from "./useField.js";
-import { InputWithAddon } from "./InputWithAddon.js";
-import { MerchantBackend } from "../../declaration.js";
+import { useEffect, useState } from "preact/hooks";
export interface Props<T> extends InputProps<T> {
isValid?: (e: any) => boolean;
}
+// type Entity = PaytoUriGeneric
// https://datatracker.ietf.org/doc/html/rfc8905
type Entity = {
// iban, bitcoin, x-taler-bank. it defined the format
target: string;
// path1 if the first field to be used
- path1: string;
+ path1?: string;
// path2 if the second field to be used, optional
path2?: string;
- // options of the payto uri
- options: {
+ // params of the payto uri
+ params: {
"receiver-name"?: string;
sender?: string;
message?: string;
@@ -52,13 +52,6 @@ type Entity = {
instruction?: string;
[name: string]: string | undefined;
};
- auth: {
- type: "unset" | "basic" | "none";
- url?: string;
- username?: string;
- password?: string;
- repeat?: string;
- };
};
function isEthereumAddress(address: string) {
@@ -171,14 +164,10 @@ const targets = [
"bitcoin",
"ethereum",
];
-const accountAuthType = ["none", "basic"];
const noTargetValue = targets[0];
-const defaultTarget: Partial<Entity> = {
+const defaultTarget: Entity = {
target: noTargetValue,
- options: {},
- auth: {
- type: "unset" as const,
- },
+ params: {},
};
export function InputPaytoForm<T>({
@@ -187,110 +176,91 @@ export function InputPaytoForm<T>({
label,
tooltip,
}: Props<keyof T>): VNode {
- const { value: paytos, onChange, required } = useField<T>(name);
-
- const [value, valueHandler] = useState<Partial<Entity>>(defaultTarget);
+ const { value: initialValueStr, onChange } = useField<T>(name);
- let payToPath;
- if (value.target === "iban" && value.path1) {
- payToPath = `/${value.path1.toUpperCase()}`;
- } else if (value.path1) {
- if (value.path2) {
- payToPath = `/${value.path1}/${value.path2}`;
- } else {
- payToPath = `/${value.path1}`;
- }
+ const initialPayto = parsePaytoUri(initialValueStr ?? "")
+ const paths = !initialPayto ? [] : initialPayto.targetPath.split("/")
+ const initialPath1 = paths.length >= 1 ? paths[0] : undefined;
+ const initialPath2 = paths.length >= 2 ? paths[1] : undefined;
+ const initial: Entity = initialPayto === undefined ? defaultTarget : {
+ target: initialPayto.targetType,
+ params: initialPayto.params,
+ path1: initialPath1,
+ path2: initialPath2,
}
- const { i18n } = useTranslationContext();
+ const [value, setValue] = useState<Partial<Entity>>(initial)
- const ops = value.options ?? {};
- const url = tryUrl(`payto://${value.target}${payToPath}`);
- if (url) {
- Object.keys(ops).forEach((opt_key) => {
- const opt_value = ops[opt_key];
- if (opt_value) url.searchParams.set(opt_key, opt_value);
- });
- }
- const paytoURL = !url ? "" : url.href;
+ const { i18n } = useTranslationContext();
const errors: FormErrors<Entity> = {
target:
- value.target === noTargetValue && !paytos.length
+ value.target === noTargetValue
? i18n.str`required`
: undefined,
path1: !value.path1
? i18n.str`required`
: value.target === "iban"
- ? validateIBAN(value.path1, i18n)
- : value.target === "bitcoin"
- ? validateBitcoin(value.path1, i18n)
- : value.target === "ethereum"
- ? validateEthereum(value.path1, i18n)
- : undefined,
+ ? validateIBAN(value.path1, i18n)
+ : value.target === "bitcoin"
+ ? validateBitcoin(value.path1, i18n)
+ : value.target === "ethereum"
+ ? validateEthereum(value.path1, i18n)
+ : undefined,
path2:
value.target === "x-taler-bank"
? !value.path2
? i18n.str`required`
: undefined
: undefined,
- options: undefinedIfEmpty({
- "receiver-name": !value.options?.["receiver-name"]
+ params: undefinedIfEmpty({
+ "receiver-name": !value.params?.["receiver-name"]
? i18n.str`required`
: undefined,
}),
- auth: !value.auth
- ? undefined
- : undefinedIfEmpty({
- username:
- value.auth.type === "basic" && !value.auth.username
- ? i18n.str`required`
- : undefined,
- password:
- value.auth.type === "basic" && !value.auth.password
- ? i18n.str`required`
- : undefined,
- repeat:
- value.auth.type === "basic" && !value.auth.repeat
- ? i18n.str`required`
- : value.auth.repeat !== value.auth.password
- ? i18n.str`is not the same`
- : undefined,
- }),
};
const hasErrors = Object.keys(errors).some(
(k) => (errors as any)[k] !== undefined,
);
+ const str = hasErrors || !value.target ? undefined : stringifyPaytoUri({
+ targetType: value.target,
+ targetPath: value.path2 ? `${value.path1}/${value.path2}` : (value.path1 ?? ""),
+ params: value.params ?? {} as any,
+ isKnown: false,
+ })
+ useEffect(() => {
+ onChange(str as any)
+ }, [str])
- const submit = useCallback((): void => {
- const accounts: MerchantBackend.Instances.MerchantBankAccount[] = paytos;
- const alreadyExists =
- accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1;
- if (!alreadyExists) {
- const newValue: MerchantBackend.Instances.MerchantBankAccount = {
- payto_uri: paytoURL,
- };
- if (value.auth) {
- if (value.auth.url) {
- newValue.credit_facade_url = value.auth.url;
- }
- if (value.auth.type === "none") {
- newValue.credit_facade_credentials = {
- type: "none",
- };
- }
- if (value.auth.type === "basic") {
- newValue.credit_facade_credentials = {
- type: "basic",
- username: value.auth.username ?? "",
- password: value.auth.password ?? "",
- };
- }
- }
- onChange([newValue, ...accounts] as any);
- }
- valueHandler(defaultTarget);
- }, [value]);
+ // const submit = useCallback((): void => {
+ // // const accounts: MerchantBackend.BankAccounts.AccountAddDetails[] = paytos;
+ // // const alreadyExists =
+ // // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1;
+ // // if (!alreadyExists) {
+ // const newValue: MerchantBackend.BankAccounts.AccountAddDetails = {
+ // payto_uri: paytoURL,
+ // };
+ // if (value.auth) {
+ // if (value.auth.url) {
+ // newValue.credit_facade_url = value.auth.url;
+ // }
+ // if (value.auth.type === "none") {
+ // newValue.credit_facade_credentials = {
+ // type: "none",
+ // };
+ // }
+ // if (value.auth.type === "basic") {
+ // newValue.credit_facade_credentials = {
+ // type: "basic",
+ // username: value.auth.username ?? "",
+ // password: value.auth.password ?? "",
+ // };
+ // }
+ // }
+ // onChange(newValue as any);
+ // // }
+ // // valueHandler(defaultTarget);
+ // }, [value]);
//FIXME: translating plural singular
return (
@@ -299,11 +269,11 @@ export function InputPaytoForm<T>({
name="tax"
errors={errors}
object={value}
- valueHandler={valueHandler}
+ valueHandler={setValue}
>
<InputSelector<Entity>
name="target"
- label={i18n.str`Target type`}
+ label={i18n.str`Account type`}
tooltip={i18n.str`Method to use for wire transfer`}
values={targets}
toStr={(v) => (v === noTargetValue ? i18n.str`Choose one...` : v)}
@@ -400,150 +370,15 @@ export function InputPaytoForm<T>({
{value.target !== noTargetValue && (
<Fragment>
<Input
- name="options.receiver-name"
+ name="params.receiver-name"
label={i18n.str`Name`}
tooltip={i18n.str`Bank account owner's name.`}
/>
- <InputWithAddon
- name="auth.url"
- label={i18n.str`Account info URL`}
- help="https://bank.com"
- expand
- tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
- />
- <InputSelector
- name="auth.type"
- label={i18n.str`Auth type`}
- tooltip={i18n.str`Choose the authentication type for the account info URL`}
- values={accountAuthType}
- toStr={(str) => {
- // if (str === "unset") {
- // return "Without change";
- // }
- if (str === "none") return "Without authentication";
- return "Username and password";
- }}
- />
- {value.auth?.type === "basic" ? (
- <Fragment>
- <Input
- name="auth.username"
- label={i18n.str`Username`}
- tooltip={i18n.str`Username to access the account information.`}
- />
- <Input
- name="auth.password"
- inputType="password"
- label={i18n.str`Password`}
- tooltip={i18n.str`Password to access the account information.`}
- />
- <Input
- name="auth.repeat"
- inputType="password"
- label={i18n.str`Repeat password`}
- />
- </Fragment>
- ) : undefined}
-
- {/* <InputWithAddon
- name="options.credit_credentials"
- label={i18n.str`Account info`}
- inputType={showKey ? "text" : "password"}
- help="From where the merchant can download information about incoming wire transfers to this account"
- expand
- tooltip={i18n.str`Useful to validate the purchase`}
- fromStr={(v) => v.toUpperCase()}
- addonAfter={
- <span class="icon">
- {showKey ? (
- <i class="mdi mdi-eye" />
- ) : (
- <i class="mdi mdi-eye-off" />
- )}
- </span>
- }
- side={
- <span style={{ display: "flex" }}>
- <button
- data-tooltip={
- showKey
- ? i18n.str`show secret key`
- : i18n.str`hide secret key`
- }
- class="button is-info mr-3"
- onClick={(e) => {
- setShowKey(!showKey);
- }}
- >
- {showKey ? (
- <i18n.Translate>hide</i18n.Translate>
- ) : (
- <i18n.Translate>show</i18n.Translate>
- )}
- </button>
- </span>
- }
- /> */}
</Fragment>
)}
- {/**
- * Show the values in the list
- */}
- <div class="field is-horizontal">
- <div class="field-label is-normal" />
- <div class="field-body" style={{ display: "block" }}>
- {paytos.map(
- (v: MerchantBackend.Instances.MerchantBankAccount, i: number) => (
- <div
- key={i}
- class="tags has-addons mt-3 mb-0 mr-3"
- style={{ flexWrap: "nowrap" }}
- >
- <span
- class="tag is-medium is-info mb-0"
- style={{ maxWidth: "90%" }}
- >
- {v.payto_uri}
- </span>
- <a
- class="tag is-medium is-danger is-delete mb-0"
- onClick={() => {
- onChange(paytos.filter((f: any) => f !== v) as any);
- }}
- />
- </div>
- ),
- )}
- {!paytos.length && i18n.str`No accounts yet.`}
- {required && (
- <span class="icon has-text-danger is-right">
- <i class="mdi mdi-alert" />
- </span>
- )}
- </div>
- </div>
- {value.target !== noTargetValue && (
- <div class="buttons is-right mt-5">
- <button
- class="button is-info"
- data-tooltip={i18n.str`add tax to the tax list`}
- disabled={hasErrors}
- onClick={submit}
- >
- <i18n.Translate>Add</i18n.Translate>
- </button>
- </div>
- )}
</FormProvider>
</InputGroup>
);
}
-function tryUrl(s: string): URL | undefined {
- try {
- return new URL(s);
- } catch (e) {
- return undefined;
- }
-}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx
index 1c1fcb907..be5800d14 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx
@@ -22,32 +22,41 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import emptyImage from "../../assets/empty.png";
-import { MerchantBackend, WithId } from "../../declaration.js";
import { FormErrors, FormProvider } from "./FormProvider.js";
import { InputWithAddon } from "./InputWithAddon.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
-type Entity = MerchantBackend.Products.ProductDetail & WithId;
+type Entity = {
+ id: string,
+ description: string;
+ image?: string;
+ extra?: string;
+};
-export interface Props {
- selected?: Entity;
- onChange: (p?: Entity) => void;
- products: (MerchantBackend.Products.ProductDetail & WithId)[];
+export interface Props<T extends Entity> {
+ selected?: T;
+ onChange: (p?: T) => void;
+ label: TranslatedString;
+ list: T[];
+ withImage?: boolean;
}
-interface ProductSearch {
+interface Search {
name: string;
}
-export function InputSearchProduct({
+export function InputSearchOnList<T extends Entity>({
selected,
onChange,
- products,
-}: Props): VNode {
- const [prodForm, setProdName] = useState<Partial<ProductSearch>>({
+ label,
+ list,
+ withImage,
+}: Props<T>): VNode {
+ const [nameForm, setNameForm] = useState<Partial<Search>>({
name: "",
});
- const errors: FormErrors<ProductSearch> = {
+ const errors: FormErrors<Search> = {
name: undefined,
};
const { i18n } = useTranslationContext();
@@ -55,15 +64,17 @@ export function InputSearchProduct({
if (selected) {
return (
<article class="media">
- <figure class="media-left">
- <p class="image is-128x128">
- <img src={selected.image ? selected.image : emptyImage} />
- </p>
- </figure>
+ {withImage &&
+ <figure class="media-left">
+ <p class="image is-128x128">
+ <img src={selected.image ? selected.image : emptyImage} />
+ </p>
+ </figure>
+ }
<div class="media-content">
<div class="content">
<p class="media-meta">
- <i18n.Translate>Product id</i18n.Translate>: <b>{selected.id}</b>
+ <i18n.Translate>ID</i18n.Translate>: <b>{selected.id}</b>
</p>
<p>
<i18n.Translate>Description</i18n.Translate>:{" "}
@@ -84,15 +95,15 @@ export function InputSearchProduct({
}
return (
- <FormProvider<ProductSearch>
+ <FormProvider<Search>
errors={errors}
- object={prodForm}
- valueHandler={setProdName}
+ object={nameForm}
+ valueHandler={setNameForm}
>
- <InputWithAddon<ProductSearch>
+ <InputWithAddon<Search>
name="name"
- label={i18n.str`Product`}
- tooltip={i18n.str`search products by it's description or id`}
+ label={label}
+ tooltip={i18n.str`enter description or id`}
addonAfter={
<span class="icon">
<i class="mdi mdi-magnify" />
@@ -100,13 +111,14 @@ export function InputSearchProduct({
}
>
<div>
- <ProductList
- name={prodForm.name}
- list={products}
+ <DropdownList
+ name={nameForm.name}
+ list={list}
onSelect={(p) => {
- setProdName({ name: "" });
+ setNameForm({ name: "" });
onChange(p);
}}
+ withImage={!!withImage}
/>
</div>
</InputWithAddon>
@@ -114,13 +126,14 @@ export function InputSearchProduct({
);
}
-interface ProductListProps {
+interface DropdownListProps<T extends Entity> {
name?: string;
- onSelect: (p: MerchantBackend.Products.ProductDetail & WithId) => void;
- list: (MerchantBackend.Products.ProductDetail & WithId)[];
+ onSelect: (p: T) => void;
+ list: T[];
+ withImage: boolean;
}
-function ProductList({ name, onSelect, list }: ProductListProps) {
+function DropdownList<T extends Entity>({ name, onSelect, list, withImage }: DropdownListProps<T>) {
const { i18n } = useTranslationContext();
if (!name) {
/* FIXME
@@ -149,7 +162,7 @@ function ProductList({ name, onSelect, list }: ProductListProps) {
{!filtered.length ? (
<div class="dropdown-item">
<i18n.Translate>
- no products found with that description
+ no match found with that description or id
</i18n.Translate>
</div>
) : (
@@ -161,18 +174,20 @@ function ProductList({ name, onSelect, list }: ProductListProps) {
style={{ cursor: "pointer" }}
>
<article class="media">
- <div class="media-left">
- <div class="image" style={{ minWidth: 64 }}>
- <img
- src={p.image ? p.image : emptyImage}
- style={{ width: 64, height: 64 }}
- />
+ {withImage &&
+ <div class="media-left">
+ <div class="image" style={{ minWidth: 64 }}>
+ <img
+ src={p.image ? p.image : emptyImage}
+ style={{ width: 64, height: 64 }}
+ />
+ </div>
</div>
- </div>
+ }
<div class="media-content">
<div class="content">
<p>
- <strong>{p.id}</strong> <small>{p.price}</small>
+ <strong>{p.id}</strong> {p.extra !== undefined ? <small>{p.extra}</small> : undefined}
<br />
{p.description}
</p>
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
index 61ddf3c84..f95dfcd05 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
@@ -56,7 +56,7 @@ export function InputToggle<T>({
return (
<div class="field is-horizontal">
<div class="field-label is-normal">
- <label class="label" style={{ width: 200 }}>
+ <label class="label" >
{label}
{tooltip && (
<span class="icon has-tooltip-right" data-tooltip={tooltip}>
@@ -65,7 +65,7 @@ export function InputToggle<T>({
)}
</label>
</div>
- <div class="field-body is-flex-grow-1">
+ <div class="field-body is-flex-grow-3">
<div class="field">
<p class={expand ? "control is-expanded" : "control"}>
<label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}>