From e1d86816a7c07cb8ca2d54676d5cdbbe513f2ba7 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 4 Sep 2023 14:17:55 -0300 Subject: backoffcie new version, lot of changes --- .../merchant-backoffice-ui/src/Application.tsx | 60 +-- .../src/ApplicationReadyRoutes.tsx | 86 +++-- .../merchant-backoffice-ui/src/InstanceRoutes.tsx | 219 ++++++++--- .../src/components/exception/login.tsx | 4 +- .../src/components/form/InputDate.tsx | 11 +- .../src/components/form/InputPaytoForm.tsx | 305 ++++----------- .../src/components/form/InputSearchOnList.tsx | 204 ++++++++++ .../src/components/form/InputSearchProduct.tsx | 189 ---------- .../src/components/form/InputToggle.tsx | 4 +- .../instance/DefaultInstanceFormFields.tsx | 31 +- .../src/components/menu/SideBar.tsx | 81 ++-- .../src/components/menu/index.tsx | 58 ++- .../components/product/InventoryProductForm.tsx | 8 +- .../src/components/product/ProductForm.tsx | 18 +- .../merchant-backoffice-ui/src/context/backend.ts | 52 +-- .../merchant-backoffice-ui/src/declaration.d.ts | 420 ++++++++++++--------- .../merchant-backoffice-ui/src/hooks/backend.ts | 111 +++--- packages/merchant-backoffice-ui/src/hooks/bank.ts | 217 +++++++++++ packages/merchant-backoffice-ui/src/hooks/index.ts | 23 +- .../src/hooks/instance.test.ts | 6 +- .../merchant-backoffice-ui/src/hooks/instance.ts | 5 +- packages/merchant-backoffice-ui/src/hooks/otp.ts | 223 +++++++++++ .../src/hooks/reserve.test.ts | 114 +++--- .../merchant-backoffice-ui/src/hooks/reserves.ts | 64 ++-- packages/merchant-backoffice-ui/src/hooks/urls.ts | 34 +- .../src/hooks/useSettings.ts | 37 +- .../src/paths/admin/create/CreatePage.tsx | 96 ++--- .../instance/accounts/create/Create.stories.tsx | 28 ++ .../paths/instance/accounts/create/CreatePage.tsx | 175 +++++++++ .../src/paths/instance/accounts/create/index.tsx | 65 ++++ .../paths/instance/accounts/list/List.stories.tsx | 28 ++ .../src/paths/instance/accounts/list/ListPage.tsx | 64 ++++ .../src/paths/instance/accounts/list/Table.tsx | 385 +++++++++++++++++++ .../src/paths/instance/accounts/list/index.tsx | 107 ++++++ .../instance/accounts/update/Update.stories.tsx | 32 ++ .../paths/instance/accounts/update/UpdatePage.tsx | 114 ++++++ .../src/paths/instance/accounts/update/index.tsx | 96 +++++ .../src/paths/instance/details/DetailPage.tsx | 10 +- .../src/paths/instance/details/stories.tsx | 6 +- .../paths/instance/kyc/list/ListPage.stories.tsx | 2 +- .../src/paths/instance/kyc/list/ListPage.tsx | 6 +- .../instance/orders/create/Create.stories.tsx | 7 +- .../paths/instance/orders/create/CreatePage.tsx | 161 ++++++-- .../src/paths/instance/orders/create/index.tsx | 6 +- .../instance/orders/details/Detail.stories.tsx | 2 - .../paths/instance/orders/details/DetailPage.tsx | 28 +- .../src/paths/instance/orders/details/Timeline.tsx | 5 +- .../src/paths/instance/orders/list/ListPage.tsx | 42 ++- .../src/paths/instance/orders/list/Table.tsx | 27 +- .../src/paths/instance/orders/list/index.tsx | 18 +- .../src/paths/instance/products/list/Table.tsx | 62 +-- .../src/paths/instance/products/list/index.tsx | 48 ++- .../paths/instance/reserves/create/CreatePage.tsx | 14 +- .../reserves/create/CreatedSuccessfully.tsx | 22 +- .../src/paths/instance/reserves/create/index.tsx | 8 +- .../paths/instance/reserves/details/DetailPage.tsx | 36 +- .../instance/reserves/details/Details.stories.tsx | 6 +- .../paths/instance/reserves/details/RewardInfo.tsx | 88 +++++ .../paths/instance/reserves/details/TipInfo.tsx | 87 ----- .../instance/reserves/list/AutorizeRewardModal.tsx | 124 ++++++ .../instance/reserves/list/AutorizeTipModal.tsx | 124 ------ .../instance/reserves/list/CreatedSuccessfully.tsx | 18 +- .../paths/instance/reserves/list/List.stories.tsx | 6 - .../src/paths/instance/reserves/list/Table.tsx | 39 +- .../src/paths/instance/reserves/list/index.tsx | 82 ++-- .../paths/instance/templates/create/CreatePage.tsx | 127 ++----- .../src/paths/instance/templates/list/index.tsx | 51 ++- .../src/paths/instance/templates/qr/QrPage.tsx | 62 +-- .../src/paths/instance/templates/qr/index.tsx | 2 +- .../paths/instance/templates/update/UpdatePage.tsx | 121 +----- .../src/paths/instance/token/DetailPage.tsx | 165 ++++++++ .../src/paths/instance/token/index.tsx | 90 +++++ .../src/paths/instance/token/stories.tsx | 28 ++ .../src/paths/instance/transfers/create/index.tsx | 3 +- .../src/paths/instance/transfers/list/Table.tsx | 10 +- .../src/paths/instance/transfers/list/index.tsx | 3 +- .../src/paths/instance/update/Update.stories.tsx | 6 +- .../src/paths/instance/update/UpdatePage.tsx | 118 ++---- .../instance/validators/create/Create.stories.tsx | 28 ++ .../instance/validators/create/CreatePage.tsx | 195 ++++++++++ .../validators/create/CreatedSuccessfully.tsx | 104 +++++ .../src/paths/instance/validators/create/index.tsx | 70 ++++ .../instance/validators/list/List.stories.tsx | 28 ++ .../paths/instance/validators/list/ListPage.tsx | 64 ++++ .../src/paths/instance/validators/list/Table.tsx | 213 +++++++++++ .../src/paths/instance/validators/list/index.tsx | 106 ++++++ .../instance/validators/update/Update.stories.tsx | 32 ++ .../instance/validators/update/UpdatePage.tsx | 185 +++++++++ .../src/paths/instance/validators/update/index.tsx | 102 +++++ .../src/paths/instance/webhooks/list/Table.tsx | 5 - .../src/paths/settings/index.tsx | 103 +++-- .../merchant-backoffice-ui/src/schemas/index.ts | 4 +- 92 files changed, 4980 insertions(+), 1903 deletions(-) create mode 100644 packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx delete mode 100644 packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx create mode 100644 packages/merchant-backoffice-ui/src/hooks/bank.ts create mode 100644 packages/merchant-backoffice-ui/src/hooks/otp.ts create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx delete mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/details/TipInfo.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx delete mode 100644 packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeTipModal.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/create/Create.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/create/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/list/List.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/list/ListPage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/list/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/update/Update.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/update/UpdatePage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/update/index.tsx (limited to 'packages/merchant-backoffice-ui/src') diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx index f6a81ff8d..5e82821ae 100644 --- a/packages/merchant-backoffice-ui/src/Application.tsx +++ b/packages/merchant-backoffice-ui/src/Application.tsx @@ -19,19 +19,20 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { HttpStatusCode, LibtoolVersion } from "@gnu-taler/taler-util"; import { ErrorType, TranslationProvider, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { route } from "preact-router"; -import { useMemo, useState } from "preact/hooks"; +import { useMemo } from "preact/hooks"; import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js"; import { Loading } from "./components/exception/loading.js"; import { - NotificationCard, - NotYetReadyAppMenu, + NotConnectedAppMenu, + NotificationCard } from "./components/menu/index.js"; import { BackendContextProvider, @@ -41,23 +42,24 @@ import { ConfigContextProvider } from "./context/config.js"; import { useBackendConfig } from "./hooks/backend.js"; import { strings } from "./i18n/strings.js"; import LoginPage from "./paths/login/index.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { Settings } from "./paths/settings/index.js"; export function Application(): VNode { return ( - // - // ); } +/** + * Check connection testing against /config + * + * @returns + */ function ApplicationStatusRoutes(): VNode { - const { updateLoginStatus, triedToLog } = useBackendContext(); + const { url, updateLoginStatus, triedToLog } = useBackendContext(); const result = useBackendConfig(); const { i18n } = useTranslationContext(); @@ -71,19 +73,10 @@ function ApplicationStatusRoutes(): VNode { : { currency: "unknown", version: "unknown" }; const ctx = useMemo(() => ({ currency, version }), [currency, version]); - const [showSettings, setShowSettings] = useState(false) - - if (showSettings) { - return - setShowSettings(true)} title="UI Settings" /> - - - } - if (!triedToLog) { return ( - setShowSettings(true)} /> + ); @@ -97,7 +90,7 @@ function ApplicationStatusRoutes(): VNode { ) { return ( - setShowSettings(true)} /> + ); @@ -108,7 +101,7 @@ function ApplicationStatusRoutes(): VNode { ) { return ( - setShowSettings(true)} /> + - setShowSettings(true)} /> + - setShowSettings(true)} /> + - setShowSettings(true)} /> + + + + + + + } + return (
diff --git a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx index 277c2b176..46dea98e3 100644 --- a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx +++ b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx @@ -22,7 +22,7 @@ import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser"; import { createHashHistory } from "history"; import { Fragment, h, VNode } from "preact"; import { Router, Route, route } from "preact-router"; -import { useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import { NotificationCard, NotYetReadyAppMenu, @@ -35,52 +35,55 @@ import { INSTANCE_ID_LOOKUP } from "./utils/constants.js"; import { HttpStatusCode } from "@gnu-taler/taler-util"; import { Settings } from "./paths/settings/index.js"; +/** + * Check if admin against /management/instances + * @returns + */ export function ApplicationReadyRoutes(): VNode { const { i18n } = useTranslationContext(); + const [unauthorized, setUnauthorized] = useState(false) const { url: backendURL, - updateLoginStatus, - clearAllTokens, + updateLoginStatus: updateLoginStatus2, } = useBackendContext(); + function updateLoginStatus(url: string, token: string | undefined) { + console.log("updateing", url, token) + updateLoginStatus2(url, token) + setUnauthorized(false) + } + const result = useBackendInstancesTestForAdmin(); const clearTokenAndGoToRoot = () => { - clearAllTokens(); route("/"); }; const [showSettings, setShowSettings] = useState(false) + // useEffect(() => { + // setUnauthorized(FF) + // }, [FF]) + const unauthorizedAdmin = !result.loading && !result.ok && result.type === ErrorType.CLIENT && result.status === HttpStatusCode.Unauthorized if (showSettings) { return - setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} /> - + setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> + } - if (result.loading) return setShowSettings(true)} title="Loading..." />; - let admin = true; - let instanceNameByBackendURL; + if (result.loading) { + return setShowSettings(true)} title="Loading..." isPasswordOk={false} />; + } - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) { - return ( - - setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} /> - - - - ); - } + let admin = result.ok || unauthorizedAdmin; + let instanceNameByBackendURL: string | undefined; + + if (!admin) { + // * the testing against admin endpoint failed and it's not + // an authorization problem + // * merchant backend will return this SPA under the main + // endpoint or /instance/ endpoint + // => trying to infer the instance id const path = new URL(backendURL).pathname; const match = INSTANCE_ID_LOOKUP.exec(path); if (!match || !match[1]) { @@ -89,7 +92,7 @@ export function ApplicationReadyRoutes(): VNode { // does not match our pattern return ( - setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} /> + setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> + setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> + + + + } + const history = createHashHistory(); return ( @@ -113,6 +130,11 @@ export function ApplicationReadyRoutes(): VNode { default component={DefaultMainRoute} admin={admin} + onUnauthorized={() => setUnauthorized(true)} + onLoginPass={() => { + console.log("ahora si") + setUnauthorized(false) + }} instanceNameByBackendURL={instanceNameByBackendURL} /> @@ -122,6 +144,8 @@ export function ApplicationReadyRoutes(): VNode { function DefaultMainRoute({ instance, admin, + onUnauthorized, + onLoginPass, instanceNameByBackendURL, url, //from preact-router }: any): VNode { @@ -133,6 +157,8 @@ function DefaultMainRoute({ diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx index 1547442ea..4a4b3fee4 100644 --- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx +++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx @@ -40,6 +40,7 @@ import { import { useInstanceKYCDetails } from "./hooks/instance.js"; import InstanceCreatePage from "./paths/admin/create/index.js"; import InstanceListPage from "./paths/admin/list/index.js"; +import TokenPage from "./paths/instance/token/index.js"; import ListKYCPage from "./paths/instance/kyc/list/index.js"; import OrderCreatePage from "./paths/instance/orders/create/index.js"; import OrderDetailsPage from "./paths/instance/orders/details/index.js"; @@ -47,6 +48,9 @@ import OrderListPage from "./paths/instance/orders/list/index.js"; import ProductCreatePage from "./paths/instance/products/create/index.js"; import ProductListPage from "./paths/instance/products/list/index.js"; import ProductUpdatePage from "./paths/instance/products/update/index.js"; +import BankAccountCreatePage from "./paths/instance/accounts/create/index.js"; +import BankAccountListPage from "./paths/instance/accounts/list/index.js"; +import BankAccountUpdatePage from "./paths/instance/accounts/update/index.js"; import ReservesCreatePage from "./paths/instance/reserves/create/index.js"; import ReservesDetailsPage from "./paths/instance/reserves/details/index.js"; import ReservesListPage from "./paths/instance/reserves/list/index.js"; @@ -58,6 +62,9 @@ import TemplateUpdatePage from "./paths/instance/templates/update/index.js"; import WebhookCreatePage from "./paths/instance/webhooks/create/index.js"; import WebhookListPage from "./paths/instance/webhooks/list/index.js"; import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js"; +import ValidatorCreatePage from "./paths/instance/validators/create/index.js"; +import ValidatorListPage from "./paths/instance/validators/list/index.js"; +import ValidatorUpdatePage from "./paths/instance/validators/update/index.js"; import TransferCreatePage from "./paths/instance/transfers/create/index.js"; import TransferListPage from "./paths/instance/transfers/list/index.js"; import InstanceUpdatePage, { @@ -69,11 +76,16 @@ import NotFoundPage from "./paths/notfound/index.js"; import { Notification } from "./utils/types.js"; import { MerchantBackend } from "./declaration.js"; import { Settings } from "./paths/settings/index.js"; +import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js"; export enum InstancePaths { - // details = '/', error = "/error", - update = "/update", + server = "/server", + token = "/token", + + bank_list = "/bank", + bank_update = "/bank/:bid/update", + bank_new = "/bank/new", product_list = "/products", product_update = "/product/:pid/update", @@ -102,11 +114,15 @@ export enum InstancePaths { webhooks_update = "/webhooks/:tid/update", webhooks_new = "/webhooks/new", - settings = "/settings", + validators_list = "/validators", + validators_update = "/validators/:vid/update", + validators_new = "/validators/new", + + settings = "/inteface", } // eslint-disable-next-line @typescript-eslint/no-empty-function -const noop = () => {}; +const noop = () => { }; export enum AdminPaths { list_instances = "/instances", @@ -118,6 +134,8 @@ export interface Props { id: string; admin?: boolean; path: string; + onUnauthorized: () => void; + onLoginPass: () => void; setInstanceName: (s: string) => void; } @@ -125,40 +143,29 @@ export function InstanceRoutes({ id, admin, path, + onUnauthorized, + onLoginPass, setInstanceName, }: Props): VNode { - const [_, updateDefaultToken] = useBackendDefaultToken(); + const [defaultToken, updateDefaultToken] = useBackendDefaultToken(); const [token, updateToken] = useBackendInstanceToken(id); - const { - updateLoginStatus: changeBackend, - addTokenCleaner, - clearAllTokens, - } = useBackendContext(); - const cleaner = useCallback(() => { - updateToken(undefined); - }, [id]); const { i18n } = useTranslationContext(); type GlobalNotifState = (Notification & { to: string }) | undefined; const [globalNotification, setGlobalNotification] = useState(undefined); - useEffect(() => { - addTokenCleaner(cleaner); - }, [addTokenCleaner, cleaner]); - const changeToken = (token?: string) => { if (admin) { updateToken(token); } else { updateDefaultToken(token); } + onLoginPass() }; - const updateLoginStatus = (url: string, token?: string) => { - changeBackend(url); - if (!token) return; - changeToken(token); - }; + // const updateLoginStatus = (url: string, token?: string) => { + // changeToken(token); + // }; const value = useMemo( () => ({ id, token, admin, changeToken }), @@ -192,18 +199,17 @@ export function InstanceRoutes({ }; } - const LoginPageAccessDenied = () => ( - - - - - ); + // const LoginPageAccessDeniend = onUnauthorized + const LoginPageAccessDenied = () => { + onUnauthorized() + return + } function IfAdminCreateDefaultOr(Next: FunctionComponent) { return function IfAdminCreateDefaultOrImpl(props?: T) { @@ -234,8 +240,10 @@ export function InstanceRoutes({ } const clearTokenAndGoToRoot = () => { - clearAllTokens(); route("/"); + // clear all tokens + updateToken(undefined) + updateDefaultToken(undefined) }; return ( @@ -244,11 +252,12 @@ export function InstanceRoutes({ instance={id} admin={admin} onShowSettings={() => { - route("/settings") + route("/inteface") }} path={path} onLogout={clearTokenAndGoToRoot} setInstanceName={setInstanceName} + isPasswordOk={defaultToken !== undefined} /> @@ -308,7 +317,7 @@ export function InstanceRoutes({ * Update instance page */} { route(`/`); @@ -321,6 +330,19 @@ export function InstanceRoutes({ onUnauthorized={LoginPageAccessDenied} onLoadError={ServerErrorRedirectTo(InstancePaths.error)} /> + {/** + * Update instance page + */} + { + route(`/`); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.error)} + /> {/** * Product pages */} @@ -328,7 +350,7 @@ export function InstanceRoutes({ path={InstancePaths.product_list} component={ProductListPage} onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onCreate={() => { route(InstancePaths.product_new); }} @@ -360,6 +382,45 @@ export function InstanceRoutes({ route(InstancePaths.product_list); }} /> + {/** + * Bank pages + */} + { + route(InstancePaths.bank_new); + }} + onSelect={(id: string) => { + route(InstancePaths.bank_update.replace(":bid", id)); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + { + route(InstancePaths.bank_list); + }} + onBack={() => { + route(InstancePaths.bank_list); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + { + route(InstancePaths.bank_list); + }} + onBack={() => { + route(InstancePaths.bank_list); + }} + /> {/** * Order pages */} @@ -373,7 +434,7 @@ export function InstanceRoutes({ route(InstancePaths.order_details.replace(":oid", id)); }} onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> { - route(InstancePaths.order_list); + onConfirm={(orderId: string) => { + route(InstancePaths.order_details.replace(":oid", orderId)); }} onBack={() => { route(InstancePaths.order_list); @@ -404,7 +465,7 @@ export function InstanceRoutes({ component={TransferListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onCreate={() => { route(InstancePaths.transfers_new); }} @@ -427,7 +488,7 @@ export function InstanceRoutes({ component={WebhookListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onCreate={() => { route(InstancePaths.webhooks_new); }} @@ -458,6 +519,45 @@ export function InstanceRoutes({ route(InstancePaths.webhooks_list); }} /> + {/** + * Validator pages + */} + { + route(InstancePaths.validators_new); + }} + onSelect={(id: string) => { + route(InstancePaths.validators_update.replace(":vid", id)); + }} + /> + { + route(InstancePaths.validators_list); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.validators_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.validators_list); + }} + /> + { + route(InstancePaths.validators_list); + }} + onBack={() => { + route(InstancePaths.validators_list); + }} + /> {/** * Templates pages */} @@ -466,7 +566,7 @@ export function InstanceRoutes({ component={TemplateListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onCreate={() => { route(InstancePaths.templates_new); }} @@ -535,7 +635,7 @@ export function InstanceRoutes({ component={ReservesListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onSelect={(id: string) => { route(InstancePaths.reserves_details.replace(":rid", id)); }} @@ -590,7 +690,7 @@ function AdminInstanceUpdatePage({ const { updateLoginStatus: changeBackend } = useBackendContext(); const updateLoginStatus = (url: string, token?: string): void => { changeBackend(url); - if (token) changeToken(token); + changeToken(token); }; const value = useMemo( () => ({ id, token, admin: true, changeToken }), @@ -607,20 +707,20 @@ function AdminInstanceUpdatePage({ const notif = error.type === ErrorType.TIMEOUT ? { - message: i18n.str`The request to the backend take too long and was cancelled`, - description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, - type: "ERROR" as const, - } + message: i18n.str`The request to the backend take too long and was cancelled`, + description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, + type: "ERROR" as const, + } : { - message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, - description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, - details: - error.type === ErrorType.CLIENT || + message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, + description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, + details: + error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER - ? error.payload.detail - : undefined, - type: "ERROR" as const, - }; + ? error.payload.detail + : undefined, + type: "ERROR" as const, + }; return ( @@ -650,7 +750,8 @@ function AdminInstanceUpdatePage({ function KycBanner(): VNode { const kycStatus = useInstanceKYCDetails(); const { i18n } = useTranslationContext(); - const today = format(new Date(), "yyyy-MM-dd"); + const [settings] = useSettings(); + const today = format(new Date(), dateFormatForSettings(settings)); const [lastHide, setLastHide] = useLocalStorage("kyc-last-hide"); const hasBeenHidden = today === lastHide; const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect"; diff --git a/packages/merchant-backoffice-ui/src/components/exception/login.tsx b/packages/merchant-backoffice-ui/src/components/exception/login.tsx index f2f94a7c5..4fa440fc7 100644 --- a/packages/merchant-backoffice-ui/src/components/exception/login.tsx +++ b/packages/merchant-backoffice-ui/src/components/exception/login.tsx @@ -93,7 +93,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode { e.keyCode === 13 @@ -186,7 +186,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode { e.keyCode === 13 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx index 1f41c3564..a398629dc 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx @@ -20,16 +20,18 @@ */ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; -import { h, VNode } from "preact"; +import { ComponentChildren, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { DatePicker } from "../picker/DatePicker.js"; import { InputProps, useField } from "./useField.js"; +import { dateFormatForSettings, useSettings } from "../../hooks/useSettings.js"; export interface Props extends InputProps { readonly?: boolean; expand?: boolean; //FIXME: create separated components InputDate and InputTimestamp withTimestampSupport?: boolean; + side?: ComponentChildren; } export function InputDate({ @@ -41,9 +43,11 @@ export function InputDate({ tooltip, expand, withTimestampSupport, + side, }: Props): VNode { const [opened, setOpened] = useState(false); const { i18n } = useTranslationContext(); + const [settings] = useSettings() const { error, required, value, onChange } = useField(name); @@ -51,14 +55,14 @@ export function InputDate({ if (!value) { strValue = withTimestampSupport ? "unknown" : ""; } else if (value instanceof Date) { - strValue = format(value, "yyyy/MM/dd"); + strValue = format(value, dateFormatForSettings(settings)); } else if (value.t_s) { strValue = value.t_s === "never" ? withTimestampSupport ? "never" : "" - : format(new Date(value.t_s * 1000), "yyyy/MM/dd"); + : format(new Date(value.t_s * 1000), dateFormatForSettings(settings)); } return ( @@ -142,6 +146,7 @@ export function InputDate({ )} + {side}
extends InputProps { isValid?: (e: any) => boolean; } +// type Entity = PaytoUriGeneric // https://datatracker.ietf.org/doc/html/rfc8905 type Entity = { // iban, bitcoin, x-taler-bank. it defined the format target: string; // path1 if the first field to be used - path1: string; + path1?: string; // path2 if the second field to be used, optional path2?: string; - // options of the payto uri - options: { + // params of the payto uri + params: { "receiver-name"?: string; sender?: string; message?: string; @@ -52,13 +52,6 @@ type Entity = { instruction?: string; [name: string]: string | undefined; }; - auth: { - type: "unset" | "basic" | "none"; - url?: string; - username?: string; - password?: string; - repeat?: string; - }; }; function isEthereumAddress(address: string) { @@ -171,14 +164,10 @@ const targets = [ "bitcoin", "ethereum", ]; -const accountAuthType = ["none", "basic"]; const noTargetValue = targets[0]; -const defaultTarget: Partial = { +const defaultTarget: Entity = { target: noTargetValue, - options: {}, - auth: { - type: "unset" as const, - }, + params: {}, }; export function InputPaytoForm({ @@ -187,110 +176,91 @@ export function InputPaytoForm({ label, tooltip, }: Props): VNode { - const { value: paytos, onChange, required } = useField(name); - - const [value, valueHandler] = useState>(defaultTarget); + const { value: initialValueStr, onChange } = useField(name); - let payToPath; - if (value.target === "iban" && value.path1) { - payToPath = `/${value.path1.toUpperCase()}`; - } else if (value.path1) { - if (value.path2) { - payToPath = `/${value.path1}/${value.path2}`; - } else { - payToPath = `/${value.path1}`; - } + const initialPayto = parsePaytoUri(initialValueStr ?? "") + const paths = !initialPayto ? [] : initialPayto.targetPath.split("/") + const initialPath1 = paths.length >= 1 ? paths[0] : undefined; + const initialPath2 = paths.length >= 2 ? paths[1] : undefined; + const initial: Entity = initialPayto === undefined ? defaultTarget : { + target: initialPayto.targetType, + params: initialPayto.params, + path1: initialPath1, + path2: initialPath2, } - const { i18n } = useTranslationContext(); + const [value, setValue] = useState>(initial) - const ops = value.options ?? {}; - const url = tryUrl(`payto://${value.target}${payToPath}`); - if (url) { - Object.keys(ops).forEach((opt_key) => { - const opt_value = ops[opt_key]; - if (opt_value) url.searchParams.set(opt_key, opt_value); - }); - } - const paytoURL = !url ? "" : url.href; + const { i18n } = useTranslationContext(); const errors: FormErrors = { target: - value.target === noTargetValue && !paytos.length + value.target === noTargetValue ? i18n.str`required` : undefined, path1: !value.path1 ? i18n.str`required` : value.target === "iban" - ? validateIBAN(value.path1, i18n) - : value.target === "bitcoin" - ? validateBitcoin(value.path1, i18n) - : value.target === "ethereum" - ? validateEthereum(value.path1, i18n) - : undefined, + ? validateIBAN(value.path1, i18n) + : value.target === "bitcoin" + ? validateBitcoin(value.path1, i18n) + : value.target === "ethereum" + ? validateEthereum(value.path1, i18n) + : undefined, path2: value.target === "x-taler-bank" ? !value.path2 ? i18n.str`required` : undefined : undefined, - options: undefinedIfEmpty({ - "receiver-name": !value.options?.["receiver-name"] + params: undefinedIfEmpty({ + "receiver-name": !value.params?.["receiver-name"] ? i18n.str`required` : undefined, }), - auth: !value.auth - ? undefined - : undefinedIfEmpty({ - username: - value.auth.type === "basic" && !value.auth.username - ? i18n.str`required` - : undefined, - password: - value.auth.type === "basic" && !value.auth.password - ? i18n.str`required` - : undefined, - repeat: - value.auth.type === "basic" && !value.auth.repeat - ? i18n.str`required` - : value.auth.repeat !== value.auth.password - ? i18n.str`is not the same` - : undefined, - }), }; const hasErrors = Object.keys(errors).some( (k) => (errors as any)[k] !== undefined, ); + const str = hasErrors || !value.target ? undefined : stringifyPaytoUri({ + targetType: value.target, + targetPath: value.path2 ? `${value.path1}/${value.path2}` : (value.path1 ?? ""), + params: value.params ?? {} as any, + isKnown: false, + }) + useEffect(() => { + onChange(str as any) + }, [str]) - const submit = useCallback((): void => { - const accounts: MerchantBackend.Instances.MerchantBankAccount[] = paytos; - const alreadyExists = - accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1; - if (!alreadyExists) { - const newValue: MerchantBackend.Instances.MerchantBankAccount = { - payto_uri: paytoURL, - }; - if (value.auth) { - if (value.auth.url) { - newValue.credit_facade_url = value.auth.url; - } - if (value.auth.type === "none") { - newValue.credit_facade_credentials = { - type: "none", - }; - } - if (value.auth.type === "basic") { - newValue.credit_facade_credentials = { - type: "basic", - username: value.auth.username ?? "", - password: value.auth.password ?? "", - }; - } - } - onChange([newValue, ...accounts] as any); - } - valueHandler(defaultTarget); - }, [value]); + // const submit = useCallback((): void => { + // // const accounts: MerchantBackend.BankAccounts.AccountAddDetails[] = paytos; + // // const alreadyExists = + // // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1; + // // if (!alreadyExists) { + // const newValue: MerchantBackend.BankAccounts.AccountAddDetails = { + // payto_uri: paytoURL, + // }; + // if (value.auth) { + // if (value.auth.url) { + // newValue.credit_facade_url = value.auth.url; + // } + // if (value.auth.type === "none") { + // newValue.credit_facade_credentials = { + // type: "none", + // }; + // } + // if (value.auth.type === "basic") { + // newValue.credit_facade_credentials = { + // type: "basic", + // username: value.auth.username ?? "", + // password: value.auth.password ?? "", + // }; + // } + // } + // onChange(newValue as any); + // // } + // // valueHandler(defaultTarget); + // }, [value]); //FIXME: translating plural singular return ( @@ -299,11 +269,11 @@ export function InputPaytoForm({ name="tax" errors={errors} object={value} - valueHandler={valueHandler} + valueHandler={setValue} > name="target" - label={i18n.str`Target type`} + label={i18n.str`Account type`} tooltip={i18n.str`Method to use for wire transfer`} values={targets} toStr={(v) => (v === noTargetValue ? i18n.str`Choose one...` : v)} @@ -400,150 +370,15 @@ export function InputPaytoForm({ {value.target !== noTargetValue && ( - - { - // if (str === "unset") { - // return "Without change"; - // } - if (str === "none") return "Without authentication"; - return "Username and password"; - }} - /> - {value.auth?.type === "basic" ? ( - - - - - - ) : undefined} - - {/* v.toUpperCase()} - addonAfter={ - - {showKey ? ( - - ) : ( - - )} - - } - side={ - - - - } - /> */} )} - {/** - * Show the values in the list - */} -
- - {value.target !== noTargetValue && ( -
- -
- )} ); } -function tryUrl(s: string): URL | undefined { - try { - return new URL(s); - } catch (e) { - return undefined; - } -} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx new file mode 100644 index 000000000..be5800d14 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx @@ -0,0 +1,204 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import emptyImage from "../../assets/empty.png"; +import { FormErrors, FormProvider } from "./FormProvider.js"; +import { InputWithAddon } from "./InputWithAddon.js"; +import { TranslatedString } from "@gnu-taler/taler-util"; + +type Entity = { + id: string, + description: string; + image?: string; + extra?: string; +}; + +export interface Props { + selected?: T; + onChange: (p?: T) => void; + label: TranslatedString; + list: T[]; + withImage?: boolean; +} + +interface Search { + name: string; +} + +export function InputSearchOnList({ + selected, + onChange, + label, + list, + withImage, +}: Props): VNode { + const [nameForm, setNameForm] = useState>({ + name: "", + }); + + const errors: FormErrors = { + name: undefined, + }; + const { i18n } = useTranslationContext(); + + if (selected) { + return ( +
+ {withImage && +
+

+ +

+
+ } +
+
+

+ ID: {selected.id} +

+

+ Description:{" "} + {selected.description} +

+
+ +
+
+
+
+ ); + } + + return ( + + errors={errors} + object={nameForm} + valueHandler={setNameForm} + > + + name="name" + label={label} + tooltip={i18n.str`enter description or id`} + addonAfter={ + + + + } + > +
+ { + setNameForm({ name: "" }); + onChange(p); + }} + withImage={!!withImage} + /> +
+ + + ); +} + +interface DropdownListProps { + name?: string; + onSelect: (p: T) => void; + list: T[]; + withImage: boolean; +} + +function DropdownList({ name, onSelect, list, withImage }: DropdownListProps) { + const { i18n } = useTranslationContext(); + if (!name) { + /* FIXME + this BR is added to occupy the space that will be added when the + dropdown appears + */ + return ( +
+
+
+ ); + } + const filtered = list.filter( + (p) => p.id.includes(name) || p.description.includes(name), + ); + + return ( + + ); +} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx deleted file mode 100644 index 1c1fcb907..000000000 --- a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx +++ /dev/null @@ -1,189 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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 - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import emptyImage from "../../assets/empty.png"; -import { MerchantBackend, WithId } from "../../declaration.js"; -import { FormErrors, FormProvider } from "./FormProvider.js"; -import { InputWithAddon } from "./InputWithAddon.js"; - -type Entity = MerchantBackend.Products.ProductDetail & WithId; - -export interface Props { - selected?: Entity; - onChange: (p?: Entity) => void; - products: (MerchantBackend.Products.ProductDetail & WithId)[]; -} - -interface ProductSearch { - name: string; -} - -export function InputSearchProduct({ - selected, - onChange, - products, -}: Props): VNode { - const [prodForm, setProdName] = useState>({ - name: "", - }); - - const errors: FormErrors = { - name: undefined, - }; - const { i18n } = useTranslationContext(); - - if (selected) { - return ( -
-
-

- -

-
-
-
-

- Product id: {selected.id} -

-

- Description:{" "} - {selected.description} -

-
- -
-
-
-
- ); - } - - return ( - - errors={errors} - object={prodForm} - valueHandler={setProdName} - > - - name="name" - label={i18n.str`Product`} - tooltip={i18n.str`search products by it's description or id`} - addonAfter={ - - - - } - > -
- { - setProdName({ name: "" }); - onChange(p); - }} - /> -
- - - ); -} - -interface ProductListProps { - name?: string; - onSelect: (p: MerchantBackend.Products.ProductDetail & WithId) => void; - list: (MerchantBackend.Products.ProductDetail & WithId)[]; -} - -function ProductList({ name, onSelect, list }: ProductListProps) { - const { i18n } = useTranslationContext(); - if (!name) { - /* FIXME - this BR is added to occupy the space that will be added when the - dropdown appears - */ - return ( -
-
-
- ); - } - const filtered = list.filter( - (p) => p.id.includes(name) || p.description.includes(name), - ); - - return ( - - ); -} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx index 61ddf3c84..f95dfcd05 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx @@ -56,7 +56,7 @@ export function InputToggle({ return (
-
-
+

diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx index cdbae4ae0..cb318906f 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.update: + case InstancePaths.server: return `${id}: Settings`; case InstancePaths.order_list: return `${id}: Orders`; @@ -50,6 +50,12 @@ function getInstanceTitle(path: string, id: string): string { return `${id}: New webhook`; case InstancePaths.webhooks_update: return `${id}: Update webhook`; + case InstancePaths.validators_list: + return `${id}: Validators`; + case InstancePaths.validators_new: + return `${id}: New validator`; + case InstancePaths.validators_update: + return `${id}: Update validators`; case InstancePaths.templates_new: return `${id}: New template`; case InstancePaths.templates_update: @@ -58,6 +64,10 @@ 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: + return `${id}: Interface`; default: return ""; } @@ -77,6 +87,7 @@ interface MenuProps { onLogout?: () => void; onShowSettings: () => void; setInstanceName: (s: string) => void; + isPasswordOk: boolean; } function WithTitle({ @@ -100,14 +111,15 @@ export function Menu({ path, admin, setInstanceName, + isPasswordOk }: MenuProps): VNode { const [mobileOpen, setMobileOpen] = useState(false); const titleWithSubtitle = title ? title : !admin - ? getInstanceTitle(path, instance) - : getAdminTitle(path, instance); + ? getInstanceTitle(path, instance) + : getAdminTitle(path, instance); const adminInstance = instance === "default"; const mimic = admin && !adminInstance; return ( @@ -129,14 +141,15 @@ export function Menu({ mimic={mimic} instance={instance} mobile={mobileOpen} + isPasswordOk={isPasswordOk} /> )} {mimic && (