diff options
author | Sebastian <sebasjm@gmail.com> | 2021-06-10 23:51:14 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2021-06-10 23:55:04 -0300 |
commit | c9c04a14be4bf9a70cd1730d2e8b5aa8bd38f032 (patch) | |
tree | c59f0b0906f97ffe2c8394cfb963c778b82ad244 | |
parent | 352e23dbbc9a83b522964f3ba7fb06295d38c835 (diff) | |
download | merchant-backoffice-c9c04a14be4bf9a70cd1730d2e8b5aa8bd38f032.tar.gz merchant-backoffice-c9c04a14be4bf9a70cd1730d2e8b5aa8bd38f032.tar.bz2 merchant-backoffice-c9c04a14be4bf9a70cd1730d2e8b5aa8bd38f032.zip |
lots of fixes in same commit
added tooltips in buttons and fix tooltips styles in corner cases
added a start for required fields
token -> access token
fixed query product list
refactor instance update/create form
some more comments from christian email
44 files changed, 661 insertions, 455 deletions
diff --git a/packages/frontend/src/components/exception/AsyncButton.tsx b/packages/frontend/src/components/exception/AsyncButton.tsx index 95e4e72..3dad3e0 100644 --- a/packages/frontend/src/components/exception/AsyncButton.tsx +++ b/packages/frontend/src/components/exception/AsyncButton.tsx @@ -25,12 +25,12 @@ import { useAsync } from "../../hooks/async"; import { Translate } from "../../i18n"; type Props = { - children: ComponentChildren, + children: ComponentChildren, disabled: boolean; onClick?: () => Promise<void>; }; -export function AsyncButton({ onClick, disabled, children }: Props) { +export function AsyncButton({ onClick, disabled, children, ...rest }: Props) { const { isSlow, isLoading, request, cancel } = useAsync(onClick); if (isSlow) { @@ -40,7 +40,9 @@ export function AsyncButton({ onClick, disabled, children }: Props) { return <button class="button"><Translate>Loading...</Translate></button>; } - return <button class="button is-success" onClick={request} disabled={disabled}> - {children} - </button>; + return <span {...rest}> + <button class="button is-success" onClick={request} disabled={disabled}> + {children} + </button> + </span>; } diff --git a/packages/frontend/src/components/exception/login.tsx b/packages/frontend/src/components/exception/login.tsx index e7c9802..fca81ae 100644 --- a/packages/frontend/src/components/exception/login.tsx +++ b/packages/frontend/src/components/exception/login.tsx @@ -55,7 +55,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode { <p class="modal-card-title">{i18n`Login required`}</p> </header> <section class="modal-card-body" style={{ border: '1px solid', borderTop: 0, borderBottom: 0 }}> - {i18n`Please enter your auth token.`} + <Translate>Please enter your access token.</Translate> <div class="field is-horizontal"> <div class="field-label is-normal"> <label class="label">URL</label> @@ -74,12 +74,12 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode { </div> <div class="field is-horizontal"> <div class="field-label is-normal"> - <label class="label">Token</label> + <label class="label"><Translate>Access Token</Translate></label> </div> <div class="field-body"> <div class="field"> <p class="control is-expanded"> - <input class="input" type="text" placeholder={"set new token"} name="token" + <input class="input" type="text" placeholder={"set new access token"} name="token" onKeyPress={e => e.keyCode === 13 ? onConfirm(url, token ? `secret-token:${token}` : undefined) : null} value={token} onInput={(e): void => setToken(e?.currentTarget.value)} diff --git a/packages/frontend/src/components/form/DatePicker.tsx b/packages/frontend/src/components/form/DatePicker.tsx index 89d302b..084b7b0 100644 --- a/packages/frontend/src/components/form/DatePicker.tsx +++ b/packages/frontend/src/components/form/DatePicker.tsx @@ -261,11 +261,6 @@ export class DatePicker extends Component<Props, State> { </div>} - {/* {!selectYearMode && <div class="datePicker--actions"> - <button onClick={this.closeDatePicker}>CANCEL</button> - <button onClick={this.passDateToParent}>OK</button> - </div>} */} - </div> </div> diff --git a/packages/frontend/src/components/form/Input.tsx b/packages/frontend/src/components/form/Input.tsx index 200bf54..ab5554c 100644 --- a/packages/frontend/src/components/form/Input.tsx +++ b/packages/frontend/src/components/form/Input.tsx @@ -39,7 +39,7 @@ const TextInput = ({ inputType, error, ...rest }: any) => inputType === 'multili <input {...rest} class={error ? "input is-danger" : "input"} type={inputType} />; export function Input<T>({ name, readonly, placeholder, tooltip, label, expand, help, children, inputType, inputExtra, side, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode { - const { error, value, onChange } = useField<T>(name); + const { error, value, onChange, required } = useField<T>(name); return <div class="field is-horizontal"> <div class="field-label is-normal"> <label class="label"> @@ -51,7 +51,7 @@ export function Input<T>({ name, readonly, placeholder, tooltip, label, expand, </div> <div class="field-body is-flex-grow-3"> <div class="field"> - <p class={expand ? "control is-expanded" : "control"}> + <p class={expand ? "control is-expanded has-icons-right" : "control has-icons-right"}> <TextInput error={error} {...inputExtra} inputType={inputType} placeholder={placeholder} readonly={readonly} @@ -59,6 +59,9 @@ export function Input<T>({ name, readonly, placeholder, tooltip, label, expand, onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void => onChange(fromStr(e.currentTarget.value))} /> {help} {children} + { required && <span class="icon is-danger is-right"> + <i class="mdi mdi-star"></i> + </span> } </p> {error && <p class="help is-danger">{error}</p>} </div> diff --git a/packages/frontend/src/components/form/InputArray.tsx b/packages/frontend/src/components/form/InputArray.tsx index cbe5c50..5314eba 100644 --- a/packages/frontend/src/components/form/InputArray.tsx +++ b/packages/frontend/src/components/form/InputArray.tsx @@ -20,7 +20,7 @@ */ import { h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { useTranslator } from "../../i18n"; +import { Translate, useTranslator } from "../../i18n"; import { InputProps, useField } from "./useField"; export interface Props<T> extends InputProps<T> { @@ -63,24 +63,23 @@ export function InputArray<T>({ name, readonly, placeholder, tooltip, label, hel placeholder={placeholder} readonly={readonly} disabled={readonly} name={String(name)} value={currentValue} onChange={(e): void => setCurrentValue(e.currentTarget.value)} /> - {help} </p> <p class="control"> - <button class="button is-info" onClick={(): void => { + <button class="button is-info" disabled={!currentValue} onClick={(): void => { const v = fromStr(currentValue) if (!isValid(v)) { setLocalError(i18n`The value ${v} is invalid for a payment url`) return; } setLocalError(null) - onChange([v, ...array] as any); setCurrentValue(''); - }}>add</button> + }}><Translate>add</Translate></button> </p> </div> - {error && <p class="help is-danger"> {help} </p>} - {array.map((v,i) => <div key={i} class="tags has-addons mt-3 mb-0"> + {help} + {error && <p class="help is-danger"> {error} </p>} + {array.map((v, i) => <div key={i} class="tags has-addons mt-3 mb-0"> <span class="tag is-medium is-info mb-0" style={{ maxWidth: '90%' }}>{v}</span> <a class="tag is-medium is-danger is-delete mb-0" onClick={() => { onChange(array.filter(f => f !== v) as any); diff --git a/packages/frontend/src/components/form/InputDate.tsx b/packages/frontend/src/components/form/InputDate.tsx index 3986433..654d608 100644 --- a/packages/frontend/src/components/form/InputDate.tsx +++ b/packages/frontend/src/components/form/InputDate.tsx @@ -78,10 +78,12 @@ export function InputDate<T>({ name, readonly, label, placeholder, help, tooltip {error && <p class="help is-danger">{error}</p>} </div> - { !readonly && <button class="button is-info mr-3" onClick={() => onChange(undefined as any)} ><Translate>clear</Translate></button> } - {withTimestampSupport && + {!readonly && <span data-tooltip={withTimestampSupport ? i18n`change value to unknown date` : i18n`change value to empty`}> + <button class="button is-info mr-3" onClick={() => onChange(undefined as any)} ><Translate>clear</Translate></button> + </span>} + {withTimestampSupport && <span data-tooltip={i18n`change value to never`}> <button class="button is-info" onClick={() => onChange({ t_ms: 'never' } as any)}><Translate>never</Translate></button> - } + </span>} </div> <DatePicker opened={opened} diff --git a/packages/frontend/src/components/form/InputGroup.tsx b/packages/frontend/src/components/form/InputGroup.tsx index 58e4260..0720cfb 100644 --- a/packages/frontend/src/components/form/InputGroup.tsx +++ b/packages/frontend/src/components/form/InputGroup.tsx @@ -25,8 +25,8 @@ import { useGroupField } from "./useGroupField"; export interface Props<T> { name: T; children: ComponentChildren; - label: string; - tooltip?: string; + label: ComponentChildren; + tooltip?: ComponentChildren; alternative?: ComponentChildren; } @@ -51,14 +51,10 @@ export function InputGroup<T>({ name, label, children, tooltip, alternative }: P </button> </header> {active ? <div class="card-content"> - <div class="content"> {children} - </div> </div> : ( alternative ? <div class="card-content"> - <div class="content"> {alternative} - </div> </div> : undefined )} </div>; diff --git a/packages/frontend/src/components/form/InputSearchProduct.tsx b/packages/frontend/src/components/form/InputSearchProduct.tsx index 87ae722..43f79c9 100644 --- a/packages/frontend/src/components/form/InputSearchProduct.tsx +++ b/packages/frontend/src/components/form/InputSearchProduct.tsx @@ -114,18 +114,24 @@ function ProductList({ name, onSelect }: ProductListProps) { products = <div class="dropdown-content"> {!filtered.length ? <div class="dropdown-item" > - <Translate>no results</Translate> + <Translate>no products found with that description</Translate> </div> : filtered.map(p => ( <div key={p.id} class="dropdown-item" onClick={() => onSelect(p)} style={{ cursor: 'pointer' }}> - <table> - <tr> - <td style={{ width: 32 }}> - <div class="image" style={{ minWidth: 32 }}><img src={p.image} style={{ width: 32, height: 32 }} /></div> - </td> - <td><b>{p.id}</b>: {p.description}</td> - </tr> - </table> + <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 }} /></div> + </div> + <div class="media-content"> + <div class="content"> + <p> + <strong>{p.id}</strong> <small>{p.price}</small> + <br /> + {p.description} + </p> + </div> + </div> + </article> </div> )) } diff --git a/packages/frontend/src/components/form/InputSecured.stories.tsx b/packages/frontend/src/components/form/InputSecured.stories.tsx index 136d412..7314add 100644 --- a/packages/frontend/src/components/form/InputSecured.stories.tsx +++ b/packages/frontend/src/components/form/InputSecured.stories.tsx @@ -35,14 +35,14 @@ export const InitialValueEmpty = (): VNode => { const [state, setState] = useState<Partial<T>>({ auth_token: '' }) return <FormProvider<T> object={state} errors={{}} valueHandler={setState}> Initial value: '' - <InputSecured<T> name="auth_token" label="Token" /> + <InputSecured<T> name="auth_token" label="Access token" /> </FormProvider> } export const InitialValueToken = (): VNode => { const [state, setState] = useState<Partial<T>>({ auth_token: 'token' }) return <FormProvider<T> object={state} errors={{}} valueHandler={setState}> - <InputSecured<T> name="auth_token" label="Token" /> + <InputSecured<T> name="auth_token" label="Access token" /> </FormProvider> } @@ -50,6 +50,6 @@ export const InitialValueNull = (): VNode => { const [state, setState] = useState<Partial<T>>({ auth_token: null }) return <FormProvider<T> object={state} errors={{}} valueHandler={setState}> Initial value: '' - <InputSecured<T> name="auth_token" label="Token" /> + <InputSecured<T> name="auth_token" label="Access token" /> </FormProvider> } diff --git a/packages/frontend/src/components/form/InputSecured.tsx b/packages/frontend/src/components/form/InputSecured.tsx index 64737e3..7d8d655 100644 --- a/packages/frontend/src/components/form/InputSecured.tsx +++ b/packages/frontend/src/components/form/InputSecured.tsx @@ -57,7 +57,7 @@ export function InputSecured<T>({ name, readonly, placeholder, tooltip, label, h <div class="field has-addons"> <button class="button" onClick={(): void => { setActive(!active); }} > <div class="icon is-left"><i class="mdi mdi-lock-reset" /></div> - <span><Translate>Manage token</Translate></span> + <span><Translate>Manage access token</Translate></span> </button> <TokenStatus prev={initial} post={value} /> </div> diff --git a/packages/frontend/src/components/form/InputStock.tsx b/packages/frontend/src/components/form/InputStock.tsx index ce3add8..b323012 100644 --- a/packages/frontend/src/components/form/InputStock.tsx +++ b/packages/frontend/src/components/form/InputStock.tsx @@ -103,23 +103,25 @@ export function InputStock<T>({ name, readonly, placeholder, tooltip, label, hel const stockAddedErrors: FormErrors<typeof addedStock> = { lost: currentStock + addedStock.incoming < addedStock.lost ? - i18n`lost cannot be greater that current + incoming (max ${currentStock + addedStock.incoming})` + i18n`lost cannot be greater than current and incoming (max ${currentStock + addedStock.incoming})` : undefined } - const stockUpdateDescription = stockAddedErrors.lost ? '' : ( - !!addedStock.incoming || !!addedStock.lost ? - i18n`current stock will change from ${currentStock} to ${currentStock + addedStock.incoming - addedStock.lost}` : - i18n`current stock will stay at ${currentStock}` - ) + // const stockUpdateDescription = stockAddedErrors.lost ? '' : ( + // !!addedStock.incoming || !!addedStock.lost ? + // i18n`current stock will change from ${currentStock} to ${currentStock + addedStock.incoming - addedStock.lost}` : + // i18n`current stock will stay at ${currentStock}` + // ) return <Fragment> <div class="card"> <header class="card-header"> - {label} - {tooltip && <span class="icon" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} + <p class="card-header-title"> + {label} + {tooltip && <span class="icon" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span>} + </p> </header> <div class="card-content"> <FormProvider<Entity> name="stock" errors={errors} object={formValue} valueHandler={valueHandler}> @@ -130,14 +132,15 @@ export function InputStock<T>({ name, readonly, placeholder, tooltip, label, hel <InputNumber name="lost" label={i18n`Lost`} /> </FormProvider> - <div class="field is-horizontal"> + {/* <div class="field is-horizontal"> <div class="field-label is-normal" /> <div class="field-body is-flex-grow-3"> <div class="field"> {stockUpdateDescription} </div> </div> - </div> + </div> */} + </Fragment> : <InputNumber<Entity> name="current" label={i18n`Current`} side={ diff --git a/packages/frontend/src/components/form/InputWithAddon.tsx b/packages/frontend/src/components/form/InputWithAddon.tsx index 83b6c2e..22a4960 100644 --- a/packages/frontend/src/components/form/InputWithAddon.tsx +++ b/packages/frontend/src/components/form/InputWithAddon.tsx @@ -38,7 +38,7 @@ const defaultToString = (f?: any): string => f || '' const defaultFromString = (v: string): any => v as any export function InputWithAddon<T>({ name, readonly, addonBefore, children, expand, label, placeholder, help, tooltip, inputType, inputExtra, side, addonAfter, toStr = defaultToString, fromStr = defaultFromString }: Props<keyof T>): VNode { - const { error, value, onChange } = useField<T>(name); + const { error, value, onChange, required } = useField<T>(name); return <div class="field is-horizontal"> <div class="field-label is-normal"> @@ -55,11 +55,14 @@ export function InputWithAddon<T>({ name, readonly, addonBefore, children, expan {addonBefore && <div class="control"> <a class="button is-static">{addonBefore}</a> </div>} - <p class={expand ? "control is-expanded" : "control"}> + <p class={`control${expand ? " is-expanded" :""}${required ? " has-icons-right" : ''}`}> <input {...(inputExtra || {})} class={error ? "input is-danger" : "input"} type={inputType} placeholder={placeholder} readonly={readonly} name={String(name)} value={toStr(value)} onChange={(e): void => onChange(fromStr(e.currentTarget.value))} /> + {required && <span class="icon is-danger is-right"> + <i class="mdi mdi-star"></i> + </span>} {help} {children} </p> diff --git a/packages/frontend/src/components/form/TextField.tsx b/packages/frontend/src/components/form/TextField.tsx new file mode 100644 index 0000000..50ea26a --- /dev/null +++ b/packages/frontend/src/components/form/TextField.tsx @@ -0,0 +1,53 @@ +/* + This file is part of GNU Taler + (C) 2021 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 <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ +import { ComponentChildren, h, VNode } from "preact"; +import { useField, InputProps } from "./useField"; + +interface Props<T> extends InputProps<T> { + inputType?: 'text' | 'number' | 'multiline' | 'password'; + expand?: boolean; + side?: ComponentChildren; + children: ComponentChildren; +} + +export function TextField<T>({ name, tooltip, label, expand, help, children, side}: Props<keyof T>): VNode { + const { error } = useField<T>(name); + return <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && <span class="icon" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span>} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class={expand ? "control is-expanded has-icons-right" : "control has-icons-right"}> + {children} + {help} + </p> + {error && <p class="help is-danger">{error}</p>} + </div> + {side} + </div> + </div>; +} diff --git a/packages/frontend/src/components/form/useField.tsx b/packages/frontend/src/components/form/useField.tsx index 969291c..c552711 100644 --- a/packages/frontend/src/components/form/useField.tsx +++ b/packages/frontend/src/components/form/useField.tsx @@ -24,6 +24,7 @@ import { useFormContext } from "./FormProvider"; interface Use<V> { error?: string; + required: boolean; value: any; initial: any; onChange: (v: V) => void; @@ -47,9 +48,10 @@ export function useField<T>(name: keyof T): Use<T[typeof name]> { const value = readField(object, String(name)) const initial = readField(initialObject, String(name)) const isDirty = value !== initial - + const hasError = readField(errors, String(name)) return { - error: isDirty ? readField(errors, String(name)) : undefined, + error: isDirty ? hasError : undefined, + required: !isDirty && hasError, value, initial, onChange: updateField(name) as any, @@ -76,9 +78,9 @@ const setValueDeeper = (object: any, names: string[], value: any): any => { export interface InputProps<T> { name: T; - label: string; + label: ComponentChildren; placeholder?: string; - tooltip?: string; + tooltip?: ComponentChildren; readonly?: boolean; help?: ComponentChildren; }
\ No newline at end of file diff --git a/packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx b/packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx new file mode 100644 index 0000000..2b98383 --- /dev/null +++ b/packages/frontend/src/components/instance/DefaultInstanceFormFields.tsx @@ -0,0 +1,88 @@ +/* + This file is part of GNU Taler + (C) 2021 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 <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { Fragment, h } from "preact"; +import { Input } from "../form/Input"; +import { InputCurrency } from "../form/InputCurrency"; +import { InputDuration } from "../form/InputDuration"; +import { InputGroup } from "../form/InputGroup"; +import { InputLocation } from "../form/InputLocation"; +import { InputPayto } from "../form/InputPayto"; +import { InputWithAddon } from "../form/InputWithAddon"; +import { useBackendContext } from "../../context/backend"; +import { useTranslator } from "../../i18n"; +import { Entity } from "../../paths/admin/create/CreatePage"; + +export function DefaultInstanceFormFields({ readonlyId, showId }: { readonlyId?: boolean; showId: boolean }) { + const i18n = useTranslator(); + const backend = useBackendContext(); + return <Fragment> + { showId && <InputWithAddon<Entity> name="id" label={i18n`ID`} addonBefore={`${backend.url}/private/instances/`} readonly={readonlyId} tooltip={i18n`display name identification`} /> } + + <Input<Entity> name="name" label={i18n`Name`} tooltip={i18n`descriptive name`} /> + + <InputPayto<Entity> name="payto_uris" label={i18n`Account address`} help="x-taler-bank/bank.taler:5882/blogger" tooltip={i18n`where the money will be sent`} /> + + <InputCurrency<Entity> name="default_max_deposit_fee" label={i18n`Default max deposit fee`} tooltip={i18n`max deposit fee when an order has not overridden it`} /> + + <InputCurrency<Entity> name="default_max_wire_fee" label={i18n`Default max wire fee`} tooltip={i18n`max wire fee when the order has not overridden it`} /> + + <Input<Entity> name="default_wire_fee_amortization" label={i18n`Default wire fee amortization`} tooltip={i18n`max wire fee amortization when the order has not overridden it`} /> + + <InputGroup name="address" label={i18n`Address`} tooltip={i18n`physical location of merchant`}> + <InputLocation name="address" /> + </InputGroup> + + <InputGroup name="jurisdiction" label={i18n`Jurisdiction`} tooltip={i18n`legal location of merchant`}> + <InputLocation name="jurisdiction" /> + </InputGroup> + + <InputDuration<Entity> name="default_pay_delay" label={i18n`Default payment delay`} tooltip={i18n`max time to pay if the order does not override it`} /> + + <InputDuration<Entity> name="default_wire_transfer_delay" label={i18n`Default wire transfer delay`} tooltip={i18n`min time to wait the transfer if the merchant does not override it`} /> + + </Fragment>; +} +/* + <InputWithAddon<Entity> name="id" label={i18n`Identifier`} addonBefore={`${backend.url}/private/instances/`} readonly={!!forceId} tooltip={i18n`Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.`} /> + + <Input<Entity> name="name" label={i18n`Business name`} tooltip={i18n`Legal name of the business represented by this instance.`} /> + + <InputPayto<Entity> name="payto_uris" label={i18n`Bank account URI`} help="x-taler-bank/bank.taler:5882/blogger" tooltip={i18n`URI specifying bank account for crediting revenue.`} /> + + <InputCurrency<Entity> name="default_max_deposit_fee" label={i18n`Default max deposit fee`} tooltip={i18n`Maximum deposit fees this merchant is willing to pay per order by default.`} /> + + <InputCurrency<Entity> name="default_max_wire_fee" label={i18n`Default max wire fee`} tooltip={i18n`Maximum wire fees this merchant is willing to pay per wire transfer by default.`} /> + + <Input<Entity> name="default_wire_fee_amortization" label={i18n`Default wire fee amortization`} tooltip={i18n`Number of orders excess wire transfer fees will be divided by to compute per order surcharge.`} /> + + <InputGroup name="address" label={i18n`Address`} tooltip={i18n`Physical location of the merchant.`}> + <InputLocation name="address" /> + </InputGroup> + + <InputGroup name="jurisdiction" label={i18n`Jurisdiction`} tooltip={i18n`Jurisdiction for legal disputes with the merchant.`}> + <InputLocation name="jurisdiction" /> + </InputGroup> + + <InputDuration<Entity> name="default_pay_delay" label={i18n`Default payment delay`} tooltip={i18n`Time customers have to pay an order before the offer expires by default.`} /> + + <InputDuration<Entity> name="default_wire_transfer_delay" label={i18n`Default wire transfer delay`} tooltip={i18n`Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.`} /> +*/
\ No newline at end of file diff --git a/packages/frontend/src/components/menu/index.tsx b/packages/frontend/src/components/menu/index.tsx index 31826ed..3a7ccab 100644 --- a/packages/frontend/src/components/menu/index.tsx +++ b/packages/frontend/src/components/menu/index.tsx @@ -27,15 +27,12 @@ import { Sidebar } from "./SideBar"; function getInstanceTitle(path: string, id: string): string { switch (path) { - // case InstancePaths.details: return `${id}` case InstancePaths.update: return `${id}: Settings` case InstancePaths.order_list: return `${id}: Orders` case InstancePaths.order_new: return `${id}: New order` - case InstancePaths.order_details: return `${id}: Detail of the order` case InstancePaths.product_list: return `${id}: Products` case InstancePaths.product_new: return `${id}: New product` case InstancePaths.product_update: return `${id}: Update product` - case InstancePaths.reserves_details: return `${id}: Detail of a reserve` case InstancePaths.reserves_new: return `${id}: New reserve` case InstancePaths.reserves_list: return `${id}: Reserves` case InstancePaths.transfers_list: return `${id}: Transfers` diff --git a/packages/frontend/src/components/modal/index.tsx b/packages/frontend/src/components/modal/index.tsx index 8f6b722..9874a19 100644 --- a/packages/frontend/src/components/modal/index.tsx +++ b/packages/frontend/src/components/modal/index.tsx @@ -44,7 +44,7 @@ export function ConfirmModal({ active, description, onCancel, onConfirm, childre <div class="modal-background " onClick={onCancel} /> <div class="modal-card"> <header class="modal-card-head"> - {!description ? null : <p class="modal-card-title has-text-white-ter">{description}</p>} + {!description ? null : <p class="modal-card-title">{description}</p>} <button class="delete " aria-label="close" onClick={onCancel} /> </header> <section class="modal-card-body"> @@ -125,6 +125,7 @@ interface UpdateTokenModalProps { onClear: () => void; } +//FIXME: merge UpdateTokenModal with SetTokenNewInstanceModal export function UpdateTokenModal({ onCancel, onClear, onConfirm, oldToken }: UpdateTokenModalProps): VNode { type State = { old_token: string, new_token: string, repeat_token: string } const [form, setValue] = useState<Partial<State>>({ @@ -134,7 +135,7 @@ export function UpdateTokenModal({ onCancel, onClear, onConfirm, oldToken }: Upd const hasInputTheCorrectOldToken = oldToken && oldToken !== form.old_token const errors = { - old_token: hasInputTheCorrectOldToken ? i18n`is not the same as the current token` : undefined, + old_token: hasInputTheCorrectOldToken ? i18n`is not the same as the current access token` : undefined, new_token: !form.new_token ? i18n`cannot be empty` : (form.new_token === form.old_token ? i18n`cannot be the same as the old token` : undefined), repeat_token: form.new_token !== form.repeat_token ? i18n`is not the same` : undefined } @@ -143,7 +144,7 @@ export function UpdateTokenModal({ onCancel, onClear, onConfirm, oldToken }: Upd const instance = useInstanceContext() - const text = i18n`You are updating the authorization token from instance with id ${instance.id}` + const text = i18n`You are updating the access token from instance with id ${instance.id}` return <ClearConfirmModal description={text} onCancel={onCancel} @@ -154,11 +155,11 @@ export function UpdateTokenModal({ onCancel, onClear, onConfirm, oldToken }: Upd <div class="column" /> <div class="column is-four-fifths" > <FormProvider errors={errors} object={form} valueHandler={setValue}> - {oldToken && <Input<State> name="old_token" label={i18n`Old token`} tooltip={i18n`token currently in use`} inputType="password" />} - <Input<State> name="new_token" label={i18n`New token`} tooltip={i18n`next token to be used`} inputType="password" /> - <Input<State> name="repeat_token" label={i18n`Repeat token`} tooltip={i18n`confirm the same token`} inputType="password" /> + {oldToken && <Input<State> name="old_token" label={i18n`Old access token`} tooltip={i18n`access token currently in use`} inputType="password" />} + <Input<State> name="new_token" label={i18n`New access token`} tooltip={i18n`next access token to be used`} inputType="password" /> + <Input<State> name="repeat_token" label={i18n`Repeat access token`} tooltip={i18n`confirm the same access token`} inputType="password" /> </FormProvider> - <p><Translate>Clearing the auth token will mean public access to the instance</Translate></p> + <p><Translate>Clearing the access token will mean public access to the instance</Translate></p> </div> <div class="column" /> </div> @@ -173,13 +174,13 @@ export function SetTokenNewInstanceModal({ onCancel, onClear, onConfirm }: Updat const i18n = useTranslator() const errors = { - new_token: !form.new_token ? i18n`cannot be empty` : (form.new_token === form.old_token ? i18n`cannot be the same as the old token` : undefined), + new_token: !form.new_token ? i18n`cannot be empty` : (form.new_token === form.old_token ? i18n`cannot be the same as the old access token` : undefined), repeat_token: form.new_token !== form.repeat_token ? i18n`is not the same` : undefined } const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) - const text = i18n`You are setting the authorization token for the new instance` + const text = i18n`You are setting the access token for the new instance` return <ClearConfirmModal description={text} onCancel={onCancel} @@ -190,10 +191,10 @@ export function SetTokenNewInstanceModal({ onCancel, onClear, onConfirm }: Updat <div class="column" /> <div class="column is-four-fifths" > <FormProvider errors={errors} object={form} valueHandler={setValue}> - <Input<State> name="new_token" label={i18n`New token`} tooltip={i18n`next token to be used`} inputType="password" /> - <Input<State> name="repeat_token" label={i18n`Repeat token`} tooltip={i18n`confirm the same token`} inputType="password" /> + <Input<State> name="new_token" label={i18n`New access token`} tooltip={i18n`next access token to be used`} inputType="password" /> + <Input<State> name="repeat_token" label={i18n`Repeat access token`} tooltip={i18n`confirm the same access token`} inputType="password" /> </FormProvider> - <p><Translate>Clearing the auth token will mean public access to the instance</Translate></p> + <p><Translate>Clearing the access token will mean public access to the instance</Translate></p> </div> <div class="column" /> </div> diff --git a/packages/frontend/src/context/backend.ts b/packages/frontend/src/context/backend.ts index c1b2c14..9355859 100644 --- a/packages/frontend/src/context/backend.ts +++ b/packages/frontend/src/context/backend.ts @@ -49,7 +49,6 @@ export function useBackendContextState(): BackendContextType { const [url, triedToLog, changeBackend, resetBackend] = useBackendURL(); const [token, _updateToken] = useBackendDefaultToken(); const updateToken = (t?:string) => { - // console.log("update token", t) _updateToken(t) } diff --git a/packages/frontend/src/declaration.d.ts b/packages/frontend/src/declaration.d.ts index 21199f4..82bc694 100644 --- a/packages/frontend/src/declaration.d.ts +++ b/packages/frontend/src/declaration.d.ts @@ -685,6 +685,15 @@ export namespace MerchantBackend { // The order was neither claimed nor paid. order_status: "unpaid"; + // when was the order created + creation_time: Timestamp; + + // Order summary text. + summary: string; + + // Total amount of the order (to be paid by the customer). + total_amount: Amount; + // URI that the wallet must process to complete the payment. taler_pay_uri: string; diff --git a/packages/frontend/src/hooks/product.ts b/packages/frontend/src/hooks/product.ts index 6dd2d13..cdb1c4e 100644 --- a/packages/frontend/src/hooks/product.ts +++ b/packages/frontend/src/hooks/product.ts @@ -138,7 +138,7 @@ export function useInstanceProducts(): HttpResponse<(MerchantBackend.Products.Pr if (!list?.data || !list.data.products.length || listError || listLoading) return null return [`/private/products/${list.data.products[pageIndex].product_id}`, token, url] }, fetcher, { - + revalidateAll: true, }) useEffect(() => { diff --git a/packages/frontend/src/i18n/index.tsx b/packages/frontend/src/i18n/index.tsx index 5be1a3b..63c8e19 100644 --- a/packages/frontend/src/i18n/index.tsx +++ b/packages/frontend/src/i18n/index.tsx @@ -21,7 +21,7 @@ /** * Imports */ -import { Component, ComponentChild, ComponentChildren, h, Fragment, VNode } from "preact"; +import { ComponentChild, ComponentChildren, h, Fragment, VNode } from "preact"; import { useTranslationContext } from "../context/translation"; @@ -40,11 +40,10 @@ export function useTranslator() { } - /** * Convert template strings to a msgid */ -function toI18nString(stringSeq: ReadonlyArray<string>): string { + function toI18nString(stringSeq: ReadonlyArray<string>): string { let s = ""; for (let i = 0; i < stringSeq.length; i++) { s += stringSeq[i]; diff --git a/packages/frontend/src/paths/admin/create/CreatePage.tsx b/packages/frontend/src/paths/admin/create/CreatePage.tsx index 488d137..82286de 100644 --- a/packages/frontend/src/paths/admin/create/CreatePage.tsx +++ b/packages/frontend/src/paths/admin/create/CreatePage.tsx @@ -24,22 +24,13 @@ import { useState } from "preact/hooks"; import * as yup from 'yup'; import { AsyncButton } from "../../../components/exception/AsyncButton"; import { FormErrors, FormProvider } from "../../../components/form/FormProvider"; -import { Input } from "../../../components/form/Input"; -import { InputCurrency } from "../../../components/form/InputCurrency"; -import { InputDuration } from "../../../components/form/InputDuration"; -import { InputGroup } from "../../../components/form/InputGroup"; -import { InputLocation } from "../../../components/form/InputLocation"; -import { InputPayto } from "../../../components/form/InputPayto"; -import { InputSecured } from "../../../components/form/InputSecured"; -import { InputWithAddon } from "../../../components/form/InputWithAddon"; -import { SetTokenNewInstanceModal, UpdateTokenModal } from "../../../components/modal"; -import { useBackendContext } from "../../../context/backend"; -import { useInstanceContext } from "../../../context/instance"; +import { SetTokenNewInstanceModal } from "../../../components/modal"; import { MerchantBackend } from "../../../declaration"; import { Translate, useTranslator } from "../../../i18n"; import { InstanceCreateSchema as schema } from '../../../schemas'; +import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields"; -type Entity = MerchantBackend.Instances.InstanceConfigurationMessage & { auth_token?: string } +export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage & { auth_token?: string } interface Props { onCreate: (d: Entity) => Promise<void>; @@ -50,9 +41,9 @@ interface Props { function with_defaults(id?: string): Partial<Entity> { return { id, - default_pay_delay: { d_ms: 1000 * 60 * 5 }, + default_pay_delay: { d_ms: 1000 * 60 * 60 }, // one hour default_wire_fee_amortization: 1, - default_wire_transfer_delay: { d_ms: 2000 * 60 * 5 }, + default_wire_transfer_delay: { d_ms: 1000 * 2 * 60 * 60 * 24 }, // one day }; } export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { @@ -78,13 +69,13 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { // schema.validateSync(value, { abortEarly: false }) return onCreate(schema.cast(value) as Entity); } - const backend = useBackendContext() function updateToken(token: string | null) { valueHandler(old => ({ ...old, auth_token: token === null ? undefined : token })) } const i18n = useTranslator() + return <div> <div class="columns"> <div class="column" /> @@ -108,44 +99,37 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { <div class="column" /> </div> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-item has-text-centered"> + <h1 class="title"> + <button class="button is-danger" onClick={() => updateIsTokenDialogActive(true)} > + <div class="icon is-centered"><i class="mdi mdi-lock-reset" /></div> + <span><Translate>Set access token</Translate></span> + </button> + </h1> + </div> + </div> + </div></section> + + <section class="section is-main-section"> <div class="columns"> <div class="column" /> - <div class="column is-two-thirds"> - <FormProvider<Entity> errors={errors} object={value} valueHandler={valueHandler} > - - <InputWithAddon<Entity> name="id" label={i18n`Identifier`} addonBefore={`${backend.url}/private/instances/`} readonly={!!forceId} tooltip={i18n`Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.`} /> - - <Input<Entity> name="name" label={i18n`Business name`} tooltip={i18n`Legal name of the business represented by this instance.`} /> + <div class="column is-four-fifths"> - <InputPayto<Entity> name="payto_uris" label={i18n`Bank account URI`} help="x-taler-bank/bank.taler:5882/blogger" tooltip={i18n`URI specifying bank account for crediting revenue.`} /> - - <InputCurrency<Entity> name="default_max_deposit_fee" label={i18n`Default max deposit fee`} tooltip={i18n`Maximum deposit fees this merchant is willing to pay per order by default.`} /> - - <InputCurrency<Entity> name="default_max_wire_fee" label={i18n`Default max wire fee`} tooltip={i18n`Maximum wire fees this merchant is willing to pay per wire transfer by default.`} /> - - <Input<Entity> name="default_wire_fee_amortization" label={i18n`Default wire fee amortization`} tooltip={i18n`Number of orders excess wire transfer fees will be divided by to compute per order surcharge.`} /> - - <InputGroup name="address" label={i18n`Address`} tooltip={i18n`Physical location of the merchant.`}> - <InputLocation name="address" /> - </InputGroup> - - <InputGroup name="jurisdiction" label={i18n`Jurisdiction`} tooltip={i18n`Jurisdiction for legal disputes with the merchant.`}> - <InputLocation name="jurisdiction" /> - </InputGroup> - - <InputDuration<Entity> name="default_pay_delay" label={i18n`Default payment delay`} tooltip={i18n`Time customers have to pay an order before the offer expires by default.`} /> + <FormProvider<Entity> errors={errors} object={value} valueHandler={valueHandler} > - <InputDuration<Entity> name="default_wire_transfer_delay" label={i18n`Default wire transfer delay`} tooltip={i18n`Maximum time an exchange is allowed to delay wiring funds to the merchant, enabling it to aggregate smaller payments into larger wire transfers and reducing wire fees.`} /> + <DefaultInstanceFormFields readonlyId={!!forceId} showId={true} /> </FormProvider> <div class="buttons is-right mt-5"> - {onBack && <button class="button" onClick={onBack} ><Translate>Cancel</Translate></button>} - {isTokenSet ? - <AsyncButton onClick={submit} disabled={hasErrors} ><Translate>Confirm</Translate></AsyncButton> : - <button class="button" onClick={() => updateIsTokenDialogActive(true)} ><Translate>Set token</Translate></button> - } + {onBack && <button class="button" onClick={onBack}><Translate>Cancel</Translate></button>} + <AsyncButton onClick={submit} disabled={!isTokenSet && hasErrors} data-tooltip={ + hasErrors ? i18n`Need to complete fields first, check fields marked with a star` : 'confirm operation' + }><Translate>Confirm</Translate></AsyncButton> </div> </div> diff --git a/packages/frontend/src/paths/admin/create/InstanceCreatedSuccessfully.tsx b/packages/frontend/src/paths/admin/create/InstanceCreatedSuccessfully.tsx index 45aec1c..00b3f20 100644 --- a/packages/frontend/src/paths/admin/create/InstanceCreatedSuccessfully.tsx +++ b/packages/frontend/src/paths/admin/create/InstanceCreatedSuccessfully.tsx @@ -49,7 +49,7 @@ export function InstanceCreatedSuccessfully({ entity, onConfirm }: { entity: Ent </div> <div class="field is-horizontal"> <div class="field-label is-normal"> - <label class="label">Token</label> + <label class="label">Access token</label> </div> <div class="field-body is-flex-grow-3"> <div class="field"> diff --git a/packages/frontend/src/paths/admin/list/Table.tsx b/packages/frontend/src/paths/admin/list/Table.tsx index 1f512c0..4210a92 100644 --- a/packages/frontend/src/paths/admin/list/Table.tsx +++ b/packages/frontend/src/paths/admin/list/Table.tsx @@ -22,7 +22,7 @@ import { h, VNode } from "preact"; import { StateUpdater, useEffect, useState } from "preact/hooks"; import { MerchantBackend } from "../../../declaration"; -import { Translate } from "../../../i18n"; +import { Translate, useTranslator } from "../../../i18n"; interface Props { instances: MerchantBackend.Instances.Instance[]; @@ -50,6 +50,7 @@ export function CardTable({ instances, onCreate, onUpdate, onDelete, selected }: } }, [actionQueue, selected, onUpdate]) + const i18n = useTranslator() return <div class="card has-table"> <header class="card-header"> @@ -63,9 +64,11 @@ export function CardTable({ instances, onCreate, onUpdate, onDelete, selected }: </button> </div> <div class="card-header-icon" aria-label="more options"> - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> - </button> + <span class="has-tooltip-left" data-tooltip={i18n`add new instance`}> + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> + </button> + </span> </div> </header> @@ -96,48 +99,48 @@ function toggleSelected<T>(id: T): (prev: T[]) => T[] { function Table({ rowSelection, rowSelectionHandler, instances, onUpdate, onDelete }: TableProps): VNode { return ( <div class="table-container"> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th class="is-checkbox-cell"> - <label class="b-checkbox checkbox"> - <input type="checkbox" checked={rowSelection.length === instances.length} onClick={(): void => rowSelectionHandler(rowSelection.length === instances.length ? [] : instances.map(i => i.id))} /> - <span class="check" /> - </label> - </th> - <th><Translate>ID</Translate></th> - <th><Translate>Name</Translate></th> - <th /> - </tr> - </thead> - <tbody> - {instances.map(i => { - return <tr key={i.id}> - <td class="is-checkbox-cell"> + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th class="is-checkbox-cell"> <label class="b-checkbox checkbox"> - <input type="checkbox" checked={rowSelection.indexOf(i.id) != -1} onClick={(): void => rowSelectionHandler(toggleSelected(i.id))} /> + <input type="checkbox" checked={rowSelection.length === instances.length} onClick={(): void => rowSelectionHandler(rowSelection.length === instances.length ? [] : instances.map(i => i.id))} /> <span class="check" /> </label> - </td> - <td><a href={`/orders?instance=${i.id}`} >{i.id}</a></td> - <td >{i.name}</td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button class="button is-small is-success jb-modal" type="button" onClick={(): void => onUpdate(i.id)}> - <Translate>Edit</Translate> - </button> - <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onDelete(i)}> - <Translate>Delete</Translate> - </button> - </div> - </td> + </th> + <th><Translate>ID</Translate></th> + <th><Translate>Name</Translate></th> + <th /> </tr> - })} - - </tbody> - </table> + </thead> + <tbody> + {instances.map(i => { + return <tr key={i.id}> + <td class="is-checkbox-cell"> + <label class="b-checkbox checkbox"> + <input type="checkbox" checked={rowSelection.indexOf(i.id) != -1} onClick={(): void => rowSelectionHandler(toggleSelected(i.id))} /> + <span class="check" /> + </label> + </td> + <td><a href={`/orders?instance=${i.id}`} >{i.id}</a></td> + <td >{i.name}</td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <button class="button is-small is-success jb-modal" type="button" onClick={(): void => onUpdate(i.id)}> + <Translate>Edit</Translate> + </button> + <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onDelete(i)}> + <Translate>Delete</Translate> + </button> + </div> + </td> + </tr> + })} + + </tbody> + </table> </div> - ) + ) } function EmptyTable(): VNode { diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx index 1b47564..ae32dac 100644 --- a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx +++ b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx @@ -257,7 +257,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { with a total price of {totalPriceProducts} </p> } tooltip={i18n`Add products without inventory management to the order.`}> - <NonInventoryProductFrom value={editingProduct} onAddProduct={(p) => { + <NonInventoryProductFrom productToEdit={editingProduct} onAddProduct={(p) => { setEditingProduct(undefined) return addNewProduct(p) }} /> diff --git a/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx b/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx index 6cf48f8..b97b39a 100644 --- a/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx +++ b/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx @@ -39,12 +39,14 @@ export function InventoryProductForm({ currentProducts, onAddProduct }: Props): const i18n = useTranslator() + const productWithInfiniteStock = state.product && state.product.total_stock === -1 + const submit = (): void => { if (!state.product) { setErrors({ product: i18n`You must enter a valid product identifier.` }); return; } - if (state.product.total_stock === -1) { + if (productWithInfiniteStock) { onAddProduct(state.product, 1) } else { if (!state.quantity || state.quantity <= 0) { @@ -73,9 +75,9 @@ export function InventoryProductForm({ currentProducts, onAddProduct }: Props): return <FormProvider<Form> errors={errors} object={state} valueHandler={setState}> <InputSearchProduct selected={state.product} onChange={(p) => setState(v => ({ ...v, product: p }))} /> - <InputNumber<Form> name="quantity" label={i18n`Quantity`} tooltip={i18n`how many products will be added`} /> + { state.product && !productWithInfiniteStock && <InputNumber<Form> name="quantity" label={i18n`Quantity`} tooltip={i18n`how many products will be added`} /> } <div class="buttons is-right mt-5"> - <button class="button is-success" onClick={submit}><Translate>Add</Translate></button> + <button class="button is-success" disabled={!state.product} onClick={submit}><Translate>Add</Translate></button> </div> </FormProvider> } diff --git a/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx b/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx index 967f1cb..756ec23 100644 --- a/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx +++ b/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx @@ -28,22 +28,22 @@ import { NonInventoryProductSchema as schema } from '../../../../schemas'; import * as yup from 'yup'; -import { useTranslator } from "../../../../i18n"; +import { Translate, useTranslator } from "../../../../i18n"; type Entity = MerchantBackend.Product interface Props { onAddProduct: (p: Entity) => Promise<void>; - value?: Entity; + productToEdit?: Entity; } -export function NonInventoryProductFrom({ value, onAddProduct }: Props): VNode { +export function NonInventoryProductFrom({ productToEdit, onAddProduct }: Props): VNode { const [showCreateProduct, setShowCreateProduct] = useState(false) - const editing = !!value + const isEditing = !!productToEdit useEffect(() => { - setShowCreateProduct(editing) - }, [editing]) + setShowCreateProduct(isEditing) + }, [isEditing]) const [submitForm, addFormSubmitter] = useListener<Partial<MerchantBackend.Product> | undefined>((result) => { if (result) { @@ -62,10 +62,12 @@ export function NonInventoryProductFrom({ value, onAddProduct }: Props): VNode { return <Fragment> <div class="buttons"> - <button class="button is-success" onClick={() => setShowCreateProduct(true)} >add new product</button> + <button class="button is-success" onClick={() => setShowCreateProduct(true)} ><Translate>add new product</Translate></button> </div> - {showCreateProduct && <ConfirmModal active onCancel={() => setShowCreateProduct(false)} onConfirm={submitForm}> - <ProductForm initial={value} onSubscribe={addFormSubmitter} /> + {showCreateProduct && <ConfirmModal active + description="alskdj alsk jdalksjd laksjd lkasjd" + onCancel={() => setShowCreateProduct(false)} onConfirm={submitForm}> + <ProductForm initial={productToEdit} onSubscribe={addFormSubmitter} /> </ConfirmModal>} </Fragment> } diff --git a/packages/frontend/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx b/packages/frontend/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx index db0be90..14c5d68 100644 --- a/packages/frontend/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx +++ b/packages/frontend/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx @@ -17,6 +17,7 @@ import { h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; import { CreatedSuccessfully } from "../../../../components/notifications/CreatedSuccessfully"; import { useOrderAPI } from "../../../../hooks/order"; +import { Translate } from "../../../../i18n"; import { Entity } from "./index"; interface Props { @@ -38,7 +39,7 @@ export function OrderCreatedSuccessfully({ entity, onConfirm, onCreateAnother }: return <CreatedSuccessfully onConfirm={onConfirm} onCreateAnother={onCreateAnother}> <div class="field is-horizontal"> <div class="field-label is-normal"> - <label class="label">Amount</label> + <label class="label"><Translate>Amount</Translate></label> </div> <div class="field-body is-flex-grow-3"> <div class="field"> @@ -50,7 +51,7 @@ export function OrderCreatedSuccessfully({ entity, onConfirm, onCreateAnother }: </div> <div class="field is-horizontal"> <div class="field-label is-normal"> - <label class="label">Summary</label> + <label class="label"><Translate>Summary</Translate></label> </div> <div class="field-body is-flex-grow-3"> <div class="field"> @@ -62,7 +63,7 @@ export function OrderCreatedSuccessfully({ entity, onConfirm, onCreateAnother }: </div> <div class="field is-horizontal"> <div class="field-label is-normal"> - <label class="label">Order ID</label> + <label class="label"><Translate>Order ID</Translate></label> </div> <div class="field-body is-flex-grow-3"> <div class="field"> @@ -74,7 +75,7 @@ export function OrderCreatedSuccessfully({ entity, onConfirm, onCreateAnother }: </div> <div class="field is-horizontal"> <div class="field-label is-normal"> - <label class="label">Payment URL</label> + <label class="label"><Translate>Payment URL</Translate></label> </div> <div class="field-body is-flex-grow-3"> <div class="field"> diff --git a/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx index be05b43..9f148b6 100644 --- a/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx +++ b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { AmountJson, Amounts } from "@gnu-taler/taler-util"; import { format } from "date-fns"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -29,7 +30,7 @@ import { InputDate } from "../../../../components/form/InputDate"; import { InputDuration } from "../../../../components/form/InputDuration"; import { InputGroup } from "../../../../components/form/InputGroup"; import { InputLocation } from "../../../../components/form/InputLocation"; -import { NotificationCard } from "../../../../components/menu"; +import { TextField } from "../../../../components/form/TextField"; import { ProductList } from "../../../../components/product/ProductList"; import { MerchantBackend } from "../../../../declaration"; import { Translate, useTranslator } from "../../../../i18n"; @@ -52,6 +53,35 @@ type Unpaid = MerchantBackend.Orders.CheckPaymentUnpaidResponse type Claimed = MerchantBackend.Orders.CheckPaymentClaimedResponse +function ContractTerms({ value }: { value: CT }) { + const i18n = useTranslator() + + return <InputGroup name="contract_terms" label={i18n`Contract Terms`}> + <FormProvider<CT> object={value} valueHandler={null} > + <Input<CT> readonly name="summary" label={i18n`Summary`} tooltip={i18n`human-readable description of the whole purchase`} /> + <InputCurrency<CT> readonly name="amount" label={i18n`Amount`} tooltip={i18n`total price for the transaction`} /> + {value.fulfillment_url && + <Input<CT> readonly name="fulfillment_url" label={i18n`Fulfillment URL`} tooltip={i18n`URL for this purchase`} /> + } + <Input<CT> readonly name="max_fee" label={i18n`Max fee`} tooltip={i18n`maximum total deposit fee accepted by the merchant for this contract`} /> + <Input<CT> readonly name="max_wire_fee" label={i18n`Max wire fee`} tooltip={i18n`maximum wire fee accepted by the merchant`} /> + <Input<CT> readonly name="wire_fee_amortization" label={i18n`Wire fee amortization`} tooltip={i18n`over how many customer transactions does the merchant expect to amortize wire fees on average`} /> + <InputDate<CT> readonly name="timestamp" label={i18n`Created at`} tooltip={i18n`time when this contract was generated`} /> + <InputDate<CT> readonly name="refund_deadline" label={i18n`Refund deadline`} tooltip={i18n`after this deadline has passed no refunds will be accepted`} /> + <InputDate<CT> readonly name="pay_deadline" label={i18n`Payment deadline`} tooltip={i18n`after this deadline, the merchant won't accept payments for the contract`} /> + <InputDate<CT> readonly name="wire_transfer_deadline" label={i18n`Wire transfer deadline`} tooltip={i18n`transfer deadline for the exchange`} /> + <InputDate<CT> readonly name="delivery_date" label={i18n`Delivery date`} tooltip={i18n`time indicating when the order should be delivered`} /> + {value.delivery_date && + <InputGroup name="delivery_location" label={i18n`Location`} tooltip={i18n`where the order will be delivered`} > + <InputLocation name="payments.delivery_location" /> + </InputGroup> + } + <InputDuration<CT> readonly name="auto_refund" label={i18n`Auto-refund delay`} tooltip={i18n`how long the wallet should try to get an automatic refund for the purchase`} /> + <Input<CT> readonly name="extra" label={i18n`Extra info`} tooltip={i18n`extra data that is only interpreted by the merchant frontend`} /> + </FormProvider> + </InputGroup> +} + function ClaimedPage({ id, order }: { id: string; order: MerchantBackend.Orders.CheckPaymentClaimedResponse }) { const events: Event[] = [] events.push({ @@ -126,11 +156,8 @@ function ClaimedPage({ id, order }: { id: string; order: MerchantBackend.Orders. whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', - // maxWidth: '100%', }}> - {/* <a href={order.order_status_url} rel="nofollow" target="new">{order.order_status_url}</a> */} - <p><Translate>pay at</Translate>: <b>missing value, there is no order_status_url</b></p> - <p><Translate>created at</Translate>: {format(new Date(order.contract_terms.timestamp.t_ms), 'yyyy-MM-dd HH:mm:ss')}</p> + <p><b><Translate>claimed at</Translate>:</b> {format(new Date(order.contract_terms.timestamp.t_ms), 'yyyy-MM-dd HH:mm:ss')}</p> </div> </div> </div> @@ -155,18 +182,12 @@ function ClaimedPage({ id, order }: { id: string; order: MerchantBackend.Orders. </div> </section> - {order.contract_terms.products.length > 0 && - <section class="section"> - <div class="columns"> - <div class="column is-12" > - <div class="title"><Translate>Product list</Translate></div> - <ProductList list={order.contract_terms.products} /> - </div> - <div class="column" /> - </div> - </section> - } + {order.contract_terms.products.length ? <Fragment> + <div class="title"><Translate>Product list</Translate></div> + <ProductList list={order.contract_terms.products} /> + </Fragment> : undefined} + {value.contract_terms && <ContractTerms value={value.contract_terms} />} </div> <div class="column" /> </div> @@ -207,19 +228,43 @@ function PaidPage({ id, order, onRefund }: { id: string; order: MerchantBackend. type: 'refund', }) }) - order.wire_details.forEach(e => { - events.push({ - when: new Date(e.execution_time.t_ms), - description: `wired`, - type: 'wired', - }) - }) - if (order.contract_terms.wire_transfer_deadline.t_ms !== 'never' && - order.contract_terms.wire_transfer_deadline.t_ms < new Date().getTime()) events.push({ - when: new Date(order.contract_terms.wire_transfer_deadline.t_ms - 1000 * 10), - description: `wired (faked)`, - type: 'wired', - }) + if (order.wire_details && order.wire_details.length) { + if (order.wire_details.length > 1) { + let last: MerchantBackend.Orders.TransactionWireTransfer | null = null + let first: MerchantBackend.Orders.TransactionWireTransfer | null = null + let total: AmountJson | null = null + + order.wire_details.forEach(w => { + if (last === null || last.execution_time.t_ms < w.execution_time.t_ms) { + last = w + } + if (first === null || first.execution_time.t_ms > w.execution_time.t_ms) { + first = w + } + total = total === null ? Amounts.parseOrThrow(w.amount) : Amounts.add(total, Amounts.parseOrThrow(w.amount)).amount + }) + events.push({ + when: new Date(last!.execution_time.t_ms), + description: `wired ${Amounts.stringify(total!)}`, + type: 'wired-range', + }) + events.push({ + when: new Date(first!.execution_time.t_ms), + description: `wire transfer started...`, + type: 'wired-range', + }) + } else { + order.wire_details.forEach(e => { + events.push({ + when: new Date(e.execution_time.t_ms), + description: `wired ${e.amount}`, + type: 'wired', + }) + }) + + } + + } const [value, valueHandler] = useState<Partial<Paid>>(order) @@ -261,10 +306,9 @@ function PaidPage({ id, order, onRefund }: { id: string; order: MerchantBackend. <div class="level-item"> <h1 class="title"> <div class="buttons"> - {refundable && <button class="button is-danger" onClick={() => onRefund(id)}><Translate>refund</Translate></button>} - <button class="button is-info" onClick={() => { - if (order.contract_terms.fulfillment_url) copyToClipboard(order.contract_terms.fulfillment_url) - }}><Translate>copy url</Translate></button> + <span class="has-tooltip-left" data-tooltip={refundable ? i18n`refund order`: i18n`not refundable`}> + <button class="button is-danger" disabled={!refundable} onClick={() => onRefund(id)}><Translate>refund</Translate></button> + </span> </div> </h1> </div> @@ -301,53 +345,23 @@ function PaidPage({ id, order, onRefund }: { id: string; order: MerchantBackend. <InputCurrency<Paid> name="deposit_total" readonly label={i18n`Deposit total`} /> {order.refunded && <InputCurrency<Paid> name="refund_amount" readonly label={i18n`Refunded amount`} />} <Input<Paid> name="order_status" readonly label={i18n`Order status`} /> - {order.order_status_url && <Input<Paid> name="order_status_url" readonly label={i18n`Status URL`} />} + <TextField<Paid> name="order_status_url" label={i18n`Status URL`} > + <a target="_blank" href={order.order_status_url}> + {order.order_status_url} + </a> + </TextField> </FormProvider> </div> </div> </section> - {value.contract_terms && <section class="section"> - <div class="columns"> - <div class="column is-12" > - <div class="title"><Translate>Contract Terms</Translate></div> - <FormProvider<CT> object={value.contract_terms} valueHandler={null} > - <Input<CT> readonly name="summary" label={i18n`Summary`} tooltip={i18n`human-readable description of the whole purchase`} /> - <InputCurrency<CT> readonly name="amount" label={i18n`Amount`} tooltip={i18n`total price for the transaction`} /> - {value.contract_terms.fulfillment_url && - <Input<CT> readonly name="fulfillment_url" label={i18n`Fulfillment URL`} tooltip={i18n`URL for this purchase`} /> - } - <Input<CT> readonly name="max_fee" label={i18n`Max fee`} tooltip={i18n`maximum total deposit fee accepted by the merchant for this contract`} /> - <Input<CT> readonly name="max_wire_fee" label={i18n`Max wire fee`} tooltip={i18n`maximum wire fee accepted by the merchant`} /> - <Input<CT> readonly name="wire_fee_amortization" label={i18n`Wire fee amortization`} tooltip={i18n`over how many customer transactions does the merchant expect to amortize wire fees on average`} /> - <InputDate<CT> readonly name="timestamp" label={i18n`Created at`} tooltip={i18n`time when this contract was generated`} /> - <InputDate<CT> readonly name="refund_deadline" label={i18n`Refund deadline`} tooltip={i18n`after this deadline has passed no refunds will be accepted`} /> - <InputDate<CT> readonly name="pay_deadline" label={i18n`Payment deadline`} tooltip={i18n`after this deadline, the merchant won't accept payments for the contract`} /> - <InputDate<CT> readonly name="wire_transfer_deadline" label={i18n`Wire transfer deadline`} tooltip={i18n`transfer deadline for the exchange`} /> - <InputDate<CT> readonly name="delivery_date" label={i18n`Delivery date`} tooltip={i18n`time indicating when the order should be delivered`} /> - {value.contract_terms.delivery_date && - <InputGroup name="delivery_location" label={i18n`Location`} tooltip={i18n`where the order will be delivered`} > - <InputLocation name="payments.delivery_location" /> - </InputGroup> - } - <InputDuration<CT> readonly name="auto_refund" label={i18n`Auto-refund delay`} tooltip={i18n`how long the wallet should try to get an automatic refund for the purchase`} /> - <Input<CT> readonly name="extra" label={i18n`Extra info`} tooltip={i18n`extra data that is only interpreted by the merchant frontend`} /> - </FormProvider> - </div> - <div class="column" /> - </div> - </section>} - {order.contract_terms.products.length ? <section class="section"> - <div class="columns"> - <div class="column is-12" > - <div class="title"><Translate>Product list</Translate></div> - <ProductList list={order.contract_terms.products} /> - </div> - <div class="column" /> - </div> - </section> : undefined} + {order.contract_terms.products.length ? <Fragment> + <div class="title"><Translate>Product list</Translate></div> + <ProductList list={order.contract_terms.products} /> + </Fragment> : undefined} + {value.contract_terms && <ContractTerms value={value.contract_terms} />} </div> <div class="column" /> </div> @@ -380,10 +394,9 @@ function UnpaidPage({ id, order }: { id: string; order: MerchantBackend.Orders.C whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', - // maxWidth: '100%', }}> - <p><Translate>pay at</Translate>: <a href={order.order_status_url} rel="nofollow" target="new">{order.order_status_url}</a></p> - <p><Translate>created at</Translate>: <b>missing value, there is no contract term yet</b></p> + <p><b><Translate>pay at</Translate>:</b> <a href={order.order_status_url} rel="nofollow" target="new">{order.order_status_url}</a></p> + <p><b><Translate>created at</Translate>:</b> {format(new Date(order.creation_time.t_ms), 'yyyy-MM-dd HH:mm:ss')}</p> </div> </div> </div> @@ -394,8 +407,10 @@ function UnpaidPage({ id, order }: { id: string; order: MerchantBackend.Orders.C <section class="section is-main-section"> <div class="columns"> <div class="column" /> - <div class="column is-6"> + <div class="column is-four-fifths"> <FormProvider<Unpaid> object={value} valueHandler={valueHandler} > + <Input<Unpaid> readonly name="summary" label={i18n`Summary`} tooltip={i18n`human-readable description of the whole purchase`} /> + <InputCurrency<Unpaid> readonly name="total_amount" label={i18n`Amount`} tooltip={i18n`total price for the transaction`} /> <Input<Unpaid> name="order_status" readonly label={i18n`Order status`} /> <Input<Unpaid> name="order_status_url" readonly label={i18n`Order status URL`} /> <Input<Unpaid> name="taler_pay_uri" readonly label={i18n`Payment URI`} /> @@ -432,7 +447,7 @@ export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode { />} <div class="columns"> <div class="column" /> - <div class="column is-two-thirds"> + <div class="column is-four-fifths"> <div class="buttons is-right mt-5"> <button class="button" onClick={onBack}><Translate>Back</Translate></button> </div> diff --git a/packages/frontend/src/paths/instance/orders/details/Timeline.tsx b/packages/frontend/src/paths/instance/orders/details/Timeline.tsx index d4f17c4..16adbcb 100644 --- a/packages/frontend/src/paths/instance/orders/details/Timeline.tsx +++ b/packages/frontend/src/paths/instance/orders/details/Timeline.tsx @@ -47,7 +47,7 @@ export function Timeline({ events:e }: Props) { } }) return <div class="timeline"> - {events.map((e,i) => { + {state.map((e,i) => { return <div key={i} class="timeline-item"> {(() => { switch (e.type) { @@ -55,6 +55,7 @@ export function Timeline({ events:e }: Props) { case "delivery": return <div class="timeline-marker is-icon "><i class="mdi mdi-delivery" /></div> case "start": return <div class="timeline-marker is-icon is-success"><i class="mdi mdi-flag " /></div> case "wired": return <div class="timeline-marker is-icon is-success"><i class="mdi mdi-cash" /></div> + case "wired-range": return <div class="timeline-marker is-icon is-success"><i class="mdi mdi-cash" /></div> case "refund": return <div class="timeline-marker is-icon is-danger"><i class="mdi mdi-cash" /></div> case "now": return <div class="timeline-marker is-icon is-info"><i class="mdi mdi-clock" /></div> } @@ -71,5 +72,5 @@ export function Timeline({ events:e }: Props) { export interface Event { when: Date; description: string; - type: 'start' | 'refund' | 'wired' | 'deadline' | 'delivery' | 'now' + type: 'start' | 'refund' | 'wired' | 'wired-range' |'deadline' | 'delivery' | 'now' } diff --git a/packages/frontend/src/paths/instance/orders/list/Table.tsx b/packages/frontend/src/paths/instance/orders/list/Table.tsx index 4057ca2..41c7293 100644 --- a/packages/frontend/src/paths/instance/orders/list/Table.tsx +++ b/packages/frontend/src/paths/instance/orders/list/Table.tsx @@ -31,7 +31,7 @@ import { ConfirmModal } from "../../../../components/modal"; import { MerchantBackend, WithId } from "../../../../declaration"; import { useOrderDetails } from "../../../../hooks/order"; import { Translate, useTranslator } from "../../../../i18n"; -import { AuthorizeTipSchema, RefundSchema as RefundSchema } from "../../../../schemas"; +import { RefundSchema as RefundSchema } from "../../../../schemas"; import { mergeRefunds, subtractPrices, sumPrices } from "../../../../utils/amount"; import { AMOUNT_ZERO_REGEX } from "../../../../utils/constants"; @@ -54,6 +54,7 @@ export function CardTable({ instances, onCreate, onRefund, onCopyURL, onSelect, const [showRefund, setShowRefund] = useState<string | undefined>(undefined) + const i18n = useTranslator() return <div class="card has-table"> <header class="card-header"> @@ -62,9 +63,11 @@ export function CardTable({ instances, onCreate, onRefund, onCopyURL, onSelect, <div class="card-header-icon" aria-label="more options" /> <div class="card-header-icon" aria-label="more options"> + <span class="has-tooltip-left" data-tooltip={i18n`add new order`}> <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> </button> + </span> </div> </header> @@ -114,7 +117,7 @@ function Table({ instances, onSelect, onRefund, onCopyURL, onLoadMoreAfter, onLo <tr> <th style={{ minWidth: 100 }}><Translate>Date</Translate></th> <th style={{ minWidth: 100 }}><Translate>Amount</Translate></th> - <th style={{ minWidth: 500 }}><Translate>Summary</Translate></th> + <th style={{ minWidth: 400 }}><Translate>Summary</Translate></th> <th style={{ minWidth: 50 }} /> </tr> </thead> diff --git a/packages/frontend/src/paths/instance/orders/list/index.tsx b/packages/frontend/src/paths/instance/orders/list/index.tsx index c85db0b..8bfe23d 100644 --- a/packages/frontend/src/paths/instance/orders/list/index.tsx +++ b/packages/frontend/src/paths/instance/orders/list/index.tsx @@ -41,7 +41,7 @@ interface Props { export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNotFound }: Props): VNode { - const [filter, setFilter] = useState<InstanceOrderFilter>({ }) + const [filter, setFilter] = useState<InstanceOrderFilter>({}) const [pickDate, setPickDate] = useState(false) const setNewDate = (date: Date) => setFilter(prev => ({ ...prev, date })) @@ -66,7 +66,7 @@ export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNo async function testIfOrderExistAndSelect() { if (!orderId) { - setErrorOrderId('Enter an order id') + setErrorOrderId(i18n`Enter an order id`) return; } try { @@ -74,11 +74,12 @@ export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNo onSelect(orderId) setErrorOrderId(undefined) } catch { - setErrorOrderId('order not found') + setErrorOrderId(i18n`order not found`) } } const i18n = useTranslator() + const dateTooltip = i18n`jump to order closer to a given date` return <section class="section is-main-section"> <NotificationCard notification={notif} /> @@ -91,11 +92,11 @@ export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNo <input class={errorOrderId ? "input is-danger" : "input"} type="text" value={orderId} onChange={e => setOrderId(e.currentTarget.value)} placeholder={i18n`order id`} /> {errorOrderId && <p class="help is-danger">{errorOrderId}</p>} </div> - <div class="control"> - <a class="button" onClick={testIfOrderExistAndSelect}> + <span class="has-tooltip-bottom" data-tooltip={i18n`view order details`}> + <button class="button" onClick={testIfOrderExistAndSelect}> <span class="icon"><i class="mdi mdi-arrow-right" /></span> - </a> - </div> + </button> + </span> </div> </div> </div> @@ -104,10 +105,26 @@ export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNo <div class="column"> <div class="tabs"> <ul> - <li class={isAllActive}><a onClick={() => setFilter({})}><Translate>All</Translate></a></li> - <li class={isPaidActive}><a onClick={() => setFilter({ paid: 'yes' })}><Translate>Paid</Translate></a></li> - <li class={isRefundedActive}><a onClick={() => setFilter({ refunded: 'yes' })}><Translate>Refunded</Translate></a></li> - <li class={isNotWiredActive}><a onClick={() => setFilter({ wired: 'no' })}><Translate>Not wired</Translate></a></li> + <li class={isAllActive}> + <div class="has-tooltip-right" data-tooltip={i18n`remove all filters`}> + <a onClick={() => setFilter({})}><Translate>All</Translate></a> + </div> + </li> + <li class={isPaidActive}> + <div class="has-tooltip-right" data-tooltip={i18n`filter paid orders`}> + <a onClick={() => setFilter({ paid: 'yes' })}><Translate>Paid</Translate></a> + </div> + </li> + <li class={isRefundedActive}> + <div class="has-tooltip-right" data-tooltip={i18n`filter refunded orders`}> + <a onClick={() => setFilter({ refunded: 'yes' })}><Translate>Refunded</Translate></a> + </div> + </li> + <li class={isNotWiredActive}> + <div class="has-tooltip-left" data-tooltip={i18n`filter not yet wired orders`}> + <a onClick={() => setFilter({ wired: 'no' })}><Translate>Not wired</Translate></a> + </div> + </li> </ul> </div> </div> @@ -120,12 +137,16 @@ export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNo </a> </div>} <div class="control"> - <input class="input" type="text" readonly value={!filter.date ? '' : format(filter.date, 'yyyy/MM/dd')} placeholder={i18n`date (YYYY/MM/DD)`} /> + <span class="has-tooltip-top" data-tooltip={dateTooltip}> + <input class="input" type="text" readonly value={!filter.date ? '' : format(filter.date, 'yyyy/MM/dd')} placeholder={i18n`date (YYYY/MM/DD)`} onClick={() => { setPickDate(true) }} /> + </span> </div> <div class="control"> - <a class="button" onClick={() => { setPickDate(true) }}> - <span class="icon"><i class="mdi mdi-calendar" /></span> - </a> + <span class="has-tooltip-left" data-tooltip={dateTooltip}> + <a class="button" onClick={() => { setPickDate(true) }}> + <span class="icon"><i class="mdi mdi-calendar" /></span> + </a> + </span> </div> </div> </div> diff --git a/packages/frontend/src/paths/instance/products/create/CreatePage.tsx b/packages/frontend/src/paths/instance/products/create/CreatePage.tsx index e6e6f1e..78c0f0d 100644 --- a/packages/frontend/src/paths/instance/products/create/CreatePage.tsx +++ b/packages/frontend/src/paths/instance/products/create/CreatePage.tsx @@ -45,7 +45,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <section class="section is-main-section"> <div class="columns"> <div class="column" /> - <div class="column is-two-thirds"> + <div class="column is-four-fifths"> <ProductForm onSubscribe={addFormSubmitter} /> <div class="buttons is-right mt-5"> diff --git a/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx b/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx index dc39d84..b567504 100644 --- a/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx +++ b/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx @@ -16,6 +16,7 @@ import { h, VNode } from "preact"; import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully"; import { Entity } from "./index"; +import emptyImage from "../../assets/empty.png"; interface Props { entity: Entity; diff --git a/packages/frontend/src/paths/instance/products/list/Table.tsx b/packages/frontend/src/paths/instance/products/list/Table.tsx index 878506d..1b4a7b3 100644 --- a/packages/frontend/src/paths/instance/products/list/Table.tsx +++ b/packages/frontend/src/paths/instance/products/list/Table.tsx @@ -28,6 +28,7 @@ import { InputNumber } from "../../../../components/form/InputNumber" import { MerchantBackend, WithId } from "../../../../declaration" import emptyImage from "../../../../assets/empty.png"; import { Translate, useTranslator } from "../../../../i18n" +import { Amounts } from "@gnu-taler/taler-util" type Entity = MerchantBackend.Products.ProductDetail & WithId @@ -35,23 +36,23 @@ interface Props { instances: Entity[]; onDelete: (id: Entity) => void; onSelect: (product: Entity) => void; - onUpdate: (id: string, data: MerchantBackend.Products.ProductPatchDetail) => void; + onUpdate: (id: string, data: MerchantBackend.Products.ProductPatchDetail) => Promise<void>; onCreate: () => void; selected?: boolean; } export function CardTable({ instances, onCreate, onSelect, onUpdate, onDelete }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string | undefined>(undefined) - + const i18n = useTranslator() return <div class="card has-table"> <header class="card-header"> <p class="card-header-title"><span class="icon"><i class="mdi mdi-shopping" /></span><Translate>Products</Translate></p> - - <div class="card-header-icon" aria-label="more options" /> <div class="card-header-icon" aria-label="more options"> - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> - </button> + <span class="has-tooltip-left" data-tooltip={i18n`add new product`}> + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> + </button> + </span> </div> </header> @@ -71,12 +72,13 @@ interface TableProps { rowSelection: string | undefined; instances: Entity[]; onSelect: (id: Entity) => void; - onUpdate: (id: string, data: MerchantBackend.Products.ProductPatchDetail) => void; + onUpdate: (id: string, data: MerchantBackend.Products.ProductPatchDetail) => Promise<void>; onDelete: (id: Entity) => void; rowSelectionHandler: StateUpdater<string | undefined>; } function Table({ rowSelection, rowSelectionHandler, instances, onSelect, onUpdate, onDelete }: TableProps): VNode { + const i18n = useTranslator() return ( <div class="table-container"> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -108,30 +110,38 @@ function Table({ rowSelection, rowSelectionHandler, instances, onSelect, onUpdat stockInfo = <label title={restStockInfo}>{totalStock} {i.unit}</label> } + const isFree = Amounts.parseOrThrow(i.price).value === 0 + return <Fragment key={i.id}><tr key="info"> <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} > <img src={i.image ? i.image : emptyImage} style={{ border: 'solid black 1px', width: 100, height: 100 }} /> </td> <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.description}</td> - <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.price} / {i.unit}</td> + <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} > + {isFree ? i18n`free` : `${i.price} / ${i.unit}`} + </td> <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{sum(i.taxes)}</td> <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{difference(i.price, sum(i.taxes))}</td> <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{stockInfo}</td> <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.total_sold} {i.unit}</td> <td class="is-actions-cell right-sticky"> <div class="buttons is-right"> - <button class="button is-small is-success jb-modal" type="button" onClick={(): void => onSelect(i)}> - Update - </button> - <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onDelete(i)}> - Delete - </button> + <span class="has-tooltip-bottom" data-tooltip={i18n`go to product update page`}> + <button class="button is-small is-success " type="button" onClick={(): void => onSelect(i)}> + <Translate>Update</Translate> + </button> + </span> + <span class="has-tooltip-left" data-tooltip={i18n`remove this product from the database`}> + <button class="button is-small is-danger" type="button" onClick={(): void => onDelete(i)}> + <Translate>Delete</Translate> + </button> + </span> </div> </td> </tr> {rowSelection === i.id && <tr key="form"> <td colSpan={10} > - <FastProductUpdateForm product={i} onUpdate={(prod) => onUpdate(i.id, prod)} onCancel={() => rowSelectionHandler(undefined)} /> + <FastProductUpdateForm product={i} onUpdate={(prod) => onUpdate(i.id, prod).then(r => rowSelectionHandler(undefined))} onCancel={() => rowSelectionHandler(undefined)} /> </td> </tr>} </Fragment> @@ -144,7 +154,7 @@ function Table({ rowSelection, rowSelectionHandler, instances, onSelect, onUpdat interface FastProductUpdateFormProps { product: Entity; - onUpdate: (data: MerchantBackend.Products.ProductPatchDetail) => void; + onUpdate: (data: MerchantBackend.Products.ProductPatchDetail) => Promise<void>; onCancel: () => void; } interface FastProductUpdate { @@ -152,9 +162,12 @@ interface FastProductUpdate { lost: number; price: string; } +interface UpdatePrice { + price: string; +} function FastProductWithInfiniteStockUpdateForm({ product, onUpdate, onCancel }: FastProductUpdateFormProps) { - const [value, valueHandler] = useState<{ price: string }>({ price: product.price }) + const [value, valueHandler] = useState<UpdatePrice>({ price: product.price }) const i18n = useTranslator() return <Fragment> @@ -164,14 +177,12 @@ function FastProductWithInfiniteStockUpdateForm({ product, onUpdate, onCancel }: <div class="buttons is-right mt-5"> <button class="button" onClick={onCancel} ><Translate>Cancel</Translate></button> - <button class="button is-info" onClick={() => { - - return onUpdate({ + <span class="has-tooltip-left" data-tooltip={i18n`update product with new price`}> + <button class="button is-info" onClick={() => onUpdate({ ...product, price: value.price, - }) - - }}><Translate>Confirm</Translate></button> + })}><Translate>Confirm</Translate></button> + </span> </div> </Fragment> @@ -190,12 +201,6 @@ function FastProductWithManagedStockUpdateForm({ product, onUpdate, onCancel }: : undefined } - const stockUpdateDescription = errors.lost ? '' : ( - !!value.incoming || !!value.lost ? - `current stock will change from ${currentStock} to ${currentStock + value.incoming - value.lost}` : - `current stock will stay at ${currentStock}` - ) - const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) const i18n = useTranslator() @@ -203,29 +208,20 @@ function FastProductWithManagedStockUpdateForm({ product, onUpdate, onCancel }: <FormProvider<FastProductUpdate> name="added" errors={errors} object={value} valueHandler={valueHandler as any} > <InputNumber<FastProductUpdate> name="incoming" label={i18n`Incoming`} tooltip={i18n`add more elements to the inventory`} /> <InputNumber<FastProductUpdate> name="lost" label={i18n`Lost`} tooltip={i18n`report elements lost in the inventory`} /> - <div class="field is-horizontal"> - <div class="field-label is-normal" /> - <div class="field-body is-flex-grow-3"> - <div class="field"> - {stockUpdateDescription} - </div> - </div> - </div> <InputCurrency<FastProductUpdate> name="price" label={i18n`Price`} tooltip={i18n`new price for the product`} /> </FormProvider> <div class="buttons is-right mt-5"> <button class="button" onClick={onCancel} ><Translate>Cancel</Translate></button> - <button class="button is-info" disabled={hasErrors} onClick={() => { - - return onUpdate({ + <span class="has-tooltip-left" data-tooltip={hasErrors ? i18n`the are value with errors` : i18n`update product with new stock and price`}> + <button class="button is-info" disabled={hasErrors} onClick={() => onUpdate({ ...product, total_stock: product.total_stock + value.incoming, total_lost: product.total_lost + value.lost, price: value.price, }) - - }}><Translate>Confirm</Translate></button> + }><Translate>Confirm</Translate></button> + </span> </div> </Fragment> diff --git a/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx b/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx index e0a2b16..1dfca99 100644 --- a/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx +++ b/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx @@ -26,7 +26,7 @@ import { MerchantBackend, WithId } from "../../../../declaration"; import { useListener } from "../../../../hooks"; import { Translate } from "../../../../i18n"; -type Entity = MerchantBackend.Products.ProductDetail & { product_id: string} +type Entity = MerchantBackend.Products.ProductDetail & { product_id: string } interface Props { onUpdate: (d: Entity) => Promise<void>; @@ -41,17 +41,30 @@ export function UpdatePage({ product, onUpdate, onBack }: Props): VNode { }) return <div> - <section class="section is-main-section"> + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"><Translate>Product id:</Translate><b>{product.product_id}</b></span> + </div> + </div> + </div> + </div> + </section> + <hr /> + <div class="columns"> <div class="column" /> - <div class="column is-two-thirds"> + <div class="column is-four-fifths"> <ProductForm initial={product} onSubscribe={addFormSubmitter} alreadyExist /> <div class="buttons is-right mt-5"> {onBack && <button class="button" onClick={onBack} ><Translate>Cancel</Translate></button>} <AsyncButton onClick={submitForm} disabled={!submitForm}><Translate>Confirm</Translate></AsyncButton> </div> - </div> <div class="column" /> </div> diff --git a/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx b/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx index 8669242..cdaf475 100644 --- a/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx +++ b/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx @@ -133,7 +133,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <section class="section is-main-section"> <div class="columns"> <div class="column" /> - <div class="column is-two-thirds"> + <div class="column is-four-fifths"> <div class="tabs is-toggle is-fullwidth is-small"> <ul> diff --git a/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx b/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx index 08942f6..06fbf20 100644 --- a/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx +++ b/packages/frontend/src/paths/instance/reserves/details/DetailPage.tsx @@ -20,22 +20,16 @@ */ import { Amounts } from "@gnu-taler/taler-util"; -import { format, isAfter } from "date-fns"; +import { format } from "date-fns"; import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; import { FormProvider } from "../../../../components/form/FormProvider"; import { Input } from "../../../../components/form/Input"; import { InputCurrency } from "../../../../components/form/InputCurrency"; import { InputDate } from "../../../../components/form/InputDate"; -import { InputDuration } from "../../../../components/form/InputDuration"; -import { InputGroup } from "../../../../components/form/InputGroup"; -import { InputLocation } from "../../../../components/form/InputLocation"; -import { NotificationCard } from "../../../../components/menu"; -import { ProductList } from "../../../../components/product/ProductList"; +import { TextField } from "../../../../components/form/TextField"; import { MerchantBackend } from "../../../../declaration"; import { useTipDetails } from "../../../../hooks/tips"; import { Translate, useTranslator } from "../../../../i18n"; -import { mergeRefunds } from "../../../../utils/amount"; type Entity = MerchantBackend.Tips.ReserveDetail; type CT = MerchantBackend.ContractTerms @@ -49,59 +43,60 @@ interface Props { export function DetailPage({ id, selected, onBack }: Props): VNode { const i18n = useTranslator() const didExchangeAckTransfer = Amounts.isNonZero(Amounts.parseOrThrow(selected.exchange_initial_amount)) - return <div class="section main-section"> - <FormProvider object={{ ...selected, id }} valueHandler={null} > - <InputDate<Entity> name="creation_time" label={i18n`Created at`} readonly /> - <InputDate<Entity> name="expiration_time" label={i18n`Valid until`} readonly /> - <InputCurrency<Entity> name="merchant_initial_amount" label={i18n`Created balance`} readonly /> - <Input<Entity> name="exchange_url" label={i18n`Exchange URL`} readonly /> - - {didExchangeAckTransfer && <Fragment> - <InputCurrency<Entity> name="exchange_initial_amount" label={i18n`Exchange balance`} readonly /> - <InputCurrency<Entity> name="pickup_amount" label={i18n`Picked up`} readonly /> - <InputCurrency<Entity> name="committed_amount" label={i18n`Committed`} readonly /> - </Fragment> - } - <Input<Entity> name="payto_uri" label={i18n`Account address`} readonly /> - <Input name="id" label={i18n`Subject`} readonly /> - </FormProvider> - - {didExchangeAckTransfer ? <Fragment> - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"><i class="mdi mdi-cash-register" /></span> - <Translate>Tips</Translate> - </p> - - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {selected.tips && selected.tips.length > 0 ? <Table tips={selected.tips} /> : <EmptyTable />} - </div></div> - </div> - </div> - </Fragment> : <Fragment> - <p class="is-size-5"><Translate>Now you should transfer to the exchange into the account address indicated above and the transaction must carry the subject message.</Translate></p> - - <p class="is-size-5"><Translate>For example :</Translate></p> - <pre> - {selected.payto_uri}?message={id}&amount={selected.merchant_initial_amount} - </pre> - </Fragment> - } - - <div class="columns"> - <div class="column" /> - <div class="column is-two-thirds"> + return <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <div class="section main-section"> + <FormProvider object={{ ...selected, id }} valueHandler={null} > + <InputDate<Entity> name="creation_time" label={i18n`Created at`} readonly /> + <InputDate<Entity> name="expiration_time" label={i18n`Valid until`} readonly /> + <InputCurrency<Entity> name="merchant_initial_amount" label={i18n`Created balance`} readonly /> + <TextField<Entity> name="exchange_url" label={i18n`Exchange URL`} readonly > + <a target="_blank" href={selected.exchange_url}>{selected.exchange_url}</a> + </TextField> + + {didExchangeAckTransfer && <Fragment> + <InputCurrency<Entity> name="exchange_initial_amount" label={i18n`Exchange balance`} readonly /> + <InputCurrency<Entity> name="pickup_amount" label={i18n`Picked up`} readonly /> + <InputCurrency<Entity> name="committed_amount" label={i18n`Committed`} readonly /> + </Fragment> + } + <Input<Entity> name="payto_uri" label={i18n`Account address`} readonly /> + <Input name="id" label={i18n`Subject`} readonly /> + </FormProvider> + + {didExchangeAckTransfer ? <Fragment> + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"><i class="mdi mdi-cash-register" /></span> + <Translate>Tips</Translate> + </p> + + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {selected.tips && selected.tips.length > 0 ? <Table tips={selected.tips} /> : <EmptyTable />} + </div></div> + </div> + </div> + </Fragment> : <Fragment> + <p class="is-size-5"><Translate>Now you should transfer to the exchange into the account address indicated above and the transaction must carry the subject message.</Translate></p> + <p class="is-size-5"><Translate>For example :</Translate></p> + <pre> + {selected.payto_uri}?message={id}&amount={selected.merchant_initial_amount} + </pre> + </Fragment> + } + <div class="buttons is-right mt-5"> <button class="button" onClick={onBack}><Translate>Back</Translate></button> </div> + </div> - <div class="column" /> </div> - + <div class="column" /> </div> } diff --git a/packages/frontend/src/paths/instance/reserves/list/Table.tsx b/packages/frontend/src/paths/instance/reserves/list/Table.tsx index c53dd2a..6bca85b 100644 --- a/packages/frontend/src/paths/instance/reserves/list/Table.tsx +++ b/packages/frontend/src/paths/instance/reserves/list/Table.tsx @@ -22,7 +22,7 @@ import { format } from "date-fns" import { Fragment, h, VNode } from "preact" import { MerchantBackend, WithId } from "../../../../declaration" -import { Translate } from "../../../../i18n" +import { Translate, useTranslator } from "../../../../i18n" type Entity = MerchantBackend.Tips.ReserveStatusEntry & WithId @@ -47,6 +47,7 @@ export function CardTable({ instances, onCreate, onSelect, onNewTip, onDelete }: return prev }, new Array<Array<Entity>>([], [])) + const i18n = useTranslator() return <Fragment> {withoutFunds.length > 0 && <div class="card has-table"> @@ -67,9 +68,12 @@ export function CardTable({ instances, onCreate, onSelect, onNewTip, onDelete }: <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash" /></span><Translate>Reserves ready</Translate></p> <div class="card-header-icon" aria-label="more options" /> <div class="card-header-icon" aria-label="more options"> - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> - </button> + <span class="has-tooltip-left" data-tooltip={i18n`add new reserve`}> + + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> + </button> + </span> </div> </header> <div class="card-content"> diff --git a/packages/frontend/src/paths/instance/transfers/create/CreatePage.tsx b/packages/frontend/src/paths/instance/transfers/create/CreatePage.tsx index 748722f..861268f 100644 --- a/packages/frontend/src/paths/instance/transfers/create/CreatePage.tsx +++ b/packages/frontend/src/paths/instance/transfers/create/CreatePage.tsx @@ -77,7 +77,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <section class="section is-main-section"> <div class="columns"> <div class="column" /> - <div class="column is-two-thirds"> + <div class="column is-four-fifths"> <FormProvider object={state} valueHandler={setState} errors={errors}> <Input<Entity> name="wtid" label={i18n`Transfer ID`} help="" tooltip={i18n`unique identifier of the wire transfer, usually 52 random characters long`} /> diff --git a/packages/frontend/src/paths/instance/transfers/list/Table.tsx b/packages/frontend/src/paths/instance/transfers/list/Table.tsx index ad7248f..3794b01 100644 --- a/packages/frontend/src/paths/instance/transfers/list/Table.tsx +++ b/packages/frontend/src/paths/instance/transfers/list/Table.tsx @@ -59,6 +59,7 @@ export function CardTable({ instances, onCreate, onUpdate, onDelete, selected, o } }, [actionQueue, selected, onUpdate]) + const i18n = useTranslator() return <div class="card has-table"> <header class="card-header"> @@ -72,9 +73,11 @@ export function CardTable({ instances, onCreate, onUpdate, onDelete, selected, o </button> </div> <div class="card-header-icon" aria-label="more options"> - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> - </button> + <span class="has-tooltip-left" data-tooltip={i18n`add new transfer`}> + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small" ><i class="mdi mdi-plus mdi-36px" /></span> + </button> + </span> </div> </header> diff --git a/packages/frontend/src/paths/instance/transfers/list/index.tsx b/packages/frontend/src/paths/instance/transfers/list/index.tsx index 5486451..5effb5f 100644 --- a/packages/frontend/src/paths/instance/transfers/list/index.tsx +++ b/packages/frontend/src/paths/instance/transfers/list/index.tsx @@ -74,7 +74,7 @@ export default function ListTransfer({ onUnauthorized, onLoadError, onCreate, on return <section class="section is-main-section"> <div class="columns"> <div class="column" /> - <div class="column is-6"> + <div class="column is-10"> <FormProvider object={form} valueHandler={setForm as any}> <InputSelector name="payto_uri" label={i18n`Address`} values={accounts} @@ -87,9 +87,21 @@ export default function ListTransfer({ onUnauthorized, onLoadError, onCreate, on </div> <div class="tabs"> <ul> - <li class={isAllTransfers}><a onClick={() => setFilter(undefined)}><Translate>All</Translate></a></li> - <li class={isVerifiedTransfers}><a onClick={() => setFilter('yes')}><Translate>Verified</Translate></a></li> - <li class={isNonVerifiedTransfers}><a onClick={() => setFilter('no')}><Translate>Non Verified</Translate></a></li> + <li class={isAllTransfers}> + <div class="has-tooltip-right" data-tooltip={i18n`remove all filters`}> + <a onClick={() => setFilter(undefined)}><Translate>All</Translate></a> + </div> + </li> + <li class={isVerifiedTransfers}> + <div class="has-tooltip-right" data-tooltip={i18n`remove all filters`}> + <a onClick={() => setFilter('yes')}><Translate>Verified</Translate></a> + </div> + </li> + <li class={isNonVerifiedTransfers}> + <div class="has-tooltip-right" data-tooltip={i18n`remove all filters`}> + <a onClick={() => setFilter('no')}><Translate>Non Verified</Translate></a> + </div> + </li> </ul> </div> <View diff --git a/packages/frontend/src/paths/instance/update/UpdatePage.tsx b/packages/frontend/src/paths/instance/update/UpdatePage.tsx index 3fe17ff..5367e5d 100644 --- a/packages/frontend/src/paths/instance/update/UpdatePage.tsx +++ b/packages/frontend/src/paths/instance/update/UpdatePage.tsx @@ -36,6 +36,7 @@ import { useInstanceContext } from "../../../context/instance"; import { MerchantBackend } from "../../../declaration"; import { Translate, useTranslator } from "../../../i18n"; import { InstanceUpdateSchema as schema } from '../../../schemas'; +import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields"; type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & { auth_token?: string } @@ -101,8 +102,6 @@ export function UpdatePage({ onUpdate, onChangeAuth, selected, onBack }: Props): } const [active, setActive] = useState(false); - const i18n = useTranslator() - return <div> <section class="section"> @@ -112,7 +111,7 @@ export function UpdatePage({ onUpdate, onChangeAuth, selected, onBack }: Props): <div class="level"> <div class="level-left"> <div class="level-item"> - <span class="is-size-4">Instance id: <b>{id}</b></span> + <span class="is-size-4"><Translate>Instance id</Translate>: <b>{id}</b></span> </div> </div> <div class="level-right"> @@ -120,7 +119,7 @@ export function UpdatePage({ onUpdate, onChangeAuth, selected, onBack }: Props): <h1 class="title"> <button class="button is-danger" onClick={(): void => { setActive(!active); }} > <div class="icon is-left"><i class="mdi mdi-lock-reset" /></div> - <span><Translate>Manage token</Translate></span> + <span><Translate>Manage access token</Translate></span> </button> </h1> </div> @@ -142,42 +141,25 @@ export function UpdatePage({ onUpdate, onChangeAuth, selected, onBack }: Props): <div class="column" /> </div> <hr /> + <div class="columns"> <div class="column" /> <div class="column is-four-fifths"> <FormProvider<Entity> errors={errors} object={value} valueHandler={valueHandler} > - <Input<Entity> name="name" label={i18n`Name`} tooltip={i18n`display name of this instance`} /> - - <InputPayto<Entity> name="payto_uris" label={i18n`Account address`} help="x-taler-bank/bank.taler:5882/blogger" /> - - <InputCurrency<Entity> name="default_max_deposit_fee" label={i18n`Default max deposit fee`} /> - - <InputCurrency<Entity> name="default_max_wire_fee" label={i18n`Default max wire fee`} /> - - <Input<Entity> name="default_wire_fee_amortization" inputType="number" label={i18n`Default wire fee amortization`} /> - - <InputGroup name="address" label={i18n`Address`}> - <InputLocation name="address" /> - </InputGroup> - - <InputGroup name="jurisdiction" label={i18n`Jurisdiction`}> - <InputLocation name="jurisdiction" /> - </InputGroup> - - <InputDuration<Entity> name="default_pay_delay" label={i18n`Default payment delay`} /> - - <InputDuration<Entity> name="default_wire_transfer_delay" label={i18n`Default wire transfer delay`} /> + <DefaultInstanceFormFields showId={false} /> </FormProvider> <div class="buttons is-right mt-4"> - <button class="button" onClick={onBack} ><Translate>Cancel</Translate></button> + <button class="button" onClick={onBack} data-tooltip="cancel operation"><Translate>Cancel</Translate></button> + <AsyncButton onClick={submit} disabled={hasErrors} ><Translate>Confirm</Translate></AsyncButton> </div> </div> <div class="column" /> </div> + </section> </div > diff --git a/packages/frontend/src/scss/main.scss b/packages/frontend/src/scss/main.scss index 08e2f79..100cadc 100644 --- a/packages/frontend/src/scss/main.scss +++ b/packages/frontend/src/scss/main.scss @@ -13,14 +13,14 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - - /** + +/** * * @author Sebastian Javier Marchano (sebasjm) */ - - /* Theme style (colors & sizes) */ - @import "theme-default"; + +/* Theme style (colors & sizes) */ +@import "theme-default"; /* Core Libs & Lib configs */ @import "libs/all"; @@ -83,28 +83,28 @@ $tooltip-color: red; } .toast > .message { - white-space:pre-wrap; - opacity:80%; + white-space: pre-wrap; + opacity: 80%; } div { &.is-loading { - position: relative; - pointer-events: none; - opacity: 0.5; - &:after { - // @include loader; - position: absolute; - top: calc(50% - 2.5em); - left: calc(50% - 2.5em); - width: 5em; - height: 5em; - border-width: 0.25em; - } + position: relative; + pointer-events: none; + opacity: 0.5; + &:after { + // @include loader; + position: absolute; + top: calc(50% - 2.5em); + left: calc(50% - 2.5em); + width: 5em; + height: 5em; + border-width: 0.25em; + } } } -input[type=checkbox]:indeterminate + .check { +input[type="checkbox"]:indeterminate + .check { background: red !important; } @@ -115,7 +115,7 @@ input[type=checkbox]:indeterminate + .check { } .right-sticky .buttons { - flex-wrap: nowrap + flex-wrap: nowrap; } .table.is-striped tbody tr:not(.is-selected):nth-child(even) .right-sticky { @@ -132,13 +132,12 @@ tr:hover .right-sticky { .content-full-size { height: calc(100% - 3rem); position: absolute; - width: calc(100% - 14rem); - display:flex; + width: calc(100% - 14rem); + display: flex; } .content-full-size .column .card { min-width: 200px; - } @include touch { @@ -146,7 +145,7 @@ tr:hover .right-sticky { height: 100%; position: absolute; width: 100%; - } + } } .column.is-half { @@ -158,9 +157,21 @@ input:read-only { cursor: initial; } -span.icon[data-tooltip]:before { - z-index: 1000; - max-width: 250px; - transform: inherit; - // white-space: normal; +[data-tooltip]:before { + max-width: 15rem; + width: max-content; + text-align: left; + transition: opacity 0.1s linear 1s; + // transform: inherit !important; + white-space: pre-wrap !important; + font-weight: normal; + // position: relative; +} + +.icon[data-tooltip]:before { + transition: none; +} + +span[data-tooltip] { + border-bottom: none; } |