commit 1f89d3a791d201a1cbfe0a0993e648c4bcb61853 parent 38b872bd8cd388cbb1d09369a7f5e78535dcc213 Author: Sebastian <sebasjm@gmail.com> Date: Thu, 25 Mar 2021 10:42:30 -0300 refund modal Diffstat:
21 files changed, 682 insertions(+), 150 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -5,8 +5,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Future work] - - complete product list information (#6792) - - complete order list information (#6793) - gettext templates should be generated from the source code (#6791) - date format (error handling) @@ -27,9 +25,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - check if there is a way to remove auto async for /routes /components/{async,routes} so it can be turned on when building non-single-bundle - product detail: we could have some button that brings us to thedetailed screen for the product - - + + - BUG: updating instance, shoot POST /instance/default/private/auth with token method:token token:secret-token:undefined ## [Unreleased] + - complete order list information (#6793) + - complete product list information (#6792) + - missing fields in the instance update + ## [0.0.5] - 2021-03-18 - change the admin title to "instances" if we are listing the instances and "settings: $ID" on updating instances (#6790) - update title with: Taler Backoffice: $PAGE_TITLE (#6790) diff --git a/packages/frontend/src/ApplicationReadyRoutes.tsx b/packages/frontend/src/ApplicationReadyRoutes.tsx @@ -20,7 +20,6 @@ */ import { Fragment, h, VNode } from 'preact'; import { route } from 'preact-router'; -import { Notification } from "./utils/types"; import { useBackendContext } from './context/backend'; import { useBackendInstancesTestForAdmin } from "./hooks/backend"; import { InstanceRoutes } from "./InstanceRoutes"; @@ -63,13 +62,13 @@ export function ApplicationReadyRoutes(): VNode { const path = new URL(backendURL).pathname const match = INSTANCE_ID_LOOKUP.exec(path) if (!match || !match[1]) { - // this should be rare becuase + // this should be rare because // query to /config is ok but the URL // doest not match with our pattern return <Fragment> <NotYetReadyAppMenu title="Error" onLogout={clearTokenAndGoToRoot} /> <NotificationCard notification={{ - message: i18n`Couldnt access the server`, + message: i18n`Couldn't access the server`, description: i18n`Could not infer instance id from url ${backendURL}`, type: 'ERROR', }} @@ -88,7 +87,7 @@ export function ApplicationReadyRoutes(): VNode { return <Fragment> <NotYetReadyAppMenu title="Error" /> <NotificationCard notification={{ - message: i18n`Couldnt access the server`, + message: i18n`Couldn't access the server`, description: list.error.message, type: 'ERROR' }} /> diff --git a/packages/frontend/src/InstanceRoutes.tsx b/packages/frontend/src/InstanceRoutes.tsx @@ -40,7 +40,7 @@ import ProductUpdatePage from './paths/instance/products/update' import OrderListPage from './paths/instance/orders/list' import OrderCreatePage from './paths/instance/orders/create' -import OrderUpdatePage from './paths/instance/orders/update' +import OrderDetailsPage from './paths/instance/orders/details' import TipListPage from './paths/instance/tips/list' import TipCreatePage from './paths/instance/tips/create' @@ -63,8 +63,8 @@ export enum InstancePaths { product_new = '/product/new', order_list = '/orders', - order_update = '/order/:oid/update', - // order_new = '/oreder/new', + order_details = '/order/:oid/details', + // order_new = '/order/new', // tips_list = '/tips', // tips_update = '/tip/:rid/update', @@ -241,6 +241,18 @@ export function InstanceRoutes({ id, admin }: Props): VNode { }} onLoadError={(error: SwrError) => { + if (admin) { + return <Fragment> + <NotificationCard notification={{ + message: 'No default instance', + description: 'in order to use merchant backoffice, you should create the default instance', + type: 'INFO' + }} /> + <InstanceCreatePage onError={() => null} forceId="default" onConfirm={() => { + route(AdminPaths.list_instances) + }} /> + </Fragment> + } return <Fragment> <NotificationCard notification={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR' }} /> <LoginPage onConfirm={updateLoginStatus} /> @@ -320,6 +332,10 @@ export function InstanceRoutes({ id, admin }: Props): VNode { <Route path={InstancePaths.order_list} component={OrderListPage} + onSelect={ (id:string) => { + route(InstancePaths.order_details.replace(':oid',id)) + }} + onUnauthorized={() => { return <Fragment> <NotificationCard notification={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} /> @@ -350,8 +366,30 @@ export function InstanceRoutes({ id, admin }: Props): VNode { </Fragment> }} /> - <Route path={InstancePaths.order_update} - component={OrderUpdatePage} + <Route path={InstancePaths.order_details} + component={OrderDetailsPage} + + onUnauthorized={() => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} + + onNotFound={() => { + return <NotFoundPage /> + }} + + onLoadError={(error: SwrError) => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR' }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} + + onBack={() => { + route(InstancePaths.order_list) + }} /> {/* <Route path={InstancePaths.tips_list} @@ -432,7 +470,7 @@ export function InstanceRoutes({ id, admin }: Props): VNode { {/* example of loading page*/} <Route path="/loading" component={Loading} /> - <Route default component={NotFoundPage} /> + <Route default component={NotFoundPage} /> </Router> </InstanceContextProvider>; diff --git a/packages/frontend/src/components/form/InputSelector.tsx b/packages/frontend/src/components/form/InputSelector.tsx @@ -0,0 +1,71 @@ +/* + 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 { h, VNode } from "preact"; +import { Message, useMessage } from "preact-messages"; +import { useField } from "./Field"; + +interface Props<T> { + name: T; + readonly?: boolean; + expand?: boolean; + values: string[]; + toStr?: (v?: any) => string; + fromStr?: (s: string) => any; +} + +const defaultToString = (f?: any): string => f || '' +const defaultFromString = (v: string): any => v as any + +export function InputSelector<T>({ name, readonly, expand, values, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode { + const { error, value, onChange } = useField<T>(name); + + const placeholder = useMessage(`fields.instance.${name}.placeholder`); + const tooltip = useMessage(`fields.instance.${name}.tooltip`); + + return <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <Message id={`fields.instance.${name}.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 select" : "control select"}> + <select class={error ? "select is-danger" : "select"} + name={String(name)} disabled={readonly} readonly={readonly} + onChange={(e) => { onChange(fromStr(e.currentTarget.value)) }}> + <option>{placeholder}</option> + {values + // .filter((l) => l !== value) + .map(v => <option value={toStr(v)}>{toStr(v)}</option>)} + </select> + <Message id={`fields.instance.${name}.help`}> </Message> + </p> + {error ? <p class="help is-danger"> + <Message id={`validation.${error.type}`} fields={error.params}>{error.message} </Message> + </p> : null} + </div> + </div> + </div>; +} diff --git a/packages/frontend/src/components/menu/SideBar.tsx b/packages/frontend/src/components/menu/SideBar.tsx @@ -105,14 +105,21 @@ export function Sidebar({ mobile, instance, onLogout, admin }: Props): VNode { </span> </div> </li> - {admin && + {admin && <Fragment> + <p class="menu-label">Instances</p> <li> <a href="/instance/new" class="has-icon"> <span class="icon"><i class="mdi mdi-plus" /></span> - <span class="menu-item-label">New Instance</span> + <span class="menu-item-label">New</span> </a> </li> - } + <li> + <a href="/instances" class="has-icon"> + <span class="icon"><i class="mdi mdi-format-list-bulleted" /></span> + <span class="menu-item-label">List</span> + </a> + </li> + </Fragment>} <li> <a class="has-icon is-state-info is-hoverable" onClick={(): void => onLogout()}> <span class="icon"><i class="mdi mdi-logout default" /></span> diff --git a/packages/frontend/src/components/menu/index.tsx b/packages/frontend/src/components/menu/index.tsx @@ -31,7 +31,7 @@ function getInstanceTitle(path: string, id: string): string { case InstancePaths.update: return `${id}: Settings` case InstancePaths.order_list: return `${id}: Orders` // case InstancePaths.order_new: return `${id}: New order` - case InstancePaths.order_update: return `${id}: Update 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` @@ -93,15 +93,13 @@ interface NotifProps { notification?: Notification; } export function NotificationCard({ notification:n }: NotifProps) { - // const [n, setNotif] = useState(notification) if (!n) return null return <div class="notification"> <div class="columns is-vcentered"> <div class="column is-12"> - <article class={n.type === 'ERROR' ? "message is-danger" : "message is-info"}> + <article class={n.type === 'ERROR' ? "message is-danger" : (n.type === 'WARN' ? "message is-warning" : "message is-info")}> <div class="message-header"> <p>{n.message}</p> - {/* {n.type !== 'ERROR' && <button class="delete" aria-label="delete" onClick={() => setNotif(undefined)}></button> } */} </div> <div class="message-body"> {n.description} diff --git a/packages/frontend/src/components/modal/index.tsx b/packages/frontend/src/components/modal/index.tsx @@ -42,15 +42,17 @@ 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"> <Message id={description} /></p> } + {!description ? null : <p class="modal-card-title"> <Message id={description} /></p>} <button class="delete " aria-label="close" onClick={onCancel} /> </header> <section class="modal-card-body"> {children} </section> <footer class="modal-card-foot"> - <button class="button " onClick={onCancel} ><Message id="Cancel" /></button> - <button class={danger ? "button is-danger " : "button is-info "} disabled={disabled} onClick={onConfirm} ><Message id="Confirm" /></button> + <div class="buttons is-right" style={{width: '100%'}}> + <button class="button " onClick={onCancel} ><Message id="Cancel" /></button> + <button class={danger ? "button is-danger " : "button is-info "} disabled={disabled} onClick={onConfirm} ><Message id="Confirm" /></button> + </div> </footer> </div> <button class="modal-close is-large " aria-label="close" onClick={onCancel} /> @@ -62,7 +64,7 @@ export function ClearConfirmModal({ description, onCancel, onClear, onConfirm, c <div class="modal-background " onClick={onCancel} /> <div class="modal-card"> <header class="modal-card-head"> - {!description ? null : <p class="modal-card-title"> <Message id={description} /></p> } + {!description ? null : <p class="modal-card-title"> <Message id={description} /></p>} <button class="delete " aria-label="close" onClick={onCancel} /> </header> <section class="modal-card-body"> @@ -100,16 +102,16 @@ interface UpdateTokenModalProps { } export function UpdateTokenModal({ element, onCancel, onClear, onConfirm, oldToken }: UpdateTokenModalProps): VNode { - type State = {old_token: string, new_token: string} + type State = { old_token: string, new_token: string } const [form, setValue] = useState<Partial<State>>({ old_token: '', new_token: '' }) const errors = { old_token: oldToken && oldToken !== form.old_token ? { message: 'should be the same' } : undefined, - new_token: !form.new_token ? { message: 'should be the same' } : ( form.new_token === form.old_token ? { message: 'cant repeat' } : undefined ), + new_token: !form.new_token ? { message: 'should be the same' } : (form.new_token === form.old_token ? { message: 'cant repeat' } : undefined), } - + return <ClearConfirmModal description="update_token" onCancel={onCancel} onConfirm={() => onConfirm(form.new_token!)} @@ -124,3 +126,5 @@ export function UpdateTokenModal({ element, onCancel, onClear, onConfirm, oldTok <p>Clearing the auth token will mean public access to the instance</p> </ClearConfirmModal> } + + diff --git a/packages/frontend/src/declaration.d.ts b/packages/frontend/src/declaration.d.ts @@ -528,6 +528,136 @@ export namespace MerchantBackend { } namespace Orders { + + type MerchantOrderStatusResponse = CheckPaymentPaidResponse | + CheckPaymentClaimedResponse | + CheckPaymentUnpaidResponse; + interface CheckPaymentPaidResponse { + // The customer paid for this contract. + order_status: "paid"; + + // Was the payment refunded (even partially)? + refunded: boolean; + + // True if there are any approved refunds that the wallet has + // not yet obtained. + refund_pending: boolean; + + // Did the exchange wire us the funds? + wired: boolean; + + // Total amount the exchange deposited into our bank account + // for this contract, excluding fees. + deposit_total: Amount; + + // Numeric error code indicating errors the exchange + // encountered tracking the wire transfer for this purchase (before + // we even got to specific coin issues). + // 0 if there were no issues. + exchange_ec: number; + + // HTTP status code returned by the exchange when we asked for + // information to track the wire transfer for this purchase. + // 0 if there were no issues. + exchange_hc: number; + + // Total amount that was refunded, 0 if refunded is false. + refund_amount: Amount; + + // Contract terms. + contract_terms: ContractTerms; + + // The wire transfer status from the exchange for this order if + // available, otherwise empty array. + wire_details: TransactionWireTransfer[]; + + // Reports about trouble obtaining wire transfer details, + // empty array if no trouble were encountered. + wire_reports: TransactionWireReport[]; + + // The refund details for this order. One entry per + // refunded coin; empty array if there are no refunds. + refund_details: RefundDetails[]; + + // Status URL, can be used as a redirect target for the browser + // to show the order QR code / trigger the wallet. + order_status_url: string; + } + interface CheckPaymentClaimedResponse { + // A wallet claimed the order, but did not yet pay for the contract. + order_status: "claimed"; + + // Contract terms. + contract_terms: ContractTerms; + + } + interface CheckPaymentUnpaidResponse { + // The order was neither claimed nor paid. + order_status: "unpaid"; + + // URI that the wallet must process to complete the payment. + taler_pay_uri: string; + + // Alternative order ID which was paid for already in the same session. + // Only given if the same product was purchased before in the same session. + already_paid_order_id?: string; + + // Fulfillment URL of an already paid order. Only given if under this + // session an already paid order with a fulfillment URL exists. + already_paid_fulfillment_url?: string; + + // Status URL, can be used as a redirect target for the browser + // to show the order QR code / trigger the wallet. + order_status_url: string; + + // We do we NOT return the contract terms here because they may not + // exist in case the wallet did not yet claim them. + } + interface RefundDetails { + // Reason given for the refund. + reason: string; + + // When was the refund approved. + timestamp: Timestamp; + + // Total amount that was refunded (minus a refund fee). + amount: Amount; + } + interface TransactionWireTransfer { + // Responsible exchange. + exchange_url: string; + + // 32-byte wire transfer identifier. + wtid: Base32; + + // Execution time of the wire transfer. + execution_time: Timestamp; + + // Total amount that has been wire transferred + // to the merchant. + amount: Amount; + + // Was this transfer confirmed by the merchant via the + // POST /transfers API, or is it merely claimed by the exchange? + confirmed: boolean; + } + interface TransactionWireReport { + // Numerical error code. + code: number; + + // Human-readable error description. + hint: string; + + // Numerical error code from the exchange. + exchange_ec: number; + + // HTTP status code received from the exchange. + exchange_hc: number; + + // Public key of the coin for which we got the exchange error. + coin_pub: CoinPublicKey; + } + interface OrderHistory { // timestamp-sorted array of all orders matching the query. // The order of the sorting depends on the sign of delta. diff --git a/packages/frontend/src/hooks/backend.ts b/packages/frontend/src/hooks/backend.ts @@ -25,7 +25,7 @@ import { MerchantBackend } from '../declaration'; import { useBackendContext, useInstanceContext } from '../context/backend'; import { useEffect, useMemo, useState } from 'preact/hooks'; import { MAX_RESULT_SIZE, PAGE_SIZE } from '../utils/constants'; -import { format, max } from 'date-fns'; +import { add, addHours, addSeconds, format, max } from 'date-fns'; function mutateAll(re: RegExp) { cache.keys().filter(key => re.test(key)).forEach(key => mutate(key, null)) @@ -99,7 +99,8 @@ async function request(url: string, options: RequestOptions = {}): Promise<any> } catch (e) { const info = e.response?.data const status = e.response?.status - throw { info, status, message: e.message, backend: url, hasToken: !!options.token } + const hint = info?.hint + throw { info, status, message: hint || e.message, backend: url, hasToken: !!options.token } } } @@ -110,8 +111,12 @@ function fetcher(url: string, token: string, backend: string) { type YesOrNo = 'yes' | 'no'; -function orderFetcher(url: string, token: string, backend: string, paid?: YesOrNo, refunded?: YesOrNo, wired?: YesOrNo, date?: Date, delta?: number) { - return request(`${backend}${url}`, { token, params: { paid, refunded, wired, delta, date: date? format(date, 'yyyy-MM-dd HH:mm:ss'): undefined } }) +function orderFetcher(url: string, token: string, backend: string, paid?: YesOrNo, refunded?: YesOrNo, wired?: YesOrNo, searchDate?: Date, delta?: number) { + const newDate = searchDate && addHours(searchDate, 3) // remove this, locale + // if we are + const newDatePlus1SecIfNeeded = delta && delta < 0 && newDate ? addSeconds(newDate, 1) : newDate + const date = newDatePlus1SecIfNeeded ? format(newDatePlus1SecIfNeeded, 'yyyy-MM-dd HH:mm:ss') : undefined + return request(`${backend}${url}`, { token, params: { paid, refunded, wired, delta, date } }) } function transferFetcher(url: string, token: string, backend: string) { @@ -213,6 +218,7 @@ interface OrderMutateAPI { //FIXME: add OutOfStockResponse on 410 createOrder: (data: MerchantBackend.Orders.PostOrderRequest) => Promise<MerchantBackend.Orders.PostOrderResponse>; forgetOrder: (id: string, data: MerchantBackend.Orders.ForgetRequest) => Promise<void>; + refundOrder: (id: string, data: MerchantBackend.Orders.RefundRequest) => Promise<MerchantBackend.Orders.MerchantRefundResponse>; deleteOrder: (id: string) => Promise<void>; } @@ -236,6 +242,17 @@ export function useOrderMutateAPI(): OrderMutateAPI { mutateAll(/@"\/private\/orders"@/) return res } + const refundOrder = async (orderId: string, data: MerchantBackend.Orders.RefundRequest): Promise<MerchantBackend.Orders.MerchantRefundResponse> => { + const res = await request(`${url}/private/orders/${orderId}/refund`, { + method: 'post', + token, + data + }) + + mutateAll(/@"\/private\/orders"@/) + return res + } + const forgetOrder = async (orderId: string, data: MerchantBackend.Orders.ForgetRequest): Promise<void> => { await request(`${url}/private/orders/${orderId}/forget`, { method: 'patch', @@ -253,9 +270,21 @@ export function useOrderMutateAPI(): OrderMutateAPI { mutateAll(/@"\/private\/orders"@/) } - return { createOrder, forgetOrder, deleteOrder } + return { createOrder, forgetOrder, deleteOrder, refundOrder } +} + +export function useOrderDetails(oderId:string): HttpResponse<MerchantBackend.Orders.MerchantOrderStatusResponse> { + const { url: baseUrl } = useBackendContext(); + const { token, id: instanceId, admin } = useInstanceContext(); + + const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}` + + const { data, error } = useSWR<MerchantBackend.Orders.MerchantOrderStatusResponse, SwrError>([`/private/orders/${oderId}`, token, url], fetcher) + + return { data, unauthorized: error?.status === 401, notfound: error?.status === 404, error } } + interface TransferMutateAPI { informTransfer: (data: MerchantBackend.Transfers.TransferInformation) => Promise<MerchantBackend.Transfers.MerchantTrackTransferResponse>; } @@ -411,26 +440,6 @@ export function useInstanceMutateAPI(): InstaceMutateAPI { return { updateInstance, deleteInstance, setNewToken, clearToken } } -export function useBackendInstancesTestForAdmin(): HttpResponse<MerchantBackend.Instances.InstancesResponse> { - const { url, token } = useBackendContext() - interface Result { - data?: MerchantBackend.Instances.InstancesResponse; - error?: SwrError; - } - const [result, setResult] = useState<Result | undefined>(undefined) - - useEffect(() => { - request(`${url}/private/instances`, { token }) - .then(data => setResult({ data })) - .catch(error => setResult({ error })) - }, [url, token]) - - const data = result?.data - const error = result?.error - - return { data, unauthorized: error?.status === 401, notfound: error?.status === 404, error } -} - export function useBackendInstances(): HttpResponse<MerchantBackend.Instances.InstancesResponse> { const { url, token } = useBackendContext() const { data, error } = useSWR<MerchantBackend.Instances.InstancesResponse, SwrError>(['/private/instances', token, url], fetcher) @@ -485,7 +494,7 @@ export function useInstanceOrders(args: InstanceOrderFilter, updateFilter: (d:Da const [pageAfter, setPageAfter] = useState(1) const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = pageBefore * PAGE_SIZE; + const totalBefore = args?.date ? pageBefore * PAGE_SIZE : 0; const { data:beforeData, error:beforeError } = useSWR<MerchantBackend.Orders.OrderHistory, SwrError>( [`/private/orders`, token, url, args?.paid, args?.refunded, args?.wired, args?.date, totalBefore], @@ -506,31 +515,34 @@ export function useInstanceOrders(args: InstanceOrderFilter, updateFilter: (d:Da // this has problems when there are some ids missing const isReachingEnd = afterData && afterData.orders.length < totalAfter; - const isReachingStart = beforeData && beforeData.orders.length < totalBefore; + const isReachingStart = (!args?.date) || (beforeData && beforeData.orders.length < totalBefore); + + const orders = !beforeData || !afterData ? undefined : (beforeData || lastBefore).orders.slice().reverse().concat((afterData || lastAfter).orders) + const unauthorized = beforeError?.status === 401 || afterError?.status === 401 + const notfound = beforeError?.status === 404 || afterError?.status === 404 const loadMore = () => { - if (totalAfter < MAX_RESULT_SIZE) { + if (!orders) return + if (orders.length < MAX_RESULT_SIZE) { setPageAfter(pageAfter + 1) - } else { - const from = afterData?.orders?.[PAGE_SIZE]?.timestamp?.t_ms + } else { + const from = afterData?.orders?.[afterData?.orders?.length-1]?.timestamp?.t_ms + // console.log(afterData?.orders?.map(d => d.row_id), PAGE_SIZE, from && format(new Date(from), 'yyyy/MM/dd HH:mm:ss')) if (from) updateFilter(new Date(from)) } } const loadMorePrev = () => { - if (totalBefore < MAX_RESULT_SIZE) { + if (!orders) return + if (orders.length < MAX_RESULT_SIZE) { setPageBefore(pageBefore + 1) } else { - const from = beforeData?.orders?.[PAGE_SIZE-1]?.timestamp?.t_ms + const from = beforeData?.orders?.[beforeData?.orders?.length-1]?.timestamp?.t_ms if (from) updateFilter(new Date(from)) } } - const orders = (beforeData || lastBefore || {orders:[]}).orders.slice().reverse().concat((afterData || lastAfter || {orders:[]}).orders) - const unauthorized = beforeError?.status === 401 || afterError?.status === 401 - const notfound = beforeError?.status === 404 || afterError?.status === 404 - - return { data: {orders}, loadMorePrev, loadMore, isReachingEnd, isReachingStart, unauthorized, notfound } + return { data: orders ? {orders} : undefined, loadMorePrev, loadMore, isReachingEnd, isReachingStart, unauthorized, notfound, error: beforeError ? beforeError : afterError } } export function useInstanceTips(): HttpResponse<MerchantBackend.Tips.TippingReserveStatus> { @@ -563,9 +575,45 @@ export function useInstanceTransfers(): HttpResponse<MerchantBackend.Transfers.T return { data, unauthorized: error?.status === 401, notfound: error?.status === 404, error } } + +export function useBackendInstancesTestForAdmin(): HttpResponse<MerchantBackend.Instances.InstancesResponse> { + const { url, token } = useBackendContext() + interface Result { + data?: MerchantBackend.Instances.InstancesResponse; + error?: SwrError; + } + const [result, setResult] = useState<Result | undefined>(undefined) + + useEffect(() => { + request(`${url}/private/instances`, { token }) + .then(data => setResult({ data })) + .catch(error => setResult({ error })) + }, [url, token]) + + const data = result?.data + const error = result?.error + + return { data, unauthorized: error?.status === 401, notfound: error?.status === 404, error } +} + + export function useBackendConfig(): HttpResponse<MerchantBackend.VersionResponse> { const { url, token } = useBackendContext() - const { data, error } = useSWR<MerchantBackend.VersionResponse, SwrError>(['/config', token, url], fetcher) + interface Result { + data?: MerchantBackend.VersionResponse; + error?: SwrError; + } + const [result, setResult] = useState<Result | undefined>(undefined) + + useEffect(() => { + request(`${url}/config`, { token }) + .then(data => setResult({ data })) + .catch(error => setResult({ error })) + }, [url, token]) + + const data = result?.data + const error = result?.error + return { data, unauthorized: error?.status === 401, notfound: error?.status === 404, error } } diff --git a/packages/frontend/src/messages/en.po b/packages/frontend/src/messages/en.po @@ -58,8 +58,8 @@ msgstr "Use this token to secure an instance with a password" msgid "fields.instance.payto_uris.label" msgstr "Account address" -# msgid "fields.instance.payto_uris.help" -# msgstr "x-taler-bank/bank.taler:5882/blogger" +msgid "fields.instance.payto_uris.help" +msgstr "x-taler-bank/bank.taler:5882/blogger" msgid "fields.instance.default_max_deposit_fee.label" msgstr "Max deposit fee label" @@ -283,3 +283,61 @@ msgstr "Creation succeed" msgid "create_error" msgstr "Creation failed" + +msgid "delete_instance" +msgstr "Delete instance" + +# msgid "fields.instance.refund.placeholder" +# msgstr "" + +# msgid "fields.instance.refund.tooltip" +# msgstr "" + +msgid "fields.instance.refund.label" +msgstr "Amount" + +msgid "fields.instance.mainReason.placeholder" +msgstr "select an option" + +# msgid "fields.instance.reason.tooltip" +# msgstr "" + +msgid "fields.instance.mainReason.label" +msgstr "Reason" + +msgid "fields.instance.description.label" +msgstr "Description" + +msgid "fields.instance.description.placeholder" +msgstr "add more information about the refund" + +# msgid "fields.instance.reason.tooltip" +# msgstr "" +msgid "fields.instance.order_status.placeholder" +msgstr "" + +# msgid "fields.instance.order_status.tooltip" +# msgstr "" + +msgid "fields.instance.order_status.label" +msgstr "Order status" + + +# msgid "fields.instance.order_status_url.placeholder" +# msgstr "" + +# msgid "fields.instance.order_status_url.tooltip" +# msgstr "" + +msgid "fields.instance.order_status_url.label" +msgstr "Order status URL" + +# msgid "fields.instance.taler_pay_uri.placeholder" +# msgstr "" + +# msgid "fields.instance.taler_pay_uri.tooltip" +# msgstr "" + +msgid "fields.instance.taler_pay_uri.label" +msgstr "Taler Pay URI" + diff --git a/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx @@ -0,0 +1,87 @@ +/* + 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, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { MerchantBackend } from "../../../../declaration"; +import { Input } from "../../../../components/form/Input"; +import { FormProvider } from "../../../../components/form/Field"; +import { NotificationCard } from "../../../../components/menu"; + +type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse; +interface Props { + onBack: () => void; + selected: Entity; + +} + +interface KeyValue { + [key: string]: string; +} + +type Paid = MerchantBackend.Orders.CheckPaymentPaidResponse +type Unpaid = MerchantBackend.Orders.CheckPaymentUnpaidResponse +type Claimed = MerchantBackend.Orders.CheckPaymentClaimedResponse + +export function DetailPage({ selected }: Props): VNode { + const [value, valueHandler] = useState<Partial<Entity>>(selected) + const [errors, setErrors] = useState<KeyValue>({}) + + + return <div> + <NotificationCard notification={{ + message: 'DEMO WARNING', + type:'WARN', + description: <Fragment> + <p>UNDER CONSTRUCTION: for now we are showing some field of the order depending on the state</p> + <p><b>unpaid:</b> status_url and pay_uri</p> + <p><b>claimed:</b> contractTerms.amount</p> + <p><b>paid:</b> deposit_total</p> + </Fragment> + }} /> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-6"> + <FormProvider<Entity> errors={errors} object={value} valueHandler={valueHandler} > + {selected.order_status === 'unpaid' && <Fragment> + <Input<Unpaid> name="order_status" readonly /> + <Input<Unpaid> name="order_status_url" readonly /> + <Input<Unpaid> name="taler_pay_uri" readonly /> + </Fragment>} + {selected.order_status === 'claimed' && <Fragment> + <Input<Claimed> name="order_status" readonly /> + <Input name="contract_terms.amount" readonly /> + </Fragment>} + {selected.order_status === 'paid' && <Fragment> + <Input<Paid> name="order_status" readonly /> + <Input name="contract_terms.deposit_total" readonly /> + </Fragment>} + </FormProvider> + </div> + <div class="column" /> + </div> + </section> + + </div> + +} +\ No newline at end of file diff --git a/packages/frontend/src/paths/instance/orders/details/index.tsx b/packages/frontend/src/paths/instance/orders/details/index.tsx @@ -0,0 +1,48 @@ +/* + 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/> + */ +import { Fragment, h, VNode } from "preact"; +import { Loading } from "../../../../components/exception/loading"; +import { SwrError, useOrderDetails, useOrderMutateAPI } from "../../../../hooks/backend"; + import { DetailPage } from "./DetailPage"; + +export interface Props { + oid: string; + + onBack: () => void; + onUnauthorized: () => VNode; + onNotFound: () => VNode; + onLoadError: (e: SwrError) => VNode; +} + +export default function Update({ oid, onBack, onLoadError, onNotFound, onUnauthorized }: Props): VNode { + const { refundOrder } = useOrderMutateAPI(); + const details = useOrderDetails(oid) + + if (details.unauthorized) return onUnauthorized() + if (details.notfound) return onNotFound(); + + if (!details.data) { + if (details.error) return onLoadError(details.error) + return <Loading /> + } + + return <Fragment> + <DetailPage + onBack={onBack} + selected={details.data} + /> + </Fragment> +} +\ No newline at end of file diff --git a/packages/frontend/src/paths/instance/orders/list/Table.tsx b/packages/frontend/src/paths/instance/orders/list/Table.tsx @@ -23,16 +23,23 @@ import { format } from "date-fns"; import { Fragment, h, VNode } from "preact" import { Message } from "preact-messages" import { StateUpdater, useCallback, useEffect, useRef, useState } from "preact/hooks" +import { FormErrors, FormProvider } from "../../../../components/form/Field"; +import { Input } from "../../../../components/form/Input"; +import { InputCurrency } from "../../../../components/form/InputCurrency"; +import { InputSelector } from "../../../../components/form/InputSelector"; +import { ConfirmModal } from "../../../../components/modal"; +import { useConfigContext } from "../../../../context/backend"; import { MerchantBackend, WidthId } from "../../../../declaration" +import { RefoundSchema } from "../../../../schemas"; +import { AMOUNT_REGEX } from "../../../../utils/constants"; import { Actions, buildActions } from "../../../../utils/table"; type Entity = MerchantBackend.Orders.OrderHistoryEntry & { id: string } interface Props { instances: Entity[]; - onUpdate: (id: string) => void; - onDelete: (id: Entity) => void; + onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void; onCreate: () => void; - selected?: boolean; + onSelect: (order: Entity) => void; onLoadMoreBefore?: () => void; hasMoreBefore?: boolean; hasMoreAfter?: boolean; @@ -42,23 +49,11 @@ interface Props { // onLoadMoreAfter={result.loadMore} hasMoreAfter={!result.isReachingEnd} -export function CardTable({ instances, onCreate, onUpdate, onDelete, selected, onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: Props): VNode { +export function CardTable({ instances, onCreate, onRefund, onSelect, onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: Props): VNode { const [actionQueue, actionQueueHandler] = useState<Actions<Entity>[]>([]); const [rowSelection, rowSelectionHandler] = useState<string[]>([]) - useEffect(() => { - if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'DELETE') { - onDelete(actionQueue[0].element) - actionQueueHandler(actionQueue.slice(1)) - } - }, [actionQueue, selected, onDelete]) - - useEffect(() => { - if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'UPDATE') { - onUpdate(actionQueue[0].element.id) - actionQueueHandler(actionQueue.slice(1)) - } - }, [actionQueue, selected, onUpdate]) + const [showRefund, setShowRefund] = useState<string | undefined>(undefined) return <div class="card has-table"> @@ -66,7 +61,7 @@ export function CardTable({ instances, onCreate, onUpdate, onDelete, selected, o <p class="card-header-title"><span class="icon"><i class="mdi mdi-cash-register" /></span><Message id="Orders" /></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> @@ -78,7 +73,7 @@ export function CardTable({ instances, onCreate, onUpdate, onDelete, selected, o <div class="b-table has-pagination"> <div class="table-wrapper has-mobile-cards"> {instances.length > 0 ? - <Table instances={instances} onUpdate={onUpdate} onDelete={onDelete} + <Table instances={instances} onSelect={onSelect} onRefund={(order) => setShowRefund(order.id)} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} onLoadMoreAfter={onLoadMoreAfter} onLoadMoreBefore={onLoadMoreBefore} hasMoreAfter={hasMoreAfter} hasMoreBefore={hasMoreBefore} @@ -88,15 +83,20 @@ export function CardTable({ instances, onCreate, onUpdate, onDelete, selected, o </div> </div> </div> + {showRefund && <RefundModal + onCancel={() => setShowRefund(undefined)} + onConfirm={(value) => { + onRefund(showRefund, value) + setShowRefund(undefined) + }} + />} </div> - - } interface TableProps { rowSelection: string[]; instances: Entity[]; - onUpdate: (id: string) => void; - onDelete: (id: Entity) => void; + onRefund: (id: Entity) => void; + onSelect: (id: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; onLoadMoreBefore?: () => void; hasMoreBefore?: boolean; @@ -104,13 +104,9 @@ interface TableProps { onLoadMoreAfter?: () => void; } -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] : prev.filter(e => e != id) -} - -function Table({ rowSelection, rowSelectionHandler, instances, onUpdate, onDelete, onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: TableProps): VNode { +function Table({ instances, onSelect, onRefund, onLoadMoreAfter, onLoadMoreBefore, hasMoreAfter, hasMoreBefore }: TableProps): VNode { return <div class="table-container"> - {onLoadMoreBefore && <button class="button is-fullwidth" disabled={!hasMoreBefore} onClick={onLoadMoreBefore}> load more before </button>} + {onLoadMoreBefore && <button class="button is-fullwidth" disabled={!hasMoreBefore} onClick={onLoadMoreBefore}> load more after </button>} <table class="table is-striped is-hoverable is-fullwidth"> <thead> <tr> @@ -121,23 +117,25 @@ function Table({ rowSelection, rowSelectionHandler, instances, onUpdate, onDelet </tr> </thead> <tbody> - {instances.map(i => { + {instances.map((i,pos) => { return <tr> - <td onClick={(): void => onUpdate(i.id)} style={{ cursor: 'pointer' }} >{format(new Date(i.timestamp.t_ms), 'dd/MM/yyyy HH:mm:ss')}</td> - <td onClick={(): void => onUpdate(i.id)} style={{ cursor: 'pointer' }} >{i.amount}</td> - <td onClick={(): void => onUpdate(i.id)} style={{ cursor: 'pointer' }} >{i.row_id}</td> + <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{format(new Date(i.timestamp.t_ms), 'yyyy/MM/dd HH:mm:ss')}</td> + <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.amount}</td> + <td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.summary}</td> <td class="is-actions-cell right-sticky"> <div class="buttons is-right"> - <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onDelete(i)}> - Refund - </button> + {(i.refundable || pos === 0) && + <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onRefund(i)}> + Refund + </button> + } </div> </td> </tr> })} </tbody> </table> - {onLoadMoreAfter && <button class="button is-fullwidth" disabled={!hasMoreAfter} onClick={onLoadMoreAfter}> load more after </button>} + {onLoadMoreAfter && <button class="button is-fullwidth" disabled={!hasMoreAfter} onClick={onLoadMoreAfter}> load more before </button>} </div> } @@ -150,4 +148,44 @@ function EmptyTable(): VNode { </div> } +interface RefundModalProps { + onCancel: () => void; + onConfirm: (value: MerchantBackend.Orders.RefundRequest) => void; +} + +export function RefundModal({ onCancel, onConfirm }: RefundModalProps): VNode { + const config = useConfigContext() + type State = { mainReason?: string, description?: string, refund?: string } + const [form, setValue] = useState<State>({}) + + const [errors, setErrors] = useState<FormErrors<State>>({}) + + const validateAndConfirm = () => { + try { + RefoundSchema.validateSync(form, { abortEarly: false }) + if (!form.refund) return; + onConfirm({ + refund: form.refund, + reason: `${form.mainReason}: ${form.description}` + }) + } catch (err) { + const errors = err.inner as any[] + const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message } }), {}) + setErrors(pathMessages) + } + } + return <ConfirmModal description="delete_instance" danger active onCancel={onCancel} onConfirm={validateAndConfirm}> + <div class="block"> + You are going to refund the order + </div> + <FormProvider<State> errors={errors} object={form} valueHandler={(d) => setValue(d as any)}> + <InputCurrency<State> name="refund" currency={config.currency} /> + <InputSelector name="mainReason" values={['duplicated', 'requested by the customer', 'other']} /> + {form.mainReason && <Input<State> name="description" />} + </FormProvider> + <div class="block"> + You are going to refund the order + </div> + </ConfirmModal> +} diff --git a/packages/frontend/src/paths/instance/orders/list/index.tsx b/packages/frontend/src/paths/instance/orders/list/index.tsx @@ -27,31 +27,36 @@ import { InstanceOrderFilter, SwrError, useInstanceOrders, useOrderMutateAPI, us import { CardTable } from './Table'; import { format } from 'date-fns'; import { DatePicker } from '../../../../components/form/DatePicker'; +import { NotificationCard } from '../../../../components/menu'; +import { Notification } from '../../../../utils/types'; interface Props { onUnauthorized: () => VNode; onLoadError: (e: SwrError) => VNode; onNotFound: () => VNode; + onSelect: (id: string) => void; onCreate: () => void; } -export default function ({ onUnauthorized, onLoadError, onCreate, onNotFound }: Props): VNode { +export default function ({ onUnauthorized, onLoadError, onCreate, onSelect, onNotFound }: Props): VNode { const [filter, setFilter] = useState<InstanceOrderFilter>({paid:'yes'}) const [pickDate, setPickDate] = useState(false) const setNewDate = (date:Date) => setFilter(prev => ({...prev,date})) + const result = useInstanceOrders(filter, setNewDate) - const { createOrder, deleteOrder } = useOrderMutateAPI() + const { createOrder, refundOrder } = useOrderMutateAPI() const { currency } = useConfigContext() let instances: (MerchantBackend.Orders.OrderHistoryEntry & { id: string })[]; + const [notif, setNotif] = useState<Notification | undefined>(undefined) + if (result.unauthorized) return onUnauthorized() if (result.notfound) return onNotFound() if (!result.data) { if (result.error) return onLoadError(result.error) - //if loading assume empty list instances = [] } else { instances = result.data.orders.map(o => ({ ...o, id: o.order_id })) @@ -66,6 +71,12 @@ export default function ({ onUnauthorized, onLoadError, onCreate, onNotFound }: const isAllActive = filter.paid === undefined && filter.refunded === undefined && filter.wired === undefined ? 'is-active' : '' return <section class="section is-main-section"> + <NotificationCard notification={{ + message: 'DEMO WARNING', + type:'WARN', + description: 'refund button is being forced in the first row, other depends on the refundable property' + }} /> + <NotificationCard notification={notif} /> <DatePicker opened={pickDate} @@ -112,8 +123,17 @@ export default function ({ onUnauthorized, onLoadError, onCreate, onNotFound }: summary: `some summary with a random number ${Math.floor(Math.random() * 20 + 1)}`, } })} - onDelete={(order: MerchantBackend.Orders.OrderHistoryEntry) => deleteOrder(order.order_id)} - onUpdate={() => null} + onSelect={(order) => onSelect(order.id)} + onRefund={(id, value) => refundOrder(id, value) + .then(() => setNotif({ + message: 'refund created successfully', + type: "SUCCESS" + })).catch((error) => setNotif({ + message: 'could not create the refund', + type: "ERROR", + description: error.message + })) + } onLoadMoreBefore={result.loadMorePrev} hasMoreBefore={!result.isReachingStart} onLoadMoreAfter={result.loadMore} hasMoreAfter={!result.isReachingEnd} /> diff --git a/packages/frontend/src/paths/instance/orders/update/index.tsx b/packages/frontend/src/paths/instance/orders/update/index.tsx @@ -1,26 +0,0 @@ -/* - 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 { h, VNode } from 'preact'; - -export default function ():VNode { - return <div>order update page</div> -} -\ No newline at end of file diff --git a/packages/frontend/src/paths/instance/update/UpdatePage.tsx b/packages/frontend/src/paths/instance/update/UpdatePage.tsx @@ -74,8 +74,12 @@ export function UpdatePage({ onUpdate, isLoading, selected, onBack }: Props): VN // use conversion instead of this const newToken = value.auth_token; value.auth_token = undefined; + + //if new token was not set or has been set to the actual current token + //it is not needed to send a change + //otherwise, checked where we are setting a new token or removing it const auth: MerchantBackend.Instances.InstanceAuthConfigurationMessage | undefined = - newToken === currentTokenValue ? undefined : (newToken === null ? + newToken === undefined || newToken === currentTokenValue ? undefined : (newToken === null ? { method: "external" } : { method: "token", token: `secret-token:${newToken}` }); diff --git a/packages/frontend/src/schemas/index.ts b/packages/frontend/src/schemas/index.ts @@ -58,7 +58,7 @@ export const InstanceSchema = yup.object().shape({ .test('payto', '{path} is not valid', listOfPayToUrisAreValid), default_max_deposit_fee: yup.string() .required() - .test('amount', '{path} is not valid', currencyWithAmountIsValid) + .test('amount', 'the amount is not valid', currencyWithAmountIsValid) .meta({type: 'amount'}), default_max_wire_fee: yup.string() .required() @@ -105,3 +105,10 @@ export const InstanceSchema = yup.object().shape({ export const InstanceUpdateSchema = InstanceSchema.clone().omit(['id']); export const InstanceCreateSchema = InstanceSchema.clone(); +export const RefoundSchema = yup.object().shape({ + mainReason: yup.string().required(), + description: yup.string().required(), + refund: yup.string() + .required() + .test('amount', 'the amount is not valid', currencyWithAmountIsValid), +}) +\ No newline at end of file diff --git a/packages/frontend/src/scss/_custom-calendar.scss b/packages/frontend/src/scss/_custom-calendar.scss @@ -1,11 +1,5 @@ :root { - --primary-color: #673ab7; - --primary-color-light: #9a67ea; - --primary-color-dark: #320b86; - - --secondary-color: #ffc400; - --secondary-color-light: #fff64f; - --secondary-color-dark: #c79400; + --primary-color: #3298dc; --primary-text-color-dark: rgba(0,0,0,.87); --secondary-text-color-dark: rgba(0,0,0,.57); diff --git a/packages/frontend/src/scss/_theme-default.scss b/packages/frontend/src/scss/_theme-default.scss @@ -102,7 +102,7 @@ $modal-card-head-border-bottom: 1px solid $white-ter; $modal-card-foot-border-top: 0; /* Modal card: specifics */ -$modal-card-width: 40vw; +$modal-card-width: 80vw; $modal-card-width-mobile: 90vw; $modal-card-foot-background-color: $white-ter; diff --git a/packages/frontend/src/utils/constants.ts b/packages/frontend/src/utils/constants.ts @@ -27,7 +27,7 @@ export const AMOUNT_REGEX=/^[a-zA-Z][a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/ export const INSTANCE_ID_LOOKUP = /^\/instances\/([^/]*)\/?$/ // how much rows we add every time user hit load more -export const PAGE_SIZE = 10 +export const PAGE_SIZE = 20 // how bigger can be the result set // after this threshold, load more with move the cursor -export const MAX_RESULT_SIZE = 19; -\ No newline at end of file +export const MAX_RESULT_SIZE = 39; +\ No newline at end of file diff --git a/packages/frontend/src/utils/types.ts b/packages/frontend/src/utils/types.ts @@ -14,13 +14,15 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { VNode } from "preact" + export interface KeyValue { [key: string]: string; } export interface Notification { message: string; - description?: string; + description?: string | VNode; type: MessageType; }