merchant-backoffice

ZZZ: Inactive/Deprecated
Log | Files | Refs | Submodules | README

commit 0e365961bee9abf78fa7a0dc3eaae91a8985af3e
parent 466d9b72cde265dc60c42210308dbb62e7747f1d
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu, 18 Mar 2021 01:04:22 -0300

some features

Diffstat:
MCHANGELOG.md | 12+++++-------
Mpackages/frontend/src/AdminRoutes.tsx | 15+++++----------
Mpackages/frontend/src/ApplicationReadyRoutes.tsx | 90+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mpackages/frontend/src/InstanceRoutes.tsx | 290+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mpackages/frontend/src/components/exception/loading.tsx | 8++++++--
Mpackages/frontend/src/components/exception/login.tsx | 12+-----------
Mpackages/frontend/src/components/menu/index.tsx | 190++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mpackages/frontend/src/hooks/backend.ts | 26+++++++++++++++++++++++++-
Apackages/frontend/src/hooks/notification.ts | 43+++++++++++++++++++++++++++++++++++++++++++
Mpackages/frontend/src/index.tsx | 52++++++++++++++++++----------------------------------
Mpackages/frontend/src/messages/en.po | 9+++++++--
Mpackages/frontend/src/paths/admin/create/CreatePage.tsx | 4++--
Mpackages/frontend/src/paths/admin/create/index.tsx | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mpackages/frontend/src/paths/admin/list/index.tsx | 15+++++++--------
Mpackages/frontend/src/paths/instance/details/index.tsx | 16++++++++--------
Mpackages/frontend/src/paths/instance/orders/list/index.tsx | 6++++--
Mpackages/frontend/src/paths/instance/products/list/index.tsx | 13++++++++-----
Mpackages/frontend/src/paths/instance/tips/list/index.tsx | 14+++++++++-----
Mpackages/frontend/src/paths/instance/transfers/list/index.tsx | 13++++++++-----
Mpackages/frontend/src/paths/instance/update/index.tsx | 15++++++++-------
Mpackages/frontend/src/paths/login/index.tsx | 5++---
Mpackages/frontend/src/scss/main.scss | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
22 files changed, 716 insertions(+), 302 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -22,16 +22,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - configure eslint - configure prettier - prune scss styles to reduce size - - create a loading page to be use when the data is not ready - some way to copy the url of a created instance - fix mobile: some things are still on the left - - instance id in instance list should be clickable - edit button to go to instance settings - - notifications should tale place between title and content, and not disapear - - confirmation page when creating instances - - if there is enough space for tables in mobile, make the scrollables - - - create default instance if it is not already + - check if there is a way to remove auto async for /routes /components/{async,routes} so it can be turned on when building non-single-bundle - product: main action => refund @@ -55,6 +49,10 @@ deletiing product - update title with: Taler Backoffice: $PAGE_TITLE (#6790) - paths should be /orders instead of /o (same others) - if there is enough space for tables in mobile, make the scrollables (#6789) + - show create default instance if it is not already + - notifications should tale place between title and content, and not disapear + - create a loading page to be use when the data is not ready (test at #/loading) + - confirmation page when creating instances ## [0.0.4] - 2021-03-11 - prevent letters to be input in numbers diff --git a/packages/frontend/src/AdminRoutes.tsx b/packages/frontend/src/AdminRoutes.tsx @@ -20,7 +20,6 @@ import { Notification } from "./utils/types"; import InstanceListPage from './paths/admin/list'; import InstanceCreatePage from "./paths/admin/create"; -import NotFoundPage from './paths/notfound'; export enum AdminPaths { list_instances = '/instances', @@ -29,10 +28,9 @@ export enum AdminPaths { } interface Props { - pushNotification: (n: Notification) => void; - // instances: MerchantBackend.Instances.Instance[] + // pushNotification: (n: Notification) => void; } -export function AdminRoutes({ pushNotification }: Props): VNode { +export function AdminRoutes({ }: Props): VNode { const i18n = useMessageTemplate(); return <Router> @@ -54,17 +52,14 @@ export function AdminRoutes({ pushNotification }: Props): VNode { onBack={() => route(AdminPaths.list_instances)} onConfirm={() => { - pushNotification({ message: i18n`create_success`, type: 'SUCCESS' }); - route(AdminPaths.list_instances); + // route(AdminPaths.list_instances); }} - onError={(error: any) => { - pushNotification({ message: i18n`create_error`, type: 'ERROR' }); - }} + // onError={(error: any) => { + // }} /> - {/* <Route default component={undefined} /> */} </Router> } \ No newline at end of file diff --git a/packages/frontend/src/ApplicationReadyRoutes.tsx b/packages/frontend/src/ApplicationReadyRoutes.tsx @@ -22,16 +22,15 @@ import { Fragment, h, VNode } from 'preact'; import { route } from 'preact-router'; import { Notification } from "./utils/types"; import { useBackendContext } from './context/backend'; -import { useBackendInstances } from "./hooks/backend"; +import { useBackendInstancesTestForAdmin } from "./hooks/backend"; import { InstanceRoutes } from "./InstanceRoutes"; import LoginPage from './paths/login'; import { INSTANCE_ID_LOOKUP } from './utils/constants'; -import { NotYetReadyAppMenu, Menu } from './components/menu'; +import { NotYetReadyAppMenu, Menu, NotificationCard } from './components/menu'; import { useMessageTemplate } from 'preact-messages'; interface Props { - pushNotification: (n: Notification) => void; } -export function ApplicationReadyRoutes({ pushNotification }: Props): VNode { +export function ApplicationReadyRoutes({ }: Props): VNode { const i18n = useMessageTemplate(); const { url: currentBaseUrl, changeBackend, updateToken, clearAllTokens } = useBackendContext(); @@ -39,19 +38,26 @@ export function ApplicationReadyRoutes({ pushNotification }: Props): VNode { changeBackend(url); if (token) updateToken(token); }; - const list = useBackendInstances() + const list = useBackendInstancesTestForAdmin() + + const clearTokenAndGoToRoot = () => { + clearAllTokens(); + route('/') + } if (!list.data) { if (list.unauthorized) { return <Fragment> - <NotYetReadyAppMenu title="Login" onLogout={() => { - clearAllTokens(); - route('/') - }} /> - <LoginPage - withMessage={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} - onConfirm={updateLoginStatus} + <NotYetReadyAppMenu title="Login" + onLogout={clearTokenAndGoToRoot} + /> + <NotificationCard notification={{ + message: i18n`Access denied`, + description: i18n`Check your token is valid`, + type: 'ERROR' + }} /> + <LoginPage onConfirm={updateLoginStatus} /> </Fragment> } if (list.notfound) { @@ -62,45 +68,43 @@ export function ApplicationReadyRoutes({ pushNotification }: Props): VNode { // query to /config is ok but the URL // doest not match with our pattern return <Fragment> - <NotYetReadyAppMenu title="Error" onLogout={() => { - clearAllTokens(); - route('/') - }} /> - <LoginPage - withMessage={{ message: i18n`Couldnt access the server`, description: i18n`Could not infer instance id from url ${currentBaseUrl}`, type: 'ERROR', }} - onConfirm={updateLoginStatus} + <NotYetReadyAppMenu title="Error" onLogout={clearTokenAndGoToRoot} /> + <NotificationCard notification={{ + message: i18n`Couldnt access the server`, + description: i18n`Could not infer instance id from url ${currentBaseUrl}`, + type: 'ERROR', + }} /> + <LoginPage onConfirm={updateLoginStatus} /> </Fragment> } return <Fragment> - <Menu instance={match[1]} onLogout={() => { - clearAllTokens(); - route('/') + <Menu instance={match[1]} onLogout={clearTokenAndGoToRoot} /> + <InstanceRoutes id={match[1]} /> + </Fragment> + } + + if (list.error) { + return <Fragment> + <NotYetReadyAppMenu title="Error" /> + <NotificationCard notification={{ + message: i18n`Couldnt access the server`, + description: list.error.message, + type: 'ERROR' }} /> - <InstanceRoutes id={match[1]} pushNotification={pushNotification} /> + <LoginPage onConfirm={updateLoginStatus} /> </Fragment> - } + } - if (list.error) { - return <Fragment> - <NotYetReadyAppMenu title="Error" /> - <LoginPage - withMessage={{ message: i18n`Couldnt access the server`, description: list.error.message, type: 'ERROR', }} - onConfirm={updateLoginStatus} - /> - </Fragment> + // is loading + return <NotYetReadyAppMenu title="Loading..." /> } - // is loading - return <NotYetReadyAppMenu title="Loading..." /> -} - -return <Fragment> - <Menu instance="default" admin onLogout={() => { - clearAllTokens(); - route('/') - }} /> - <InstanceRoutes admin id="default" pushNotification={pushNotification} /> -</Fragment> + return <Fragment> + <Menu instance="default" admin + onLogout={clearTokenAndGoToRoot} + /> + <InstanceRoutes admin id="default" /> + </Fragment> } diff --git a/packages/frontend/src/InstanceRoutes.tsx b/packages/frontend/src/InstanceRoutes.tsx @@ -26,11 +26,11 @@ import { useMessageTemplate } from 'preact-messages'; import { createHashHistory } from 'history'; import { useBackendInstanceToken } from './hooks'; import { InstanceContextProvider, useBackendContext } from './context/backend'; -import { SwrError } from "./hooks/backend"; -import { Notification } from './utils/types'; +import { SwrError, useInstanceDetails } from "./hooks/backend"; +// import { Notification } from './utils/types'; import LoginPage from './paths/login'; -import InstanceUpdatePage from "./paths/instance/update"; +import InstanceUpdatePage, { Props as InstanceUpdatePageProps } from "./paths/instance/update"; import DetailPage from './paths/instance/details'; import NotFoundPage from './paths/notfound'; @@ -51,6 +51,8 @@ import TransferCreatePage from './paths/instance/transfers/create' import InstanceListPage from './paths/admin/list'; import InstanceCreatePage from "./paths/admin/create"; +import { NotificationCard } from './components/menu'; +import { Loading } from './components/exception/loading'; export enum InstancePaths { details = '/', @@ -75,18 +77,45 @@ export enum InstancePaths { export enum AdminPaths { list_instances = '/instances', new_instance = '/instance/new', - instance_id_route = '/instance/:id/:rest*', + update_instance = '/instance/:id/update', } export interface Props { id: string; - pushNotification: (n: Notification) => void; admin?: boolean; } -// const rootPath = typeof window !== 'undefined' ? window.location.pathname : '/' +function AdminInstanceUpdatePage({ id, ...rest }: { id: string } & InstanceUpdatePageProps) { + const [token, updateToken] = useBackendInstanceToken(id); + const value = useMemo(() => ({ id, token, admin: true }), [id, token]) + const { changeBackend } = useBackendContext(); + const updateLoginStatus = (url: string, token?: string) => { + changeBackend(url); + if (token) + updateToken(token); + }; + const i18n = useMessageTemplate(''); + return <InstanceContextProvider value={value}> + <InstanceUpdatePage {...rest} + onLoadError={(error: SwrError) => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR' }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} + + onUnauthorized={() => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} + + /> + </InstanceContextProvider> +} -export function InstanceRoutes({ id, pushNotification, admin }: Props): VNode { +export function InstanceRoutes({ id, admin }: Props): VNode { const [token, updateToken] = useBackendInstanceToken(id); const { changeBackend, addTokenCleaner } = useBackendContext(); const cleaner = useCallback(() => { updateToken(undefined); }, [id]); @@ -117,14 +146,20 @@ export function InstanceRoutes({ id, pushNotification, admin }: Props): VNode { route(`/instance/${id}/update`); }} - onUnauthorized={() => <LoginPage - withMessage={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} - onConfirm={updateLoginStatus} />} - - onLoadError={(error: SwrError) => <LoginPage - withMessage={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR', }} - onConfirm={updateLoginStatus} />} - + onUnauthorized={() => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR' }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} + + onLoadError={(error: SwrError) => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR' }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} + /> } @@ -134,12 +169,29 @@ export function InstanceRoutes({ id, pushNotification, admin }: Props): VNode { onBack={() => route(AdminPaths.list_instances)} onConfirm={() => { - pushNotification({ message: i18n`create_success`, type: 'SUCCESS' }); + // pushNotification({ message: i18n`create_success`, type: 'SUCCESS' }); route(AdminPaths.list_instances); }} onError={(error: any) => { - pushNotification({ message: i18n`create_error`, type: 'ERROR' }); + // pushNotification({ message: i18n`create_error`, type: 'ERROR' }); + }} + + /> + } + + {admin && + <Route path={AdminPaths.update_instance} component={AdminInstanceUpdatePage} + + onBack={() => route(AdminPaths.list_instances)} + + onConfirm={() => { + // pushNotification({ message: i18n`create_success`, type: 'SUCCESS' }); + route(AdminPaths.list_instances); + }} + + onUpdateError={(e: Error) => { + // pushNotification({ message: i18n`update_error`, type: 'ERROR' }); }} /> @@ -148,49 +200,110 @@ export function InstanceRoutes({ id, pushNotification, admin }: Props): VNode { <Route path={InstancePaths.details} component={DetailPage} - onUnauthorized={() => <LoginPage - withMessage={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} - onConfirm={updateLoginStatus} />} + onUnauthorized={() => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} + + onNotFound={() => { + if (admin) { + return <Fragment> + <NotificationCard notification={{ + message: 'No default instance', + description: 'in order to use merchant backend, you should create the default instance', + type: 'INFO' + }} /> + <InstanceCreatePage onError={() => null} onConfirm={() => null} /> + </Fragment> + } + return <NotFoundPage /> + }} - onLoadError={(error: SwrError) => <LoginPage - withMessage={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR', }} - onConfirm={updateLoginStatus} />} + onLoadError={(error: SwrError) => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR' }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} /> <Route path={InstancePaths.update} component={InstanceUpdatePage} - onUnauthorized={() => <LoginPage - withMessage={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} - onConfirm={updateLoginStatus} />} + onUnauthorized={() => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} - onLoadError={(error: SwrError) => <LoginPage - withMessage={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR', }} - onConfirm={updateLoginStatus} />} + onLoadError={(error: SwrError) => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR' }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} + + onNotFound={() => { + if (admin) { + return <Fragment> + <NotificationCard notification={{ + message: 'No default instance', + description: 'in order to use merchant backend, you should create the default instance', + type: 'INFO' + }} /> + <InstanceCreatePage onError={() => null} onConfirm={() => null} /> + </Fragment> + } + return <NotFoundPage /> + }} onBack={() => { route(`/`); }} onConfirm={() => { - pushNotification({ message: i18n`create_success`, type: 'SUCCESS' }); + // pushNotification({ message: i18n`create_success`, type: 'SUCCESS' }); route(`/`); }} onUpdateError={(e: Error) => { - pushNotification({ message: i18n`update_error`, type: 'ERROR' }); + // pushNotification({ message: i18n`update_error`, type: 'ERROR' }); }} /> <Route path={InstancePaths.product_list} component={ProductListPage} - onUnauthorized={() => <LoginPage - withMessage={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} - onConfirm={updateLoginStatus} />} - onLoadError={(error: SwrError) => <LoginPage - withMessage={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR', }} - onConfirm={updateLoginStatus} />} + onUnauthorized={() => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} + + onNotFound={() => { + if (admin) { + return <Fragment> + <NotificationCard notification={{ + message: 'No default instance', + description: 'in order to use merchant backend, you should create the default instance', + type: 'INFO' + }} /> + <InstanceCreatePage onError={() => null} onConfirm={() => null} /> + </Fragment> + } + return <NotFoundPage /> + }} + + onLoadError={(error: SwrError) => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR' }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} /> <Route path={InstancePaths.product_update} component={ProductUpdatePage} @@ -201,13 +314,34 @@ export function InstanceRoutes({ id, pushNotification, admin }: Props): VNode { <Route path={InstancePaths.order_list} component={OrderListPage} - onUnauthorized={() => <LoginPage - withMessage={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} - onConfirm={updateLoginStatus} />} - onLoadError={(error: SwrError) => <LoginPage - withMessage={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR', }} - onConfirm={updateLoginStatus} />} + onUnauthorized={() => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} + + onNotFound={() => { + if (admin) { + return <Fragment> + <NotificationCard notification={{ + message: 'No default instance', + description: 'in order to use merchant backend, you should create the default instance', + type: 'INFO' + }} /> + <InstanceCreatePage onError={() => null} onConfirm={() => null} /> + </Fragment> + } + return <NotFoundPage /> + }} + + onLoadError={(error: SwrError) => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR' }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} /> <Route path={InstancePaths.order_update} component={OrderUpdatePage} @@ -218,13 +352,34 @@ export function InstanceRoutes({ id, pushNotification, admin }: Props): VNode { <Route path={InstancePaths.tips_list} component={TipListPage} - onUnauthorized={() => <LoginPage - withMessage={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} - onConfirm={updateLoginStatus} />} - onLoadError={(error: SwrError) => <LoginPage - withMessage={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR', }} - onConfirm={updateLoginStatus} />} + onUnauthorized={() => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} + + onNotFound={() => { + if (admin) { + return <Fragment> + <NotificationCard notification={{ + message: 'No default instance', + description: 'in order to use merchant backend, you should create the default instance', + type: 'INFO' + }} /> + <InstanceCreatePage onError={() => null} onConfirm={() => null} /> + </Fragment> + } + return <NotFoundPage /> + }} + + onLoadError={(error: SwrError) => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR' }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} /> <Route path={InstancePaths.tips_update} component={TipUpdatePage} @@ -235,19 +390,42 @@ export function InstanceRoutes({ id, pushNotification, admin }: Props): VNode { <Route path={InstancePaths.transfers_list} component={TransferListPage} - onUnauthorized={() => <LoginPage - withMessage={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} - onConfirm={updateLoginStatus} />} - onLoadError={(error: SwrError) => <LoginPage - withMessage={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR', }} - onConfirm={updateLoginStatus} />} + onUnauthorized={() => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} + + onNotFound={() => { + if (admin) { + return <Fragment> + <NotificationCard notification={{ + message: 'No default instance', + description: 'in order to use merchant backend, you should create the default instance', + type: 'INFO' + }} /> + <InstanceCreatePage onError={() => null} onConfirm={() => null} /> + </Fragment> + } + return <NotFoundPage /> + }} + + onLoadError={(error: SwrError) => { + return <Fragment> + <NotificationCard notification={{ message: i18n`Problem reaching the server`, description: i18n`Got message: ${error.message} from: ${error.backend} (hasToken: ${error.hasToken})`, type: 'ERROR' }} /> + <LoginPage onConfirm={updateLoginStatus} /> + </Fragment> + }} /> <Route path={InstancePaths.transfers_new} component={TransferCreatePage} /> - <Route default component={NotFoundPage} /> + {/* example of loading page*/} + <Route path="/loading" component={Loading} /> + <Route default component={NotFoundPage} /> </Router> </InstanceContextProvider>; diff --git a/packages/frontend/src/components/exception/loading.tsx b/packages/frontend/src/components/exception/loading.tsx @@ -1,5 +1,9 @@ import { h, VNode } from "preact"; -export function Loading():VNode { - return <div>loading...</div> +export function Loading(): VNode { + return <div class="columns is-centered is-vcentered" style={{height:'calc(100% - 3rem)',position:'absolute', width:'100%'}}> + <div class="column is-one-fifth"> + <div class="lds-ring"><div></div><div></div><div></div><div></div></div> + </div> + </div> } \ No newline at end of file diff --git a/packages/frontend/src/components/exception/login.tsx b/packages/frontend/src/components/exception/login.tsx @@ -21,7 +21,7 @@ import { h, VNode } from "preact"; import { useMessageTemplate } from "preact-messages"; -import { useContext, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; import { useBackendContext } from "../../context/backend"; import { Notification } from "../../utils/types"; @@ -39,16 +39,6 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode { return <div class="columns is-centered"> <div class="column is-two-thirds " > <div class="modal-card" style={{ width: '100%', margin: 0 }}> - {withMessage && <div class={withMessage.type === 'ERROR' ? "notification is-danger" : "notification is-info"}> - <div class="columns is-vcentered"> - <div class="column is-12"> - <div> - <p><strong>{withMessage.message}</strong></p> - {withMessage.description} - </div> - </div> - </div> - </div>} <header class="modal-card-head" style={{ border: '1px solid', borderBottom: 0 }}> <p class="modal-card-title">{i18n`Login required`}</p> </header> diff --git a/packages/frontend/src/components/menu/index.tsx b/packages/frontend/src/components/menu/index.tsx @@ -14,91 +14,115 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - import { ComponentChildren, Fragment, h, VNode } from "preact"; - import { useEffect, useState } from "preact/hooks"; - import { AdminPaths } from "../../AdminRoutes"; - import { InstancePaths } from "../../InstanceRoutes"; - import { NavigationBar } from "./NavigationBar"; - import { Sidebar } from "./SideBar"; - import Match from 'preact-router/match'; - - - function getInstanceTitle(path: string, id: string): string { - - switch (path) { - case InstancePaths.details: return `${id}` - case InstancePaths.update: return `${id}: Settings` - case InstancePaths.order_list: return `${id}: Orders` - case InstancePaths.order_new: return `${id}: New order` - case InstancePaths.order_update: return `${id}: Update order` - case InstancePaths.product_list: return `${id}: Products` - case InstancePaths.product_new: return `${id}: New product` - case InstancePaths.product_update: return `${id}: Update product` - case InstancePaths.tips_list: return `${id}: Tips` - case InstancePaths.tips_new: return `${id}: New tip` - case InstancePaths.tips_update: return `${id}: Update tip` - case InstancePaths.transfers_list: return `${id}: Transfers` - case InstancePaths.transfers_new: return `${id}: New Transfer` - default: return ''; - } - } - - const INSTANCE_ID_LOOKUP = /^\/instance\/([^/]*)\// - function getAdminTitle(path: string) { - if (path === AdminPaths.new_instance) return `New instance` - if (path === AdminPaths.list_instances) return `Instances` - const match = INSTANCE_ID_LOOKUP.exec(path) - if (match && match[1]) return getInstanceTitle(path.replace(INSTANCE_ID_LOOKUP, '/'), match[1]); - return getInstanceTitle(path, 'default') - } - - interface MenuProps { - title?: string; - instance: string; - admin?: boolean; - onLogout?: () => void; - } - -function WithTitle({title,children}:{title:string, children:ComponentChildren}):VNode { +import { ComponentChildren, Fragment, h, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { AdminPaths } from "../../AdminRoutes"; +import { InstancePaths } from "../../InstanceRoutes"; +import { NavigationBar } from "./NavigationBar"; +import { Sidebar } from "./SideBar"; +import Match from 'preact-router/match'; +import { Notification } from "../../utils/types"; + + +function getInstanceTitle(path: string, id: string): string { + + switch (path) { + case InstancePaths.details: return `${id}` + case InstancePaths.update: return `${id}: Settings` + case InstancePaths.order_list: return `${id}: Orders` + case InstancePaths.order_new: return `${id}: New order` + case InstancePaths.order_update: return `${id}: Update order` + case InstancePaths.product_list: return `${id}: Products` + case InstancePaths.product_new: return `${id}: New product` + case InstancePaths.product_update: return `${id}: Update product` + case InstancePaths.tips_list: return `${id}: Tips` + case InstancePaths.tips_new: return `${id}: New tip` + case InstancePaths.tips_update: return `${id}: Update tip` + case InstancePaths.transfers_list: return `${id}: Transfers` + case InstancePaths.transfers_new: return `${id}: New Transfer` + default: return ''; + } +} + +const INSTANCE_ID_LOOKUP = /^\/instance\/([^/]*)\// +function getAdminTitle(path: string) { + if (path === AdminPaths.new_instance) return `New instance` + if (path === AdminPaths.list_instances) return `Instances` + const match = INSTANCE_ID_LOOKUP.exec(path) + if (match && match[1]) return getInstanceTitle(path.replace(INSTANCE_ID_LOOKUP, '/'), match[1]); + return getInstanceTitle(path, 'default') +} + +interface MenuProps { + title?: string; + instance: string; + admin?: boolean; + onLogout?: () => void; +} + +function WithTitle({ title, children }: { title: string, children: ComponentChildren }): VNode { useEffect(() => { document.title = `Taler Backoffice: ${title}` }, [title]) return <Fragment>{children}</Fragment> } - - export function Menu({ onLogout, title, instance, admin }: MenuProps): VNode { - const [mobileOpen, setMobileOpen] = useState(false) - - return <Match>{({ path }: any) => { - const titleWithSubtitle = title ? title : (!admin ? getInstanceTitle(path, instance) : getAdminTitle(path)) - - return (<WithTitle title={titleWithSubtitle}> - <div class={mobileOpen ? "has-aside-mobile-expanded" : ""} onClick={() => setMobileOpen(false)}> - <NavigationBar onMobileMenu={() => setMobileOpen(!mobileOpen)} title={titleWithSubtitle} /> - {onLogout && <Sidebar onLogout={onLogout} admin={admin} instance={instance} mobile={mobileOpen} />} - </div> - </WithTitle> - ) - }}</Match> - - } - - interface NotYetReadyAppMenuProps { - title: string; - onLogout?: () => void; - } - - export function NotYetReadyAppMenu({ onLogout, title }: NotYetReadyAppMenuProps): VNode { - const [mobileOpen, setMobileOpen] = useState(false) - - useEffect(() => { - document.title = `Taler Backoffice: ${title}` - }, [title]) - - return <div class={mobileOpen ? "has-aside-mobile-expanded" : ""} onClick={() => setMobileOpen(false)}> - <NavigationBar onMobileMenu={() => setMobileOpen(!mobileOpen)} title={title} /> - {onLogout && <Sidebar onLogout={onLogout} instance="" mobile={mobileOpen} />} - </div> - - } - + +export function Menu({ onLogout, title, instance, admin }: MenuProps): VNode { + const [mobileOpen, setMobileOpen] = useState(false) + + return <Match>{({ path }: any) => { + const titleWithSubtitle = title ? title : (!admin ? getInstanceTitle(path, instance) : getAdminTitle(path)) + + return (<WithTitle title={titleWithSubtitle}> + <div class={mobileOpen ? "has-aside-mobile-expanded" : ""} onClick={() => setMobileOpen(false)}> + <NavigationBar onMobileMenu={() => setMobileOpen(!mobileOpen)} title={titleWithSubtitle} /> + {onLogout && <Sidebar onLogout={onLogout} admin={admin} instance={instance} mobile={mobileOpen} />} + </div> + </WithTitle> + ) + }}</Match> + +} + +interface NotYetReadyAppMenuProps { + title: string; + onLogout?: () => void; +} + +interface NotifProps { + notification?: Notification; +} +export function NotificationCard({ notification:n }: NotifProps) { + // const [n, setNotif] = useState(notification) + if (!n) return null + return <div class="notification"> + <div class="columns is-vcentered"> + <div class="column is-12"> + <article class={n.type === 'ERROR' ? "message is-danger" : "message is-info"}> + <div class="message-header"> + <p>{n.message}</p> + {/* {n.type !== 'ERROR' && <button class="delete" aria-label="delete" onClick={() => setNotif(undefined)}></button> } */} + </div> + <div class="message-body"> + {n.description} + </div> + </article> + </div> + </div> + </div> +} + +export function NotYetReadyAppMenu({ onLogout, title }: NotYetReadyAppMenuProps): VNode { + const [mobileOpen, setMobileOpen] = useState(false) + + useEffect(() => { + document.title = `Taler Backoffice: ${title}` + }, [title]) + + return <div class={mobileOpen ? "has-aside-mobile-expanded" : ""} onClick={() => setMobileOpen(false)}> + <NavigationBar onMobileMenu={() => setMobileOpen(!mobileOpen)} title={title} /> + {onLogout && <Sidebar onLogout={onLogout} instance="" mobile={mobileOpen} />} + </div> + +} + diff --git a/packages/frontend/src/hooks/backend.ts b/packages/frontend/src/hooks/backend.ts @@ -23,6 +23,7 @@ import useSWR, { mutate, cache } from 'swr'; import axios from 'axios' import { MerchantBackend } from '../declaration'; import { useBackendContext, useInstanceContext } from '../context/backend'; +import { useEffect, useState } from 'preact/hooks'; function mutateAll(re: RegExp) { cache.keys().filter(key => re.test(key)).forEach(key => mutate(key, null)) @@ -32,6 +33,8 @@ type HttpResponse<T> = HttpResponseOk<T> | HttpResponseError; interface HttpResponseOk<T> { data: T; + unauthorized: boolean; + notfound: boolean; } export interface SwrError { @@ -107,6 +110,7 @@ interface AdminMutateAPI { createInstance: (data: MerchantBackend.Instances.InstanceConfigurationMessage) => Promise<void>; deleteInstance: (id: string) => Promise<void>; } + export function useAdminMutateAPI(): AdminMutateAPI { const { url, token } = useBackendContext() @@ -121,7 +125,7 @@ export function useAdminMutateAPI(): AdminMutateAPI { } const deleteInstance = async (id: string): Promise<void> => { - await request(`${url}/private/`, { + await request(`${url}/private/instances/${id}`, { method: 'delete', token, }) @@ -395,6 +399,26 @@ export function useInstanceMutateAPI(): InstaceMutateAPI { return { updateInstance, deleteInstance, setNewToken, clearToken } } +export function useBackendInstancesTestForAdmin(): HttpResponse<MerchantBackend.Instances.InstancesResponse> { + const { url, token } = useBackendContext() + interface Result { + data?: MerchantBackend.Instances.InstancesResponse; + error?: SwrError; + } + const [result, setResult] = useState<Result|undefined>(undefined) + + useEffect(() => { + request(`${url}/private/instances`, { token }) + .then(data => setResult({data})) + .catch(error => setResult({error})) + },[url, token]) + + const data = result?.data + const error = result?.error + + return { data, unauthorized: error?.status === 401, notfound: error?.status === 404, error } +} + export function useBackendInstances(): HttpResponse<MerchantBackend.Instances.InstancesResponse> { const { url, token } = useBackendContext() const { data, error } = useSWR<MerchantBackend.Instances.InstancesResponse, SwrError>(['/private/instances', token, url], fetcher) diff --git a/packages/frontend/src/hooks/notification.ts b/packages/frontend/src/hooks/notification.ts @@ -0,0 +1,43 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { useCallback, useState } from "preact/hooks"; +import { Notification } from '../utils/types'; + +interface Result { + notification?: Notification; + pushNotification: (n: Notification) => void; + removeNotification: () => void; +} + +export function useNotification(): Result { + const [notification, setNotifications] = useState<Notification|undefined>(undefined) + + const pushNotification = useCallback((n: Notification): void => { + setNotifications(n) + },[]) + + const removeNotification = useCallback(() => { + setNotifications(undefined) + },[]) + + return { notification, pushNotification, removeNotification } +} diff --git a/packages/frontend/src/index.tsx b/packages/frontend/src/index.tsx @@ -22,21 +22,19 @@ import "./scss/main.scss" import { h, VNode } from 'preact'; -import { useCallback, useContext, useEffect, useMemo, useState } from "preact/hooks"; -import { Route, route } from 'preact-router'; +import { useEffect, useMemo } from "preact/hooks"; +import { route } from 'preact-router'; import { MessageProvider, useMessageTemplate } from 'preact-messages'; -import { Notifications } from './components/notifications'; import * as messages from './messages' import { useBackendContextState } from './hooks'; -import { useNotifications } from "./hooks/notifications"; import { BackendContextProvider, ConfigContextProvider, useBackendContext } from './context/backend'; import { useBackendConfig } from "./hooks/backend"; import { hasKey, onTranslationError } from "./utils/functions"; import LoginPage from './paths/login'; import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes"; -import { NotYetReadyAppMenu } from "./components/menu"; +import { NotificationCard, NotYetReadyAppMenu } from "./components/menu"; export function Redirect({ to }: { to: string }): null { useEffect(() => { @@ -58,7 +56,6 @@ export default function Application(): VNode { } function ApplicationStatusRoutes(): VNode { - const { notifications, pushNotification, removeNotification } = useNotifications() const { changeBackend, triedToLog, updateToken, resetBackend } = useBackendContext() const backendConfig = useBackendConfig(); const i18n = useMessageTemplate() @@ -66,16 +63,16 @@ function ApplicationStatusRoutes(): VNode { const v = `${backendConfig.data?.currency} ${backendConfig.data?.version}` const ctx = useMemo(() => ({ currency: backendConfig.data?.currency || '', version: backendConfig.data?.version || '' }), [v]) + const updateLoginInfoAndGoToRoot = (url: string, token?: string) => { + changeBackend(url) + if (token) updateToken(token) + route('/') + } + if (!triedToLog) { return <div id="app"> <NotYetReadyAppMenu title="Welcome!" /> - <LoginPage - onConfirm={(url: string, token?: string) => { - changeBackend(url) - if (token) updateToken(token) - route('/') - }} - /> + <LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> </div> } @@ -86,37 +83,24 @@ function ApplicationStatusRoutes(): VNode { if (backendConfig.unauthorized) { return <div id="app"> <NotYetReadyAppMenu title="Login" /> - <LoginPage - onConfirm={(url: string, token?: string) => { - changeBackend(url) - if (token) updateToken(token) - route('/') - }} - /> + <LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> </div> } return <div id="app"> <NotYetReadyAppMenu title="Error" /> - <LoginPage - withMessage={{ - message: i18n`Couldnt access the server`, - type: 'ERROR', - description: i18n`Got message: ${backendConfig.error.message} from: ${backendConfig.error.backend} (hasToken: ${backendConfig.error.hasToken})`, - }} - onConfirm={(url: string, token?: string) => { - changeBackend(url) - if (token) updateToken(token) - route('/') - }} - /> + <NotificationCard notification={{ + message: i18n`Couldnt access the server`, + type: 'ERROR', + description: i18n`Got message: ${backendConfig.error.message} from: ${backendConfig.error.backend} (hasToken: ${backendConfig.error.hasToken})`, + }} /> + <LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> </div> } return <div id="app" class="has-navbar-fixed-top"> <ConfigContextProvider value={ctx}> - <Notifications notifications={notifications} removeNotification={removeNotification} /> - <Route default component={ApplicationReadyRoutes} pushNotification={pushNotification} /> + <ApplicationReadyRoutes /> </ConfigContextProvider> </div> } diff --git a/packages/frontend/src/messages/en.po b/packages/frontend/src/messages/en.po @@ -276,4 +276,10 @@ msgstr "" # msgstr "" msgid "fields.instance.wired.label" -msgstr "Wired" -\ No newline at end of file +msgstr "Wired" + +msgid "create_success" +msgstr "Creation succeed" + +msgid "create_error" +msgstr "Creation failed" diff --git a/packages/frontend/src/paths/admin/create/CreatePage.tsx b/packages/frontend/src/paths/admin/create/CreatePage.tsx @@ -40,7 +40,7 @@ type Entity = MerchantBackend.Instances.InstanceConfigurationMessage & {auth_tok interface Props { onCreate: (d: Entity) => void; isLoading: boolean; - onBack: () => void; + onBack?: () => void; } interface KeyValue { @@ -114,7 +114,7 @@ export function CreatePage({ onCreate, isLoading, onBack }: Props): VNode { </FormProvider> <div class="buttons is-right mt-5"> - <button class="button" onClick={onBack} ><Message id="Cancel" /></button> + { onBack && <button class="button" onClick={onBack} ><Message id="Cancel" /></button> } <button class="button is-success" onClick={submit} ><Message id="Confirm" /></button> </div> diff --git a/packages/frontend/src/paths/admin/create/index.tsx b/packages/frontend/src/paths/admin/create/index.tsx @@ -13,24 +13,112 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { h, VNode } from "preact"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../components/exception/loading"; +import { FormProvider } from "../../../components/form/Field"; +import { Input } from "../../../components/form/Input"; +import { NotificationCard } from "../../../components/menu"; import { MerchantBackend } from "../../../declaration"; import { useAdminMutateAPI } from "../../../hooks/backend"; +import { Notification } from "../../../utils/types"; import { CreatePage } from "./CreatePage"; interface Props { - onBack: () => void; + onBack?: () => void; onConfirm: () => void; onError: (error: any) => void; } +type Entity = MerchantBackend.Instances.InstanceConfigurationMessage; export default function Create({ onBack, onConfirm, onError }: Props): VNode { const { createInstance } = useAdminMutateAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined) + const [createdOk, setCreatedOk] = useState<Entity | undefined>(undefined); - return <CreatePage - onBack={onBack} - isLoading={false} - onCreate={(d: MerchantBackend.Instances.InstanceConfigurationMessage): Promise<void> => { - return createInstance(d).then(onConfirm).catch(onError) - }} /> + if (createdOk) { + return <div class="columns is-fullwidth is-vcentered content-full-size"> + <div class="column" /> + <div class="column is-half"> + <div class="card"> + <header class="card-header has-background-success"> + <p class="card-header-title has-text-white-ter"> + Instance created successfully + </p> + </header> + <div class="card-content"> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">ID</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={createdOk.id} disabled /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Business Name</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={createdOk.name} disabled /> + </p> + </div> + </div> + </div> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label">Token</label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class="control"> + <input class="input" readonly value={createdOk.auth.token} disabled /> + </p> + </div> + </div> + </div> + </div> + <footer class="card-footer"> + <p class="card-footer-item" style={{ border: 'none' }}> + <span> + </span> + </p> + <p class="card-footer-item" style={{ border: 'none' }}> + <span> + </span> + </p> + <p class="card-footer-item"> + <button class="button is-info" onClick={onConfirm}>Continue</button> + </p> + </footer> + </div> + </div> + <div class="column" /> + </div> + } + + return <Fragment> + <NotificationCard notification={notif} /> + + <CreatePage + onBack={onBack} + isLoading={false} + onCreate={(d: MerchantBackend.Instances.InstanceConfigurationMessage) => { + createInstance(d).then((r) => { + setCreatedOk(d) + }).catch((error) => { + setNotif({ + message: 'could not create instance', + type: "ERROR", + description: error.message + }) + }) + }} /> + </Fragment> } \ No newline at end of file diff --git a/packages/frontend/src/paths/admin/list/index.tsx b/packages/frontend/src/paths/admin/list/index.tsx @@ -26,9 +26,10 @@ import { useState } from 'preact/hooks'; import { MerchantBackend } from '../../../declaration'; import { Notification } from '../../../utils/types'; import { DeleteModal } from '../../../components/modal'; +import { Loading } from '../../../components/exception/loading'; interface Props { - pushNotification: (n: Notification) => void; + // pushNotification: (n: Notification) => void; onCreate: () => void; onUpdate: (id: string) => void; instances: MerchantBackend.Instances.Instance[]; @@ -36,17 +37,15 @@ interface Props { onLoadError: (e: SwrError) => VNode; } -export default function Instances({ pushNotification, onUnauthorized, onLoadError, onCreate, onUpdate }: Props): VNode { +export default function Instances({ onUnauthorized, onLoadError, onCreate, onUpdate }: Props): VNode { const result = useBackendInstances() const [deleting, setDeleting] = useState<MerchantBackend.Instances.Instance | null>(null) const { deleteInstance } = useAdminMutateAPI() + if (result.unauthorized) return onUnauthorized() if (!result.data) { - if (result.unauthorized) return onUnauthorized() if (result.error) return onLoadError(result.error) - return <div> - loading .... - </div> + return <Loading /> } return <Fragment> @@ -63,9 +62,9 @@ export default function Instances({ pushNotification, onUnauthorized, onLoadErro onConfirm={async (): Promise<void> => { try { await deleteInstance(deleting.id) - pushNotification({ message: 'delete_success', type: 'SUCCESS' }) + // pushNotification({ message: 'delete_success', type: 'SUCCESS' }) } catch (e) { - pushNotification({ message: 'delete_error', type: 'ERROR' }) + // pushNotification({ message: 'delete_error', type: 'ERROR' }) } setDeleting(null) }} diff --git a/packages/frontend/src/paths/instance/details/index.tsx b/packages/frontend/src/paths/instance/details/index.tsx @@ -20,28 +20,28 @@ import { Notification } from "../../../utils/types"; import { useInstanceDetails, useInstanceMutateAPI, SwrError } from "../../../hooks/backend"; import { DetailPage } from "./DetailPage"; import { DeleteModal } from "../../../components/modal"; +import { Loading } from "../../../components/exception/loading"; interface Props { onUnauthorized: () => VNode; onLoadError: (e: SwrError) => VNode; onUpdate: () => void; + onNotFound: () => VNode; onDelete: () => void; - pushNotification: (n: Notification) => void; } -export default function Detail({ onUpdate, onLoadError, onUnauthorized, pushNotification, onDelete }: Props): VNode { +export default function Detail({ onUpdate, onLoadError, onUnauthorized, onDelete, onNotFound }: Props): VNode { const { id } = useInstanceContext() const result = useInstanceDetails() const [deleting, setDeleting] = useState<boolean>(false) const { deleteInstance } = useInstanceMutateAPI() + if (result.unauthorized) return onUnauthorized() + if (result.notfound) return onNotFound(); if (!result.data) { - if (result.unauthorized) return onUnauthorized() if (result.error) return onLoadError(result.error) - return <div> - loading .... - </div> + return <Loading /> } return <Fragment> @@ -56,10 +56,10 @@ export default function Detail({ onUpdate, onLoadError, onUnauthorized, pushNoti onConfirm={async (): Promise<void> => { try { await deleteInstance() - pushNotification({ message: 'delete_success', type: 'SUCCESS' }) + // pushNotification({ message: 'delete_success', type: 'SUCCESS' }) onDelete() } catch (error) { - pushNotification({ message: 'delete_error', type: 'ERROR' }) + // pushNotification({ message: 'delete_error', type: 'ERROR' }) } setDeleting(false) }} diff --git a/packages/frontend/src/paths/instance/orders/list/index.tsx b/packages/frontend/src/paths/instance/orders/list/index.tsx @@ -10,6 +10,7 @@ import { InputBoolean } from "../../../../components/form/InputBoolean"; interface Props { onUnauthorized: () => VNode; onLoadError: (e: SwrError) => VNode; + onNotFound: () => VNode; onCreate: () => void; } @@ -18,7 +19,7 @@ const fromBooleanToYesAndNo = { toBoolean: (b: string) => b === 'yes' ? true : (b === 'no' ? false : undefined) } -export default function ({ onUnauthorized, onLoadError, onCreate }: Props): VNode { +export default function ({ onUnauthorized, onLoadError, onCreate, onNotFound }: Props): VNode { const [filter, setFilter] = useState<InstanceOrderFilter>({}) const result = useInstanceOrders(filter) const { createOrder, deleteOrder } = useOrderMutateAPI() @@ -26,8 +27,9 @@ export default function ({ onUnauthorized, onLoadError, onCreate }: Props): VNod let instances: (MerchantBackend.Orders.OrderHistoryEntry & {id: string})[]; + if (result.unauthorized) return onUnauthorized() + if (result.notfound) return onNotFound() if (!result.data) { - if (result.unauthorized) return onUnauthorized() if (result.error) return onLoadError(result.error) //if loading asume empty list instances = [] diff --git a/packages/frontend/src/paths/instance/products/list/index.tsx b/packages/frontend/src/paths/instance/products/list/index.tsx @@ -5,21 +5,24 @@ import { CardTable } from './Table'; import logo from '../../../../assets/logo.jpeg'; import { useConfigContext } from '../../../../context/backend'; import { MerchantBackend } from '../../../../declaration'; +import { Loading } from '../../../../components/exception/loading'; interface Props { onUnauthorized: () => VNode; + onNotFound: () => VNode; onLoadError: (e: SwrError) => VNode; } -export default function ({ onUnauthorized, onLoadError }: Props): VNode { +export default function ({ onUnauthorized, onLoadError, onNotFound }: Props): VNode { const result = useInstanceProducts() const { createProduct, deleteProduct } = useProductMutateAPI() const { currency } = useConfigContext() + + if (result.unauthorized) return onUnauthorized() + if (result.notfound) return onNotFound() + if (!result.data) { - if (result.unauthorized) return onUnauthorized() if (result.error) return onLoadError(result.error) - return <div> - loading .... - </div> + return <Loading /> } return <section class="section is-main-section"> <CardTable instances={result.data.products.map(o => ({ ...o, id: o.product_id }))} diff --git a/packages/frontend/src/paths/instance/tips/list/index.tsx b/packages/frontend/src/paths/instance/tips/list/index.tsx @@ -1,4 +1,5 @@ import { h, VNode } from 'preact'; +import { Loading } from '../../../../components/exception/loading'; import { useConfigContext } from '../../../../context/backend'; import { MerchantBackend } from '../../../../declaration'; import { SwrError, useInstanceMutateAPI, useInstanceTips, useTipsMutateAPI } from '../../../../hooks/backend'; @@ -7,18 +8,21 @@ import { CardTable } from './Table'; interface Props { onUnauthorized: () => VNode; onLoadError: (e: SwrError) => VNode; + onNotFound: () => VNode; } -export default function ({ onUnauthorized, onLoadError }: Props): VNode { +export default function ({ onUnauthorized, onLoadError, onNotFound }: Props): VNode { const result = useInstanceTips() const { createReserve, deleteReserve } = useTipsMutateAPI() const { currency } = useConfigContext() + + if (result.unauthorized) return onUnauthorized() + if (result.notfound) return onNotFound() + if (!result.data) { - if (result.unauthorized) return onUnauthorized() if (result.error) return onLoadError(result.error) - return <div> - loading .... - </div> + return <Loading /> } + return <section class="section is-main-section"> <CardTable instances={result.data.reserves.filter(r => r.active).map(o => ({ ...o, id: o.reserve_pub }))} onCreate={() => createReserve({ diff --git a/packages/frontend/src/paths/instance/transfers/list/index.tsx b/packages/frontend/src/paths/instance/transfers/list/index.tsx @@ -1,4 +1,5 @@ import { h, VNode } from 'preact'; +import { Loading } from '../../../../components/exception/loading'; import { useConfigContext } from '../../../../context/backend'; import { SwrError, useInstanceTransfers, useTransferMutateAPI } from '../../../../hooks/backend'; import { CardTable } from './Table'; @@ -6,17 +7,19 @@ import { CardTable } from './Table'; interface Props { onUnauthorized: () => VNode; onLoadError: (e: SwrError) => VNode; + onNotFound: () => VNode; } -export default function ({ onUnauthorized, onLoadError }: Props): VNode { +export default function ({ onUnauthorized, onLoadError, onNotFound }: Props): VNode { const result = useInstanceTransfers() const { informTransfer } = useTransferMutateAPI() const { currency } = useConfigContext() + + if (result.unauthorized) return onUnauthorized() + if (result.notfound) return onNotFound(); + if (!result.data) { - if (result.unauthorized) return onUnauthorized() if (result.error) return onLoadError(result.error) - return <div> - loading .... - </div> + return <Loading /> } return <section class="section is-main-section"> <CardTable instances={result.data.transfers.map(o => ({ ...o, id: String(o.transfer_serial_id) }))} diff --git a/packages/frontend/src/paths/instance/update/index.tsx b/packages/frontend/src/paths/instance/update/index.tsx @@ -15,33 +15,34 @@ */ import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; +import { Loading } from "../../../components/exception/loading"; import { UpdateTokenModal } from "../../../components/modal"; import { useInstanceContext } from "../../../context/backend"; import { MerchantBackend } from "../../../declaration"; import { SwrError, useInstanceDetails, useInstanceMutateAPI } from "../../../hooks/backend"; import { UpdatePage } from "./UpdatePage"; -interface Props { +export interface Props { onBack: () => void; onConfirm: () => void; - pushNotification: (n: Notification) => void; onUnauthorized: () => VNode; + onNotFound: () => VNode; onLoadError: (e: SwrError) => VNode; onUpdateError: (e: Error) => void; } -export default function Update({ onBack, onConfirm, onLoadError, onUpdateError, onUnauthorized }: Props): VNode { +export default function Update({ onBack, onConfirm, onLoadError, onNotFound, onUpdateError, onUnauthorized }: Props): VNode { const { updateInstance } = useInstanceMutateAPI(); const details = useInstanceDetails() + if (details.unauthorized) return onUnauthorized() + if (details.notfound) return onNotFound(); + if (!details.data) { - if (details.unauthorized) return onUnauthorized() if (details.error) return onLoadError(details.error) - return <div> - loading .... - </div> + return <Loading /> } return <Fragment> diff --git a/packages/frontend/src/paths/login/index.tsx b/packages/frontend/src/paths/login/index.tsx @@ -23,9 +23,8 @@ import { LoginModal } from '../../components/exception/login'; import { Notification } from "../../utils/types"; interface Props { - withMessage?: Notification; onConfirm: (url: string, token?: string) => void; } -export default function LoginPage({ onConfirm, withMessage }: Props): VNode { - return <LoginModal onConfirm={onConfirm} withMessage={withMessage} /> +export default function LoginPage({ onConfirm }: Props): VNode { + return <LoginModal onConfirm={onConfirm} /> } \ No newline at end of file diff --git a/packages/frontend/src/scss/main.scss b/packages/frontend/src/scss/main.scss @@ -93,6 +93,10 @@ input[type=checkbox]:indeterminate + .check { background-color: $white; } +.right-sticky .buttons { + flex-wrap: nowrap +} + .table.is-striped tbody tr:not(.is-selected):nth-child(even) .right-sticky { background-color: #fafafa; } @@ -103,3 +107,64 @@ tr:hover .right-sticky { .table.is-striped tbody tr:nth-child(even):hover .right-sticky { background-color: hsl(0, 0%, 95%); } + +.lds-ring { + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} +.lds-ring div { + box-sizing: border-box; + display: block; + position: absolute; + width: 64px; + height: 64px; + margin: 8px; + border: 8px solid black; + border-radius: 50%; + animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + border-color: black transparent transparent transparent; +} +.lds-ring div:nth-child(1) { + animation-delay: -0.45s; +} +.lds-ring div:nth-child(2) { + animation-delay: -0.3s; +} +.lds-ring div:nth-child(3) { + animation-delay: -0.15s; +} +@keyframes lds-ring { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.content-full-size { + height: calc(100% - 3rem); + position: absolute; + width: calc(100% - 14rem); + display:flex; +} + +.content-full-size .column .card { + min-width: 200px; + +} + +@include touch { + .content-full-size { + height: 100%; + position: absolute; + width: 100%; + } +} + +.column.is-half { + flex: none; + width: 50%; +} +\ No newline at end of file