diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/components/form')
-rw-r--r-- | packages/merchant-backoffice-ui/src/components/form/InputDate.tsx | 11 | ||||
-rw-r--r-- | packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx | 305 | ||||
-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.tsx | 4 |
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 }}> |