diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src')
25 files changed, 255 insertions, 231 deletions
diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx index f0a7de53b..4210192ae 100644 --- a/packages/merchant-backoffice-ui/src/Application.tsx +++ b/packages/merchant-backoffice-ui/src/Application.tsx @@ -64,7 +64,7 @@ function ApplicationStatusRoutes(): VNode { const result = useBackendConfig(); const { i18n } = useTranslationContext(); - const { currency, version } = result.ok + const { currency, version } = result.ok && result.data ? result.data : { currency: "unknown", version: "unknown" }; const ctx = useMemo(() => ({ currency, version }), [currency, version]); diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx index f5372db8d..95be49c9d 100644 --- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx +++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx @@ -80,7 +80,7 @@ import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js"; export enum InstancePaths { error = "/error", - server = "/server", + settings = "/settings", token = "/token", bank_list = "/bank", @@ -118,7 +118,7 @@ export enum InstancePaths { validators_update = "/validators/:vid/update", validators_new = "/validators/new", - settings = "/interface", + interface = "/interface", } // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -255,7 +255,7 @@ export function InstanceRoutes({ instance={id} admin={admin} onShowSettings={() => { - route(InstancePaths.settings) + route(InstancePaths.interface) }} path={path} onLogout={clearTokenAndGoToRoot} @@ -320,7 +320,7 @@ export function InstanceRoutes({ * Update instance page */} <Route - path={InstancePaths.server} + path={InstancePaths.settings} component={InstanceUpdatePage} onBack={() => { route(`/`); @@ -353,7 +353,7 @@ export function InstanceRoutes({ path={InstancePaths.inventory_list} component={ProductListPage} onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.server)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} onCreate={() => { route(InstancePaths.inventory_new); }} @@ -392,7 +392,7 @@ export function InstanceRoutes({ path={InstancePaths.bank_list} component={BankAccountListPage} onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.server)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} onCreate={() => { route(InstancePaths.bank_new); }} @@ -437,7 +437,7 @@ export function InstanceRoutes({ route(InstancePaths.order_details.replace(":oid", id)); }} onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.server)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> <Route @@ -468,7 +468,7 @@ export function InstanceRoutes({ component={TransferListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.server)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} onCreate={() => { route(InstancePaths.transfers_new); }} @@ -491,7 +491,7 @@ export function InstanceRoutes({ component={WebhookListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.server)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} onCreate={() => { route(InstancePaths.webhooks_new); }} @@ -530,7 +530,7 @@ export function InstanceRoutes({ component={ValidatorListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.server)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} onCreate={() => { route(InstancePaths.validators_new); }} @@ -569,7 +569,7 @@ export function InstanceRoutes({ component={TemplateListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.server)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} onCreate={() => { route(InstancePaths.templates_new); }} @@ -638,7 +638,7 @@ export function InstanceRoutes({ component={ReservesListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.server)} + onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} onSelect={(id: string) => { route(InstancePaths.reserves_details.replace(":rid", id)); }} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx b/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx index 34feec202..d7b490a5d 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx @@ -107,8 +107,9 @@ export function InputWithAddon<T>({ {error && <p class="help is-danger">{error}</p>} <span class="has-text-grey">{help}</span> </div> - {side} + {expand ? <div>{side}</div> : side} </div> + </div> ); } diff --git a/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx new file mode 100644 index 000000000..2ff23dfd3 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx @@ -0,0 +1,59 @@ +import { TranslatedString } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; + +export function JumpToElementById({ testIfExist, onSelect, palceholder, description }: { palceholder: TranslatedString, description: TranslatedString, testIfExist: (id: string) => Promise<any>, onSelect: (id: string) => void }): VNode { + const { i18n } = useTranslationContext() + + const [error, setError] = useState<string | undefined>( + undefined, + ); + + const [id, setId] = useState<string>() + async function check(currentId: string | undefined): Promise<void> { + if (!currentId) { + setError(i18n.str`missing id`); + return; + } + try { + await testIfExist(currentId); + onSelect(currentId); + setError(undefined); + } catch { + setError(i18n.str`not found`); + } + } + + return <div class="level"> + <div class="level-left"> + <div class="level-item"> + <div class="field has-addons"> + <div class="control"> + <input + class={error ? "input is-danger" : "input"} + type="text" + value={id ?? ""} + onChange={(e) => setId(e.currentTarget.value)} + placeholder={palceholder} + /> + {error && <p class="help is-danger">{error}</p>} + </div> + <span + class="has-tooltip-bottom" + data-tooltip={description} + > + <button + class="button" + onClick={(e) => check(id)} + > + <span class="icon"> + <i class="mdi mdi-arrow-right" /> + </span> + </button> + </span> + </div> + </div> + </div> + </div> +} diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx index 402134096..6905cb4d0 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx @@ -177,12 +177,12 @@ export function Sidebar({ </a> </li> <li> - <a href={"/server"} class="has-icon"> + <a href={"/settings"} class="has-icon"> <span class="icon"> <i class="mdi mdi-square-edit-outline" /> </span> <span class="menu-item-label"> - <i18n.Translate>Server</i18n.Translate> + <i18n.Translate>Settings</i18n.Translate> </span> </a> </li> diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx index b8ac2c9ab..0e881d7a5 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/index.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/index.tsx @@ -24,7 +24,7 @@ import { Sidebar } from "./SideBar.js"; function getInstanceTitle(path: string, id: string): string { switch (path) { - case InstancePaths.server: + case InstancePaths.settings: return `${id}: Settings`; case InstancePaths.order_list: return `${id}: Orders`; @@ -64,9 +64,7 @@ function getInstanceTitle(path: string, id: string): string { return `${id}: Templates`; case InstancePaths.templates_use: return `${id}: Use template`; - case InstancePaths.settings: - return `${id}: Interface`; - case InstancePaths.settings: + case InstancePaths.interface: return `${id}: Interface`; default: return ""; diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts index eaeede103..4515f0557 100644 --- a/packages/merchant-backoffice-ui/src/hooks/backend.ts +++ b/packages/merchant-backoffice-ui/src/hooks/backend.ts @@ -91,7 +91,7 @@ const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000; const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000; export function useBackendConfig(): HttpResponse< - MerchantBackend.VersionResponse, + MerchantBackend.VersionResponse | undefined, RequestError<MerchantBackend.ErrorDetail> > { const { request } = useBackendBaseRequest(); @@ -340,6 +340,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { if (refunded !== undefined) params.refunded = refunded; if (wired !== undefined) params.wired = wired; if (date_s !== undefined) params.date_s = date_s; + if (delta === 0) { + //in this case we can already assume the response + //and avoid network + return Promise.resolve({ + ok: true, + data: {orders:[]} as T, + }) + } return requestHandler<T>(baseUrl, endpoint, { params, token }); }, [baseUrl, token], @@ -385,6 +393,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { const params: any = {}; if (payto_uri !== undefined) params.payto_uri = payto_uri; if (verified !== undefined) params.verified = verified; + if (delta === 0) { + //in this case we can already assume the response + //and avoid network + return Promise.resolve({ + ok: true, + data: {transfers:[]} as T, + }) + } if (delta !== undefined) { params.limit = delta; } @@ -403,6 +419,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { ): Promise<HttpResponseOk<T>> { const [endpoint, position, delta] = args const params: any = {}; + if (delta === 0) { + //in this case we can already assume the response + //and avoid network + return Promise.resolve({ + ok: true, + data: {templates:[]} as T, + }) + } if (delta !== undefined) { params.limit = delta; } @@ -421,6 +445,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { ): Promise<HttpResponseOk<T>> { const [endpoint, position, delta] = args const params: any = {}; + if (delta === 0) { + //in this case we can already assume the response + //and avoid network + return Promise.resolve({ + ok: true, + data: {webhooks:[]} as T, + }) + } if (delta !== undefined) { params.limit = delta; } diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts index 498e4eb78..3c8ef15ed 100644 --- a/packages/merchant-backoffice-ui/src/hooks/index.ts +++ b/packages/merchant-backoffice-ui/src/hooks/index.ts @@ -32,7 +32,13 @@ const calculateRootPath = () => { ? window.location.origin + window.location.pathname : "/"; - return rootPath.replace("webui/",""); + /** + * By default, merchant backend serves the html content + * from the /webui root. This should cover most of the + * cases and the rootPath will be the merchant backend + * URL where the instances are + */ + return rootPath.replace("/webui/", ""); }; const loginTokenCodec = buildCodecForObject<LoginToken>() diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts b/packages/merchant-backoffice-ui/src/hooks/templates.ts index 56cdd3046..ee8728cc8 100644 --- a/packages/merchant-backoffice-ui/src/hooks/templates.ts +++ b/packages/merchant-backoffice-ui/src/hooks/templates.ts @@ -82,10 +82,19 @@ export function useTemplateAPI(): TemplateAPI { return res; }; + const testTemplateExist = async ( + templateId: string, + ): Promise<HttpResponseOk<void>> => { + const res = await request<void>(`/private/templates/${templateId}`, { method: "GET", }); + return res; + }; + + return { createTemplate, updateTemplate, deleteTemplate, + testTemplateExist, createOrderFromTemplate, }; } @@ -98,6 +107,9 @@ export interface TemplateAPI { id: string, data: MerchantBackend.Template.TemplatePatchDetails, ) => Promise<HttpResponseOk<void>>; + testTemplateExist: ( + id: string + ) => Promise<HttpResponseOk<void>>; deleteTemplate: (id: string) => Promise<HttpResponseOk<void>>; createOrderFromTemplate: ( id: string, @@ -119,11 +131,11 @@ export function useInstanceTemplates( > { const { templateFetcher } = useBackendInstanceRequest(); - // const [pageBefore, setPageBefore] = useState(1); + const [pageBefore, setPageBefore] = useState(1); const [pageAfter, setPageAfter] = useState(1); const totalAfter = pageAfter * PAGE_SIZE; - // const totalBefore = args?.position !== undefined ? pageBefore * PAGE_SIZE : 0; + const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0; /** * FIXME: this can be cleaned up a little @@ -131,20 +143,20 @@ export function useInstanceTemplates( * the logic of double query should be inside the orderFetch so from the hook perspective and cache * is just one query and one error status */ - // const { - // data: beforeData, - // error: beforeError, - // isValidating: loadingBefore, - // } = useSWR<HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>, HttpError>( - // [ - // `/private/templates`, - // token, - // url, - // args?.position, - // totalBefore, - // ], - // templateFetcher, - // ); + const { + data: beforeData, + error: beforeError, + isValidating: loadingBefore, + } = useSWR< + HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>, + RequestError<MerchantBackend.ErrorDetail>>( + [ + `/private/templates`, + args?.position, + totalBefore, + ], + templateFetcher, + ); const { data: afterData, error: afterError, @@ -155,9 +167,13 @@ export function useInstanceTemplates( >([`/private/templates`, args?.position, -totalAfter], templateFetcher); //this will save last result - // const [lastBefore, setLastBefore] = useState< - // HttpResponse<MerchantBackend.Template.TemplateSummaryResponse, MerchantBackend.ErrorDetail> - // >({ loading: true }); + const [lastBefore, setLastBefore] = useState< + HttpResponse< + MerchantBackend.Template.TemplateSummaryResponse, + MerchantBackend.ErrorDetail + > + >({ loading: true }); + const [lastAfter, setLastAfter] = useState< HttpResponse< MerchantBackend.Template.TemplateSummaryResponse, @@ -166,19 +182,18 @@ export function useInstanceTemplates( >({ loading: true }); useEffect(() => { if (afterData) setLastAfter(afterData); - // if (beforeData) setLastBefore(beforeData); - }, [afterData /*, beforeData*/]); + if (beforeData) setLastBefore(beforeData); + }, [afterData, beforeData]); - // if (beforeError) return beforeError; + if (beforeError) return beforeError.cause; if (afterError) return afterError.cause; // if the query returns less that we ask, then we have reach the end or beginning const isReachingEnd = afterData && afterData.data.templates.length < totalAfter; - const isReachingStart = false; - // args?.position === undefined - // || - // (beforeData && beforeData.data.templates.length < totalBefore); + const isReachingStart = args?.position === undefined + || + (beforeData && beforeData.data.templates.length < totalBefore); const pagination = { isReachingEnd, @@ -188,37 +203,36 @@ export function useInstanceTemplates( if (afterData.data.templates.length < MAX_RESULT_SIZE) { setPageAfter(pageAfter + 1); } else { - const from = `${ - afterData.data.templates[afterData.data.templates.length - 1] - .template_id - }`; + const from = `${afterData.data.templates[afterData.data.templates.length - 1] + .template_id + }`; if (from && updatePosition) updatePosition(from); } }, loadMorePrev: () => { - // if (!beforeData || isReachingStart) return; - // if (beforeData.data.templates.length < MAX_RESULT_SIZE) { - // setPageBefore(pageBefore + 1); - // } else if (beforeData) { - // const from = `${beforeData.data.templates[beforeData.data.templates.length - 1] - // .template_id - // }`; - // if (from && updatePosition) updatePosition(from); - // } + if (!beforeData || isReachingStart) return; + if (beforeData.data.templates.length < MAX_RESULT_SIZE) { + setPageBefore(pageBefore + 1); + } else if (beforeData) { + const from = `${beforeData.data.templates[beforeData.data.templates.length - 1] + .template_id + }`; + if (from && updatePosition) updatePosition(from); + } }, }; - const templates = !afterData ? [] : (afterData || lastAfter).data.templates; - // const templates = - // !beforeData || !afterData - // ? [] - // : (beforeData || lastBefore).data.templates - // .slice() - // .reverse() - // .concat((afterData || lastAfter).data.templates); - if (loadingAfter /* || loadingBefore */) + // const templates = !afterData ? [] : (afterData || lastAfter).data.templates; + const templates = + !beforeData || !afterData + ? [] + : (beforeData || lastBefore).data.templates + .slice() + .reverse() + .concat((afterData || lastAfter).data.templates); + if (loadingAfter || loadingBefore) return { loading: true, data: { templates } }; - if (/*beforeData &&*/ afterData) { + if (beforeData && afterData) { return { ok: true, data: { templates }, ...pagination }; } return { loading: true }; diff --git a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts index c91fff8b4..ad6bf96e2 100644 --- a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts +++ b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts @@ -93,9 +93,11 @@ export function useInstanceWebhooks( > { const { webhookFetcher } = useBackendInstanceRequest(); + const [pageBefore, setPageBefore] = useState(1); const [pageAfter, setPageAfter] = useState(1); const totalAfter = pageAfter * PAGE_SIZE; + const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0; const { data: afterData, @@ -120,7 +122,7 @@ export function useInstanceWebhooks( const isReachingEnd = afterData && afterData.data.webhooks.length < totalAfter; - const isReachingStart = false; + const isReachingStart = true; const pagination = { isReachingEnd, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx index c29a6fa6e..9f80719a1 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx @@ -21,7 +21,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; -import { h, VNode } from "preact"; +import { h, VNode, Fragment } from "preact"; import { useState } from "preact/hooks"; import { DatePicker } from "../../../../components/picker/DatePicker.js"; import { MerchantBackend, WithId } from "../../../../declaration.js"; @@ -29,8 +29,6 @@ import { CardTable } from "./Table.js"; import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; export interface ListPageProps { - errorOrderId: string | undefined; - onShowAll: () => void; onShowNotPaid: () => void; onShowPaid: () => void; @@ -56,17 +54,18 @@ export interface ListPageProps { onSelectOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void; onRefundOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void; - onSearchOrderById: (id: string) => void; onCreate: () => void; } export function ListPage({ + hasMoreAfter, + hasMoreBefore, + onLoadMoreAfter, + onLoadMoreBefore, orders, - errorOrderId, isAllActive, onSelectOrder, onRefundOrder, - onSearchOrderById, jumpToDate, onCopyURL, onShowAll, @@ -86,42 +85,10 @@ export function ListPage({ const { i18n } = useTranslationContext(); const dateTooltip = i18n.str`select date to show nearby orders`; const [pickDate, setPickDate] = useState(false); - const [orderId, setOrderId] = useState<string>(""); const [settings] = useSettings(); return ( - <section class="section is-main-section"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <div class="field has-addons"> - <div class="control"> - <input - class={errorOrderId ? "input is-danger" : "input"} - type="text" - value={orderId} - onChange={(e) => setOrderId(e.currentTarget.value)} - placeholder={i18n.str`order id`} - /> - {errorOrderId && <p class="help is-danger">{errorOrderId}</p>} - </div> - <span - class="has-tooltip-bottom" - data-tooltip={i18n.str`jump to order with the given order ID`} - > - <button - class="button" - onClick={(e) => onSearchOrderById(orderId)} - > - <span class="icon"> - <i class="mdi mdi-arrow-right" /> - </span> - </button> - </span> - </div> - </div> - </div> - </div> + <Fragment> <div class="columns"> <div class="column is-two-thirds"> <div class="tabs" style={{ overflow: "inherit" }}> @@ -249,7 +216,11 @@ export function ListPage({ onCopyURL={onCopyURL} onSelect={onSelectOrder} onRefund={onRefundOrder} + hasMoreAfter={hasMoreAfter} + hasMoreBefore={hasMoreBefore} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} /> - </section> + </Fragment> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx index 608c9b20d..b2806bb79 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx @@ -140,10 +140,9 @@ function Table({ const [settings] = useSettings(); return ( <div class="table-container"> - {onLoadMoreBefore && ( + {hasMoreBefore && ( <button class="button is-fullwidth" - disabled={!hasMoreBefore} onClick={onLoadMoreBefore} > <i18n.Translate>load newer orders</i18n.Translate> @@ -218,10 +217,9 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( + {hasMoreAfter && ( <button class="button is-fullwidth" - disabled={!hasMoreAfter} onClick={onLoadMoreAfter} > <i18n.Translate>load older orders</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx index 48f77e3d3..34c7d348a 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx @@ -39,6 +39,7 @@ import { Notification } from "../../../../utils/types.js"; import { ListPage } from "./ListPage.js"; import { RefundModal } from "./Table.js"; import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; interface Props { onUnauthorized: () => VNode; @@ -69,9 +70,6 @@ export default function OrderList({ const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - const [errorOrderId, setErrorOrderId] = useState<string | undefined>( - undefined, - ); if (result.loading) return <Loading />; if (!result.ok) { @@ -100,24 +98,17 @@ export default function OrderList({ ? "is-active" : ""; - async function testIfOrderExistAndSelect(orderId: string): Promise<void> { - if (!orderId) { - setErrorOrderId(i18n.str`Enter an order id`); - return; - } - try { - await getPaymentURL(orderId); - onSelect(orderId); - setErrorOrderId(undefined); - } catch { - setErrorOrderId(i18n.str`order not found`); - } - } - return ( - <Fragment> + <section class="section is-main-section"> <NotificationCard notification={notif} /> + <JumpToElementById + testIfExist={getPaymentURL} + onSelect={onSelect} + description={i18n.str`jump to order with the given product ID`} + palceholder={i18n.str`order id`} + /> + <ListPage orders={result.data.orders.map((o) => ({ ...o, id: o.order_id }))} onLoadMoreBefore={result.loadMorePrev} @@ -126,7 +117,6 @@ export default function OrderList({ hasMoreAfter={!result.isReachingEnd} onSelectOrder={(order) => onSelect(order.id)} onRefundOrder={(value) => setOrderToBeRefunded(value)} - errorOrderId={errorOrderId} isAllActive={isAllActive} isNotWiredActive={isNotWiredActive} isWiredActive={isWiredActive} @@ -138,7 +128,6 @@ export default function OrderList({ getPaymentURL(id).then((resp) => copyToClipboard(resp.data)) } onCreate={onCreate} - onSearchOrderById={testIfOrderExistAndSelect} onSelectDate={setNewDate} onShowAll={() => setFilter({})} onShowNotPaid={() => setFilter({ paid: "no" })} @@ -190,7 +179,7 @@ export default function OrderList({ }} /> )} - </Fragment> + </section> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx index db73217ed..275f855cb 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx @@ -244,7 +244,10 @@ function Table({ } style={{ cursor: "pointer" }} > + <span style={{"whiteSpace":"nowrap"}}> + {i.total_sold} {i.unit} + </span> </td> <td class="is-actions-cell right-sticky"> <div class="buttons is-right"> @@ -341,7 +344,7 @@ function FastProductWithInfiniteStockUpdateForm({ <div class="buttons mt-5"> - <button class="button " onClick={onCancel}> + <button class="button mt-5" onClick={onCancel}> <i18n.Translate>Clone</i18n.Translate> </button> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx index 274a7c2ea..942b5d0ac 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx @@ -37,6 +37,7 @@ import { Notification } from "../../../../utils/types.js"; import { CardTable } from "./Table.js"; import { HttpStatusCode } from "@gnu-taler/taler-util"; import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js"; +import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; interface Props { onUnauthorized: () => VNode; @@ -74,60 +75,17 @@ export default function ProductList({ return onNotFound(); return onLoadError(result); } - const [errorId, setErrorId] = useState<string | undefined>( - undefined, - ); - - const [productId, setProductId] = useState<string>() - async function testIfProductExistAndSelect(orderId: string | undefined): Promise<void> { - if (!orderId) { - setErrorId(i18n.str`Enter a product id`); - return; - } - try { - await getProduct(orderId); - onSelect(orderId); - setErrorId(undefined); - } catch { - setErrorId(i18n.str`product not found`); - } - } return ( <section class="section is-main-section"> <NotificationCard notification={notif} /> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <div class="field has-addons"> - <div class="control"> - <input - class={errorId ? "input is-danger" : "input"} - type="text" - value={productId ?? ""} - onChange={(e) => setProductId(e.currentTarget.value)} - placeholder={i18n.str`product id`} - /> - {errorId && <p class="help is-danger">{errorId}</p>} - </div> - <span - class="has-tooltip-bottom" - data-tooltip={i18n.str`jump to product with the given product ID`} - > - <button - class="button" - onClick={(e) => testIfProductExistAndSelect(productId)} - > - <span class="icon"> - <i class="mdi mdi-arrow-right" /> - </span> - </button> - </span> - </div> - </div> - </div> - </div> + <JumpToElementById + testIfExist={getProduct} + onSelect={onSelect} + description={i18n.str`jump to product with the given product ID`} + palceholder={i18n.str`product id`} + /> <CardTable instances={result.data} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx index ab1d5ef91..bf6062c34 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx @@ -49,7 +49,6 @@ export function ListPage({ const { i18n } = useTranslationContext(); return ( - <section class="section is-main-section"> <CardTable templates={templates.map((o) => ({ ...o, @@ -65,6 +64,5 @@ export function ListPage({ onLoadMoreAfter={onLoadMoreAfter} hasMoreAfter={!onLoadMoreAfter} /> - </section> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx index 3bea9abe8..9fdf4ead9 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx @@ -136,11 +136,10 @@ function Table({ const { i18n } = useTranslationContext(); return ( <div class="table-container"> - {onLoadMoreBefore && ( + {hasMoreBefore && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more templates before the first one`} - disabled={!hasMoreBefore} onClick={onLoadMoreBefore} > <i18n.Translate>load newer templates</i18n.Translate> @@ -188,7 +187,7 @@ function Table({ data-tooltip={i18n.str`use template to create new order`} onClick={() => onNewOrder(i)} > - New order + Use template </button> <button class="button is-info is-small has-tooltip-left" @@ -204,11 +203,10 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( + {hasMoreAfter && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more templates after the last one`} - disabled={!hasMoreAfter} onClick={onLoadMoreAfter} > <i18n.Translate>load older templates</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx index 3c9bb231c..b9767442f 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx @@ -35,8 +35,9 @@ import { } from "../../../../hooks/templates.js"; import { Notification } from "../../../../utils/types.js"; import { ListPage } from "./ListPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; import { ConfirmModal } from "../../../../components/modal/index.js"; +import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; interface Props { onUnauthorized: () => VNode; @@ -60,7 +61,7 @@ export default function ListTemplates({ const [position, setPosition] = useState<string | undefined>(undefined); const { i18n } = useTranslationContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { deleteTemplate } = useTemplateAPI(); + const { deleteTemplate, testTemplateExist } = useTemplateAPI(); const result = useInstanceTemplates({ position }, (id) => setPosition(id)); const [deleting, setDeleting] = useState<MerchantBackend.Template.TemplateEntry | null>(null); @@ -81,9 +82,16 @@ export default function ListTemplates({ } return ( - <Fragment> + <section class="section is-main-section"> <NotificationCard notification={notif} /> + <JumpToElementById + testIfExist={testTemplateExist} + onSelect={onSelect} + description={i18n.str`jump to template with the given template ID`} + palceholder={i18n.str`template id`} + /> + <ListPage templates={result.data.templates} onLoadMoreBefore={ @@ -139,6 +147,6 @@ export default function ListTemplates({ </p> </ConfirmModal> )} - </Fragment> + </section> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx index db2533ecc..02b12c4c2 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx @@ -73,7 +73,7 @@ export function ListPage({ > <InputSelector name="payto_uri" - label={i18n.str`Address`} + label={i18n.str`Account URI`} values={accounts} placeholder={i18n.str`Select one account`} tooltip={i18n.str`filter by account address`} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx index 1c464cbc7..b6b1cf328 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx @@ -125,11 +125,10 @@ function Table({ const [settings] = useSettings(); return ( <div class="table-container"> - {onLoadMoreBefore && ( + {hasMoreBefore && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more transfers before the first one`} - disabled={!hasMoreBefore} onClick={onLoadMoreBefore} > <i18n.Translate>load newer transfers</i18n.Translate> @@ -198,11 +197,10 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( + {hasMoreAfter && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more transfer after the last one`} - disabled={!hasMoreAfter} onClick={onLoadMoreAfter} > <i18n.Translate>load older transfers</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx index 1bc1673ba..0fdbb9bc3 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx @@ -21,7 +21,7 @@ import { ErrorType, HttpError } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import { Loading } from "../../../../components/exception/loading.js"; import { MerchantBackend } from "../../../../declaration.js"; import { useInstanceDetails } from "../../../../hooks/instance.js"; @@ -47,7 +47,6 @@ export default function ListTransfer({ onCreate, onNotFound, }: Props): VNode { - const [form, setForm] = useState<Form>({ payto_uri: "" }); const setFilter = (s?: "yes" | "no") => setForm({ ...form, verified: s }); const [position, setPosition] = useState<string | undefined>(undefined); @@ -56,6 +55,14 @@ export default function ListTransfer({ const accounts = !instance.ok ? [] : instance.data.accounts.map((a) => a.payto_uri); + const [form, setForm] = useState<Form>({ payto_uri: "" }); + + const shoulUseDefaultAccount = accounts.length === 1 + useEffect(() => { + if (shoulUseDefaultAccount) { + setForm({...form, payto_uri: accounts[0]}) + } + }, [shoulUseDefaultAccount]) const isVerifiedTransfers = form.verified === "yes"; const isNonVerifiedTransfers = form.verified === "no"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx index cebc1ade6..4e813d777 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx @@ -118,6 +118,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { {state.otp_algorithm && state.otp_algorithm > 0 ? ( <Fragment> <InputWithAddon<Entity> + expand name="otp_key" label={i18n.str`Device key`} inputType={showKey ? "text" : "password"} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx index b639a6134..0c28027fe 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx @@ -126,11 +126,10 @@ function Table({ const { i18n } = useTranslationContext(); return ( <div class="table-container"> - {onLoadMoreBefore && ( + {hasMoreBefore && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more devices before the first one`} - disabled={!hasMoreBefore} onClick={onLoadMoreBefore} > <i18n.Translate>load newer devices</i18n.Translate> @@ -180,11 +179,10 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( + {hasMoreAfter && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more devices after the last one`} - disabled={!hasMoreAfter} onClick={onLoadMoreAfter} > <i18n.Translate>load older devices</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx index 124ced1f1..42a179d2c 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx @@ -126,11 +126,10 @@ function Table({ const { i18n } = useTranslationContext(); return ( <div class="table-container"> - {onLoadMoreBefore && ( + {hasMoreBefore && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more webhooks before the first one`} - disabled={!hasMoreBefore} onClick={onLoadMoreBefore} > <i18n.Translate>load newer webhooks</i18n.Translate> @@ -187,11 +186,10 @@ function Table({ })} </tbody> </table> - {onLoadMoreAfter && ( + {hasMoreAfter && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more webhooks after the last one`} - disabled={!hasMoreAfter} onClick={onLoadMoreAfter} > <i18n.Translate>load older webhooks</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx index a9e3c3a1b..e37ef4bef 100644 --- a/packages/merchant-backoffice-ui/src/paths/login/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -26,31 +26,15 @@ import { useBackendContext } from "../../context/backend.js"; import { useInstanceContext } from "../../context/instance.js"; import { AccessToken, LoginToken } from "../../declaration.js"; import { useCredentialsChecker } from "../../hooks/backend.js"; -import { useBackendURL } from "../../hooks/index.js"; interface Props { onConfirm: (token: LoginToken | undefined) => void; } -function getTokenValuePart(t: string): string { - if (!t) return t; - const match = /secret-token:(.*)/.exec(t); - if (!match || !match[1]) return ""; - return match[1]; -} - function normalizeToken(r: string): AccessToken { return `secret-token:${r}` as AccessToken; } -function cleanUp(s: string): string { - let result = s; - if (result.indexOf("webui/") !== -1) { - result = result.substring(0, result.indexOf("webui/")); - } - return result; -} - export function LoginPage({ onConfirm }: Props): VNode { const { url: backendURL, changeBackend, resetBackend } = useBackendContext(); const { admin, id } = useInstanceContext(); @@ -245,11 +229,14 @@ function AsyncButton({ onClick, disabled, type = "", children }: { type?: string export function ConnectionPage({ onConfirm }: { onConfirm: (s: string) => void }): VNode { const { url: backendURL } = useBackendContext() - const [url, setURL] = useState(cleanUp(backendURL)); + const [error, setError] = useState<string>(); + const [url, setURL] = useState(backendURL ?? ""); const { i18n } = useTranslationContext(); async function doConnect() { - onConfirm(url) + const withHttp = url.startsWith("http") ? url : "https://" + url + + onConfirm(withHttp) } return ( |