diff --git a/packages/merchant-backoffice-ui/package.json b/packages/merchant-backoffice-ui/package.json index 66e03a65c..23b258792 100644 --- a/packages/merchant-backoffice-ui/package.json +++ b/packages/merchant-backoffice-ui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/merchant-backoffice-ui", - "version": "0.1.0", + "version": "0.9.3-dev.27", "license": "AGPL-3.0-or-later", "type": "module", "scripts": { diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx index f0a7de53b..c871b1633 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]); @@ -151,7 +151,7 @@ function ApplicationStatusRoutes(): VNode { } const SUPPORTED_VERSION = "5:0:1" - if (!LibtoolVersion.compare( + if (result.data && !LibtoolVersion.compare( SUPPORTED_VERSION, result.data.version, )?.compatible) { 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(`/`); @@ -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(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({ {error &&

{error}

} {help} - {side} + {expand ?
{side}
: side} + ); } 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, onSelect: (id: string) => void }): VNode { + const { i18n } = useTranslationContext() + + const [error, setError] = useState( + undefined, + ); + + const [id, setId] = useState() + async function check(currentId: string | undefined): Promise { + if (!currentId) { + setError(i18n.str`missing id`); + return; + } + try { + await testIfExist(currentId); + onSelect(currentId); + setError(undefined); + } catch { + setError(i18n.str`not found`); + } + } + + return
+
+
+
+
+ setId(e.currentTarget.value)} + placeholder={palceholder} + /> + {error &&

{error}

} +
+ + + +
+
+
+
+} 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({
  • - + - Server + Settings
  • 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 > { 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(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> { 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> { 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() 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> => { + const res = await request(`/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>; + testTemplateExist: ( + id: string + ) => Promise>; deleteTemplate: (id: string) => Promise>; 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, HttpError>( - // [ - // `/private/templates`, - // token, - // url, - // args?.position, - // totalBefore, - // ], - // templateFetcher, - // ); + const { + data: beforeData, + error: beforeError, + isValidating: loadingBefore, + } = useSWR< + HttpResponseOk, + RequestError>( + [ + `/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 - // >({ 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(""); const [settings] = useSettings(); return ( -
    -
    -
    -
    -
    -
    - setOrderId(e.currentTarget.value)} - placeholder={i18n.str`order id`} - /> - {errorOrderId &&

    {errorOrderId}

    } -
    - - - -
    -
    -
    -
    +
    @@ -249,7 +216,11 @@ export function ListPage({ onCopyURL={onCopyURL} onSelect={onSelectOrder} onRefund={onRefundOrder} + hasMoreAfter={hasMoreAfter} + hasMoreBefore={hasMoreBefore} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} /> -
    + ); } 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 (
    - {onLoadMoreBefore && ( + {hasMoreBefore && (
    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( - undefined, - ); - - const [productId, setProductId] = useState() - async function testIfProductExistAndSelect(orderId: string | undefined): Promise { - 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 (
    -
    -
    -
    -
    -
    - setProductId(e.currentTarget.value)} - placeholder={i18n.str`product id`} - /> - {errorId &&

    {errorId}

    } -
    - - - -
    -
    -
    -
    + ({ ...o, @@ -65,6 +64,5 @@ export function ListPage({ onLoadMoreAfter={onLoadMoreAfter} hasMoreAfter={!onLoadMoreAfter} /> -
    ); } 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 (
    - {onLoadMoreBefore && ( + {hasMoreBefore && (