From e1d86816a7c07cb8ca2d54676d5cdbbe513f2ba7 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 4 Sep 2023 14:17:55 -0300 Subject: backoffcie new version, lot of changes --- .../src/components/form/InputDate.tsx | 11 +- .../src/components/form/InputPaytoForm.tsx | 305 +++++---------------- .../src/components/form/InputSearchOnList.tsx | 204 ++++++++++++++ .../src/components/form/InputSearchProduct.tsx | 189 ------------- .../src/components/form/InputToggle.tsx | 4 +- 5 files changed, 284 insertions(+), 429 deletions(-) create mode 100644 packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx delete mode 100644 packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx (limited to 'packages/merchant-backoffice-ui/src/components/form') 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 extends InputProps { readonly?: boolean; expand?: boolean; //FIXME: create separated components InputDate and InputTimestamp withTimestampSupport?: boolean; + side?: ComponentChildren; } export function InputDate({ @@ -41,9 +43,11 @@ export function InputDate({ tooltip, expand, withTimestampSupport, + side, }: Props): VNode { const [opened, setOpened] = useState(false); const { i18n } = useTranslationContext(); + const [settings] = useSettings() const { error, required, value, onChange } = useField(name); @@ -51,14 +55,14 @@ export function InputDate({ 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({ )} + {side} extends InputProps { 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 = { +const defaultTarget: Entity = { target: noTargetValue, - options: {}, - auth: { - type: "unset" as const, - }, + params: {}, }; export function InputPaytoForm({ @@ -187,110 +176,91 @@ export function InputPaytoForm({ label, tooltip, }: Props): VNode { - const { value: paytos, onChange, required } = useField(name); - - const [value, valueHandler] = useState>(defaultTarget); + const { value: initialValueStr, onChange } = useField(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>(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 = { 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({ name="tax" errors={errors} object={value} - valueHandler={valueHandler} + valueHandler={setValue} > 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({ {value.target !== noTargetValue && ( - - { - // if (str === "unset") { - // return "Without change"; - // } - if (str === "none") return "Without authentication"; - return "Username and password"; - }} - /> - {value.auth?.type === "basic" ? ( - - - - - - ) : undefined} - - {/* v.toUpperCase()} - addonAfter={ - - {showKey ? ( - - ) : ( - - )} - - } - side={ - - - - } - /> */} )} - {/** - * Show the values in the list - */} -
- - {value.target !== noTargetValue && ( -
- -
- )} ); } -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/InputSearchOnList.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx new file mode 100644 index 000000000..be5800d14 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx @@ -0,0 +1,204 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +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 { FormErrors, FormProvider } from "./FormProvider.js"; +import { InputWithAddon } from "./InputWithAddon.js"; +import { TranslatedString } from "@gnu-taler/taler-util"; + +type Entity = { + id: string, + description: string; + image?: string; + extra?: string; +}; + +export interface Props { + selected?: T; + onChange: (p?: T) => void; + label: TranslatedString; + list: T[]; + withImage?: boolean; +} + +interface Search { + name: string; +} + +export function InputSearchOnList({ + selected, + onChange, + label, + list, + withImage, +}: Props): VNode { + const [nameForm, setNameForm] = useState>({ + name: "", + }); + + const errors: FormErrors = { + name: undefined, + }; + const { i18n } = useTranslationContext(); + + if (selected) { + return ( +
+ {withImage && +
+

+ +

+
+ } +
+
+

+ ID: {selected.id} +

+

+ Description:{" "} + {selected.description} +

+
+ +
+
+
+
+ ); + } + + return ( + + errors={errors} + object={nameForm} + valueHandler={setNameForm} + > + + name="name" + label={label} + tooltip={i18n.str`enter description or id`} + addonAfter={ + + + + } + > +
+ { + setNameForm({ name: "" }); + onChange(p); + }} + withImage={!!withImage} + /> +
+ + + ); +} + +interface DropdownListProps { + name?: string; + onSelect: (p: T) => void; + list: T[]; + withImage: boolean; +} + +function DropdownList({ name, onSelect, list, withImage }: DropdownListProps) { + const { i18n } = useTranslationContext(); + if (!name) { + /* FIXME + this BR is added to occupy the space that will be added when the + dropdown appears + */ + return ( +
+
+
+ ); + } + const filtered = list.filter( + (p) => p.id.includes(name) || p.description.includes(name), + ); + + return ( + + ); +} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx deleted file mode 100644 index 1c1fcb907..000000000 --- a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx +++ /dev/null @@ -1,189 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -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"; - -type Entity = MerchantBackend.Products.ProductDetail & WithId; - -export interface Props { - selected?: Entity; - onChange: (p?: Entity) => void; - products: (MerchantBackend.Products.ProductDetail & WithId)[]; -} - -interface ProductSearch { - name: string; -} - -export function InputSearchProduct({ - selected, - onChange, - products, -}: Props): VNode { - const [prodForm, setProdName] = useState>({ - name: "", - }); - - const errors: FormErrors = { - name: undefined, - }; - const { i18n } = useTranslationContext(); - - if (selected) { - return ( -
-
-

- -

-
-
-
-

- Product id: {selected.id} -

-

- Description:{" "} - {selected.description} -

-
- -
-
-
-
- ); - } - - return ( - - errors={errors} - object={prodForm} - valueHandler={setProdName} - > - - name="name" - label={i18n.str`Product`} - tooltip={i18n.str`search products by it's description or id`} - addonAfter={ - - - - } - > -
- { - setProdName({ name: "" }); - onChange(p); - }} - /> -
- - - ); -} - -interface ProductListProps { - name?: string; - onSelect: (p: MerchantBackend.Products.ProductDetail & WithId) => void; - list: (MerchantBackend.Products.ProductDetail & WithId)[]; -} - -function ProductList({ name, onSelect, list }: ProductListProps) { - const { i18n } = useTranslationContext(); - if (!name) { - /* FIXME - this BR is added to occupy the space that will be added when the - dropdown appears - */ - return ( -
-
-
- ); - } - const filtered = list.filter( - (p) => p.id.includes(name) || p.description.includes(name), - ); - - return ( - - ); -} 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({ return (
-
-
+