remove webui from login url, ad qr for template, fix navbar size,

This commit is contained in:
Sebastian 2023-03-10 01:25:22 -03:00
parent 2291d460e8
commit 8ddc551cc8
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
14 changed files with 343 additions and 71 deletions

View File

@ -25,6 +25,7 @@ serve({
folder: './dist', folder: './dist',
port: 8080, port: 8080,
source: './src', source: './src',
insecure: true,
development: true, development: true,
onUpdate: async () => esbuild.build(buildConfig) onUpdate: async () => esbuild.build(buildConfig)
}) })

View File

@ -23,7 +23,7 @@ import {
TranslationProvider, TranslationProvider,
useTranslationContext, useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser"; } from "@gnu-taler/web-util/lib/index.browser";
import { h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { route } from "preact-router"; import { route } from "preact-router";
import { useMemo } from "preact/hooks"; import { useMemo } from "preact/hooks";
import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js"; import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js";
@ -70,24 +70,24 @@ function ApplicationStatusRoutes(): VNode {
if (!triedToLog) { if (!triedToLog) {
return ( return (
<div id="app"> <Fragment>
<NotYetReadyAppMenu title="Welcome!" /> <NotYetReadyAppMenu title="Welcome!" />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
</div> </Fragment>
); );
} }
if (result.clientError && result.isUnauthorized) if (result.clientError && result.isUnauthorized)
return ( return (
<div id="app"> <Fragment>
<NotYetReadyAppMenu title="Login" /> <NotYetReadyAppMenu title="Login" />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
</div> </Fragment>
); );
if (result.clientError && result.isNotfound) if (result.clientError && result.isNotfound)
return ( return (
<div id="app"> <Fragment>
<NotYetReadyAppMenu title="Error" /> <NotYetReadyAppMenu title="Error" />
<NotificationCard <NotificationCard
notification={{ notification={{
@ -97,12 +97,12 @@ function ApplicationStatusRoutes(): VNode {
}} }}
/> />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
</div> </Fragment>
); );
if (result.serverError) if (result.serverError)
return ( return (
<div id="app"> <Fragment>
<NotYetReadyAppMenu title="Error" /> <NotYetReadyAppMenu title="Error" />
<NotificationCard <NotificationCard
notification={{ notification={{
@ -112,14 +112,14 @@ function ApplicationStatusRoutes(): VNode {
}} }}
/> />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
</div> </Fragment>
); );
if (result.loading) return <Loading />; if (result.loading) return <Loading />;
if (!result.ok) if (!result.ok)
return ( return (
<div id="app"> <Fragment>
<NotYetReadyAppMenu title="Error" /> <NotYetReadyAppMenu title="Error" />
<NotificationCard <NotificationCard
notification={{ notification={{
@ -129,7 +129,7 @@ function ApplicationStatusRoutes(): VNode {
}} }}
/> />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
</div> </Fragment>
); );
return ( return (

View File

@ -52,6 +52,7 @@ import ReservesDetailsPage from "./paths/instance/reserves/details/index.js";
import ReservesListPage from "./paths/instance/reserves/list/index.js"; import ReservesListPage from "./paths/instance/reserves/list/index.js";
import TemplateCreatePage from "./paths/instance/templates/create/index.js"; import TemplateCreatePage from "./paths/instance/templates/create/index.js";
import TemplateUsePage from "./paths/instance/templates/use/index.js"; import TemplateUsePage from "./paths/instance/templates/use/index.js";
import TemplateQrPage from "./paths/instance/templates/qr/index.js";
import TemplateListPage from "./paths/instance/templates/list/index.js"; import TemplateListPage from "./paths/instance/templates/list/index.js";
import TemplateUpdatePage from "./paths/instance/templates/update/index.js"; import TemplateUpdatePage from "./paths/instance/templates/update/index.js";
import WebhookCreatePage from "./paths/instance/webhooks/create/index.js"; import WebhookCreatePage from "./paths/instance/webhooks/create/index.js";
@ -94,6 +95,7 @@ export enum InstancePaths {
templates_update = "/templates/:tid/update", templates_update = "/templates/:tid/update",
templates_new = "/templates/new", templates_new = "/templates/new",
templates_use = "/templates/:tid/use", templates_use = "/templates/:tid/use",
templates_qr = "/templates/:tid/qr",
webhooks_list = "/webhooks", webhooks_list = "/webhooks",
webhooks_update = "/webhooks/:tid/update", webhooks_update = "/webhooks/:tid/update",
@ -465,6 +467,9 @@ export function InstanceRoutes({
onNewOrder={(id: string) => { onNewOrder={(id: string) => {
route(InstancePaths.templates_use.replace(":tid", id)); route(InstancePaths.templates_use.replace(":tid", id));
}} }}
onQR={(id: string) => {
route(InstancePaths.templates_qr.replace(":tid", id));
}}
onSelect={(id: string) => { onSelect={(id: string) => {
route(InstancePaths.templates_update.replace(":tid", id)); route(InstancePaths.templates_update.replace(":tid", id));
}} }}
@ -505,6 +510,16 @@ export function InstanceRoutes({
route(InstancePaths.templates_list); route(InstancePaths.templates_list);
}} }}
/> />
<Route
path={InstancePaths.templates_qr}
component={TemplateQrPage}
onUnauthorized={LoginPageAccessDenied}
onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onBack={() => {
route(InstancePaths.templates_list);
}}
/>
{/** {/**
* reserves pages * reserves pages

View File

@ -42,6 +42,14 @@ function normalizeToken(r: string | undefined): string | undefined {
return r ? `secret-token:${encodeURIComponent(r)}` : undefined; return r ? `secret-token:${encodeURIComponent(r)}` : undefined;
} }
function cleanUp(s: string): string {
let result = s;
if (result.indexOf("webui/") !== -1) {
result = result.substring(0, result.indexOf("webui/"));
}
return result;
}
export function LoginModal({ onConfirm, withMessage }: Props): VNode { export function LoginModal({ onConfirm, withMessage }: Props): VNode {
const { url: backendUrl, token: baseToken } = useBackendContext(); const { url: backendUrl, token: baseToken } = useBackendContext();
const { admin, token: instanceToken } = useInstanceContext(); const { admin, token: instanceToken } = useInstanceContext();
@ -50,11 +58,11 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
); );
const [token, setToken] = useState(currentToken); const [token, setToken] = useState(currentToken);
const [url, setURL] = useState(backendUrl); const [url, setURL] = useState(cleanUp(backendUrl));
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
return ( return (
<div class="columns is-centered"> <div class="columns is-centered" style={{ margin: "auto" }}>
<div class="column is-two-thirds "> <div class="column is-two-thirds ">
<div class="modal-card" style={{ width: "100%", margin: 0 }}> <div class="modal-card" style={{ width: "100%", margin: 0 }}>
<header <header

View File

@ -61,7 +61,7 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode {
class="navbar-start is-justify-content-center is-flex-grow-1" class="navbar-start is-justify-content-center is-flex-grow-1"
href="https://taler.net" href="https://taler.net"
> >
<img src={logo} style={{ height: 50, margin: 10 }} /> <img src={logo} style={{ height: 35, margin: 10 }} />
</a> </a>
<div class="navbar-end"> <div class="navbar-end">
<div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}> <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>

View File

@ -57,56 +57,54 @@ export function View({
: instances; : instances;
return ( return (
<div id="app"> <section class="section is-main-section">
<section class="section is-main-section"> <div class="columns">
<div class="columns"> <div class="column is-two-thirds">
<div class="column is-two-thirds"> <div class="tabs" style={{ overflow: "inherit" }}>
<div class="tabs" style={{ overflow: "inherit" }}> <ul>
<ul> <li class={showIsActive}>
<li class={showIsActive}> <div
<div class="has-tooltip-right"
class="has-tooltip-right" data-tooltip={i18n.str`Only show active instances`}
data-tooltip={i18n.str`Only show active instances`} >
> <a onClick={() => setShow("active")}>
<a onClick={() => setShow("active")}> <i18n.Translate>Active</i18n.Translate>
<i18n.Translate>Active</i18n.Translate> </a>
</a> </div>
</div> </li>
</li> <li class={showIsDeleted}>
<li class={showIsDeleted}> <div
<div class="has-tooltip-right"
class="has-tooltip-right" data-tooltip={i18n.str`Only show deleted instances`}
data-tooltip={i18n.str`Only show deleted instances`} >
> <a onClick={() => setShow("deleted")}>
<a onClick={() => setShow("deleted")}> <i18n.Translate>Deleted</i18n.Translate>
<i18n.Translate>Deleted</i18n.Translate> </a>
</a> </div>
</div> </li>
</li> <li class={showAll}>
<li class={showAll}> <div
<div class="has-tooltip-right"
class="has-tooltip-right" data-tooltip={i18n.str`Show all instances`}
data-tooltip={i18n.str`Show all instances`} >
> <a onClick={() => setShow(null)}>
<a onClick={() => setShow(null)}> <i18n.Translate>All</i18n.Translate>
<i18n.Translate>All</i18n.Translate> </a>
</a> </div>
</div> </li>
</li> </ul>
</ul>
</div>
</div> </div>
</div> </div>
<CardTableActive </div>
instances={showingInstances} <CardTableActive
onDelete={onDelete} instances={showingInstances}
onPurge={onPurge} onDelete={onDelete}
setInstanceName={setInstanceName} onPurge={onPurge}
onUpdate={onUpdate} setInstanceName={setInstanceName}
selected={selected} onUpdate={onUpdate}
onCreate={onCreate} selected={selected}
/> onCreate={onCreate}
</section> />
</div> </section>
); );
} }

View File

@ -114,13 +114,13 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
<Input <Input
name="template_contract.summary" name="template_contract.summary"
inputType="multiline" inputType="multiline"
label={i18n.str`Order summary`} label={i18n.str`Fixed summary`}
tooltip={i18n.str`Title of the order to be shown to the customer`} tooltip={i18n.str`If specified, this template will create order with the same summary`}
/> />
<InputCurrency <InputCurrency
name="template_contract.amount" name="template_contract.amount"
label={i18n.str`Order price`} label={i18n.str`Fixed price`}
tooltip={i18n.str`Order price`} tooltip={i18n.str`If specified, this template will create order with the same price`}
/> />
<InputNumber <InputNumber
name="template_contract.minimum_age" name="template_contract.minimum_age"

View File

@ -32,6 +32,7 @@ export interface Props {
onDelete: (e: MerchantBackend.Template.TemplateEntry) => void; onDelete: (e: MerchantBackend.Template.TemplateEntry) => void;
onSelect: (e: MerchantBackend.Template.TemplateEntry) => void; onSelect: (e: MerchantBackend.Template.TemplateEntry) => void;
onNewOrder: (e: MerchantBackend.Template.TemplateEntry) => void; onNewOrder: (e: MerchantBackend.Template.TemplateEntry) => void;
onQR: (e: MerchantBackend.Template.TemplateEntry) => void;
} }
export function ListPage({ export function ListPage({
@ -40,6 +41,7 @@ export function ListPage({
onDelete, onDelete,
onSelect, onSelect,
onNewOrder, onNewOrder,
onQR,
onLoadMoreBefore, onLoadMoreBefore,
onLoadMoreAfter, onLoadMoreAfter,
}: Props): VNode { }: Props): VNode {
@ -53,6 +55,7 @@ export function ListPage({
...o, ...o,
id: String(o.template_id), id: String(o.template_id),
}))} }))}
onQR={onQR}
onCreate={onCreate} onCreate={onCreate}
onDelete={onDelete} onDelete={onDelete}
onSelect={onSelect} onSelect={onSelect}

View File

@ -31,6 +31,7 @@ interface Props {
onDelete: (e: Entity) => void; onDelete: (e: Entity) => void;
onSelect: (e: Entity) => void; onSelect: (e: Entity) => void;
onNewOrder: (e: Entity) => void; onNewOrder: (e: Entity) => void;
onQR: (e: Entity) => void;
onCreate: () => void; onCreate: () => void;
onLoadMoreBefore?: () => void; onLoadMoreBefore?: () => void;
hasMoreBefore?: boolean; hasMoreBefore?: boolean;
@ -43,6 +44,7 @@ export function CardTable({
onCreate, onCreate,
onDelete, onDelete,
onSelect, onSelect,
onQR,
onNewOrder, onNewOrder,
onLoadMoreAfter, onLoadMoreAfter,
onLoadMoreBefore, onLoadMoreBefore,
@ -84,6 +86,7 @@ export function CardTable({
onDelete={onDelete} onDelete={onDelete}
onSelect={onSelect} onSelect={onSelect}
onNewOrder={onNewOrder} onNewOrder={onNewOrder}
onQR={onQR}
rowSelection={rowSelection} rowSelection={rowSelection}
rowSelectionHandler={rowSelectionHandler} rowSelectionHandler={rowSelectionHandler}
onLoadMoreAfter={onLoadMoreAfter} onLoadMoreAfter={onLoadMoreAfter}
@ -105,6 +108,7 @@ interface TableProps {
instances: Entity[]; instances: Entity[];
onDelete: (e: Entity) => void; onDelete: (e: Entity) => void;
onNewOrder: (e: Entity) => void; onNewOrder: (e: Entity) => void;
onQR: (e: Entity) => void;
onSelect: (e: Entity) => void; onSelect: (e: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>; rowSelectionHandler: StateUpdater<string[]>;
onLoadMoreBefore?: () => void; onLoadMoreBefore?: () => void;
@ -123,6 +127,7 @@ function Table({
onLoadMoreAfter, onLoadMoreAfter,
onDelete, onDelete,
onNewOrder, onNewOrder,
onQR,
onSelect, onSelect,
onLoadMoreBefore, onLoadMoreBefore,
hasMoreAfter, hasMoreAfter,
@ -185,6 +190,13 @@ function Table({
> >
New order New order
</button> </button>
<button
class="button is-info is-small has-tooltip-left"
data-tooltip={i18n.str`create qr code for the template`}
onClick={() => onQR(i)}
>
QR
</button>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -42,12 +42,14 @@ interface Props {
onCreate: () => void; onCreate: () => void;
onSelect: (id: string) => void; onSelect: (id: string) => void;
onNewOrder: (id: string) => void; onNewOrder: (id: string) => void;
onQR: (id: string) => void;
} }
export default function ListTemplates({ export default function ListTemplates({
onUnauthorized, onUnauthorized,
onLoadError, onLoadError,
onCreate, onCreate,
onQR,
onSelect, onSelect,
onNewOrder, onNewOrder,
onNotFound, onNotFound,
@ -80,6 +82,9 @@ export default function ListTemplates({
onNewOrder={(e) => { onNewOrder={(e) => {
onNewOrder(e.template_id); onNewOrder(e.template_id);
}} }}
onQR={(e) => {
onQR(e.template_id);
}}
onDelete={(e: MerchantBackend.Template.TemplateEntry) => onDelete={(e: MerchantBackend.Template.TemplateEntry) =>
deleteTemplate(e.template_id) deleteTemplate(e.template_id)
.then(() => .then(() =>

View File

@ -0,0 +1,27 @@
/*
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 <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { QrPage as TestedComponent } from "./QrPage.js";
export default {
title: "Pages/Templates/QR",
component: TestedComponent,
};

View File

@ -0,0 +1,133 @@
/*
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 <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { buildPayto, classifyTalerUri } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
import { QR } from "../../../../components/exception/QR.js";
import {
FormErrors,
FormProvider,
} from "../../../../components/form/FormProvider.js";
import { Input } from "../../../../components/form/Input.js";
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
import { useBackendContext } from "../../../../context/backend.js";
import { useConfigContext } from "../../../../context/config.js";
import { MerchantBackend } from "../../../../declaration.js";
type Entity = MerchantBackend.Template.UsingTemplateDetails;
interface Props {
template: MerchantBackend.Template.TemplateDetails;
id: string;
onBack?: () => void;
}
export function QrPage({ template, id: templateId, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
const { url: backendUrl } = useBackendContext();
const config = useConfigContext();
const [state, setState] = useState<Partial<Entity>>({
amount: template.template_contract.amount,
summary: template.template_contract.summary,
});
const errors: FormErrors<Entity> = {};
const hasErrors = Object.keys(errors).some(
(k) => (errors as any)[k] !== undefined,
);
const fixedAmount = !!template.template_contract.amount;
const fixedSummary = !!template.template_contract.summary;
const params = new URLSearchParams();
if (!fixedAmount) {
if (state.amount) {
params.append("amount", state.amount);
} else {
params.append("amount", config.currency);
}
}
if (!fixedSummary) {
params.append("summary", state.summary ?? "");
}
const paramsStr = fixedAmount && fixedSummary ? "" : "?" + params.toString();
const merchantURL = new URL(backendUrl);
const talerProto =
merchantURL.protocol === "http:" ? "taler+http:" : "taler:";
const payTemplateUri = `${talerProto}//pay-template/${merchantURL.hostname}/${templateId}${paramsStr}`;
return (
<div>
<section class="section is-main-section">
<div class="columns">
<div class="column" />
<div class="column is-four-fifths">
<FormProvider
object={state}
valueHandler={setState}
errors={errors}
>
<InputCurrency<Entity>
name="amount"
label={i18n.str`Amount`}
readonly={fixedAmount}
tooltip={i18n.str`Amount of the order`}
/>
<Input<Entity>
name="summary"
inputType="multiline"
readonly={fixedSummary}
label={i18n.str`Order summary`}
tooltip={i18n.str`Title of the order to be shown to the customer`}
/>
</FormProvider>
<div class="buttons is-right mt-5">
{onBack && (
<button class="button" onClick={onBack}>
<i18n.Translate>Cancel</i18n.Translate>
</button>
)}
<button class="button is-info" onClick={onBack}>
<i18n.Translate>Print</i18n.Translate>
</button>
</div>
</div>
<div class="column" />
</div>
</section>
<section>
<pre>
<a href={payTemplateUri}>{payTemplateUri}</a>
</pre>
<QR text={payTemplateUri} />
</section>
</div>
);
}

View File

@ -0,0 +1,70 @@
/*
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 <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import {
HttpError,
useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
import { MerchantBackend } from "../../../../declaration.js";
import {
useTemplateAPI,
useTemplateDetails,
} from "../../../../hooks/templates.js";
import { Notification } from "../../../../utils/types.js";
import { QrPage } from "./QrPage.js";
export type Entity = MerchantBackend.Transfers.TransferInformation;
interface Props {
onBack?: () => void;
onUnauthorized: () => VNode;
onNotFound: () => VNode;
onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode;
tid: string;
}
export default function TemplateQrPage({
tid,
onBack,
onLoadError,
onNotFound,
onUnauthorized,
}: Props): VNode {
const { createOrderFromTemplate } = useTemplateAPI();
const result = useTemplateDetails(tid);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
if (result.clientError && result.isUnauthorized) return onUnauthorized();
if (result.clientError && result.isNotfound) return onNotFound();
if (result.loading) return <Loading />;
if (!result.ok) return onLoadError(result);
return (
<>
<NotificationCard notification={notif} />
<QrPage template={result.data} id={tid} onBack={onBack} />
</>
);
}

View File

@ -123,13 +123,13 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
<Input <Input
name="template_contract.summary" name="template_contract.summary"
inputType="multiline" inputType="multiline"
label={i18n.str`Order summary`} label={i18n.str`Fixed summary`}
tooltip={i18n.str`Title of the order to be shown to the customer`} tooltip={i18n.str`If specified, this template will create order with the same summary`}
/> />
<InputCurrency <InputCurrency
name="template_contract.amount" name="template_contract.amount"
label={i18n.str`Order price`} label={i18n.str`Fixed price`}
tooltip={i18n.str`total product price added up`} tooltip={i18n.str`If specified, this template will create order with the same price`}
/> />
<InputNumber <InputNumber
name="template_contract.minimum_age" name="template_contract.minimum_age"