add use-template button

This commit is contained in:
Sebastian 2023-01-27 12:35:10 -03:00
parent 7a3717125f
commit 1b2b5d62de
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
18 changed files with 312 additions and 28 deletions

View File

@ -48,6 +48,7 @@ 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";
import TemplateCreatePage from "./paths/instance/templates/create/index.js";
import TemplateUsePage from "./paths/instance/templates/use/index.js";
import TemplateListPage from "./paths/instance/templates/list/index.js";
import TemplateUpdatePage from "./paths/instance/templates/update/index.js";
import TransferCreatePage from "./paths/instance/transfers/create/index.js";
@ -85,6 +86,7 @@ export enum InstancePaths {
templates_list = "/templates",
templates_update = "/templates/:tid/update",
templates_new = "/templates/new",
templates_use = "/templates/:tid/use",
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
@ -399,6 +401,9 @@ export function InstanceRoutes({
onCreate={() => {
route(InstancePaths.templates_new);
}}
onNewOrder={(id: string) => {
route(InstancePaths.templates_use.replace(":tid", id));
}}
onSelect={(id: string) => {
route(InstancePaths.templates_update.replace(":tid", id));
}}
@ -426,6 +431,19 @@ export function InstanceRoutes({
route(InstancePaths.templates_list);
}}
/>
<Route
path={InstancePaths.templates_use}
component={TemplateUsePage}
onOrderCreated={(id: string) => {
route(InstancePaths.order_details.replace(":oid", id));
}}
onUnauthorized={LoginPageAccessDenied}
onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onBack={() => {
route(InstancePaths.templates_list);
}}
/>
{/**
* reserves pages

View File

@ -92,6 +92,7 @@ export function Input<T>({
inputType={inputType}
placeholder={placeholder}
readonly={readonly}
disabled={readonly}
name={String(name)}
value={toStr(value)}
onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void =>

View File

@ -86,6 +86,7 @@ export function InputWithAddon<T>({
type={inputType}
placeholder={placeholder}
readonly={readonly}
disabled={readonly}
name={String(name)}
value={toStr(value)}
onChange={(e): void => onChange(fromStr(e.currentTarget.value))}

View File

@ -1347,7 +1347,7 @@ export namespace MerchantBackend {
interface UsingTemplateDetails {
// Subject of the template
subject?: string;
summary?: string;
// The amount entered by the customer.
amount?: Amount;
@ -1355,7 +1355,8 @@ export namespace MerchantBackend {
interface UsingTemplateResponse {
// After enter the request. The user will be pay with a taler URL.
taler_url: string;
order_id: string,
token: string,
}
}

View File

@ -62,14 +62,14 @@ export function useTemplateAPI(): TemplateAPI {
return res;
};
const createOrder = async (
const createOrderFromTemplate = async (
templateId: string,
data: MerchantBackend.Template.UsingTemplateDetails,
): Promise<
HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse>
> => {
const res = await request<MerchantBackend.Template.UsingTemplateResponse>(
`/private/templates/${templateId}`,
`/templates/${templateId}`,
{
method: "POST",
data,
@ -79,7 +79,7 @@ export function useTemplateAPI(): TemplateAPI {
return res;
};
return { createTemplate, updateTemplate, deleteTemplate, createOrder };
return { createTemplate, updateTemplate, deleteTemplate, createOrderFromTemplate };
}
export interface TemplateAPI {
@ -91,7 +91,7 @@ export interface TemplateAPI {
data: MerchantBackend.Template.TemplatePatchDetails,
) => Promise<HttpResponseOk<void>>;
deleteTemplate: (id: string) => Promise<HttpResponseOk<void>>;
createOrder: (
createOrderFromTemplate: (
id: string,
data: MerchantBackend.Template.UsingTemplateDetails,
) => Promise<HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse>>;
@ -174,8 +174,7 @@ export function useInstanceTemplates(
if (afterData.data.templates.length < MAX_RESULT_SIZE) {
setPageAfter(pageAfter + 1);
} else {
const from = `${
afterData.data.templates[afterData.data.templates.length - 1]
const from = `${afterData.data.templates[afterData.data.templates.length - 1]
.template_id
}`;
if (from && updatePosition) updatePosition(from);

View File

@ -40,8 +40,8 @@ function convert(
const payto_uris = accounts.filter((a) => a.active).map((a) => a.payto_uri);
const defaults = {
default_wire_fee_amortization: 1,
default_pay_delay: { d_us: 1000 * 60 * 60 }, //one hour
default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 }, //two hours
default_pay_delay: { d_us: 1000 * 60 * 60 * 1000 }, //one hour
default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 * 1000 }, //two hours
};
return { ...defaults, ...rest, payto_uris };
}

View File

@ -59,11 +59,11 @@ export const Example = createExample(TestedComponent, {
default_max_deposit_fee: "TESTKUDOS:2",
default_max_wire_fee: "TESTKUDOS:1",
default_pay_delay: {
d_us: 1000000,
d_us: 1000 * 1000, //one second
},
default_wire_fee_amortization: 1,
default_wire_transfer_delay: {
d_us: 100000,
d_us: 1000 * 1000, //one second
},
merchant_pub: "ASDWQEKASJDKSADJ",
},

View File

@ -45,7 +45,7 @@ export const Example = createExample(TestedComponent, {
default_max_deposit_fee: "",
default_max_wire_fee: "",
default_pay_delay: {
d_us: 1000 * 60 * 60,
d_us: 1000 * 1000 * 60 * 60, //one hour
},
default_wire_fee_amortization: 1,
},

View File

@ -61,7 +61,9 @@ function with_defaults(config: InstanceConfig): Partial<Entity> {
const defaultPayDeadline =
!config.default_pay_delay || config.default_pay_delay.d_us === "forever"
? undefined
: add(new Date(), { seconds: config.default_pay_delay.d_us / 1000 });
: add(new Date(), {
seconds: config.default_pay_delay.d_us / (1000 * 1000),
});
return {
inventoryProducts: {},

View File

@ -72,7 +72,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
? i18n.str`can't be empty`
: state.template_contract.pay_duration.d_us === "forever"
? undefined
: state.template_contract.pay_duration.d_us < 1000
: state.template_contract.pay_duration.d_us < 1000 * 1000 //less than one second
? i18n.str`to short`
: undefined,
}),

View File

@ -31,6 +31,7 @@ export interface Props {
onCreate: () => void;
onDelete: (e: MerchantBackend.Template.TemplateEntry) => void;
onSelect: (e: MerchantBackend.Template.TemplateEntry) => void;
onNewOrder: (e: MerchantBackend.Template.TemplateEntry) => void;
}
export function ListPage({
@ -38,6 +39,7 @@ export function ListPage({
onCreate,
onDelete,
onSelect,
onNewOrder,
onLoadMoreBefore,
onLoadMoreAfter,
}: Props): VNode {
@ -54,6 +56,7 @@ export function ListPage({
onCreate={onCreate}
onDelete={onDelete}
onSelect={onSelect}
onNewOrder={onNewOrder}
onLoadMoreBefore={onLoadMoreBefore}
hasMoreBefore={!onLoadMoreBefore}
onLoadMoreAfter={onLoadMoreAfter}

View File

@ -30,6 +30,7 @@ interface Props {
templates: Entity[];
onDelete: (e: Entity) => void;
onSelect: (e: Entity) => void;
onNewOrder: (e: Entity) => void;
onCreate: () => void;
onLoadMoreBefore?: () => void;
hasMoreBefore?: boolean;
@ -42,6 +43,7 @@ export function CardTable({
onCreate,
onDelete,
onSelect,
onNewOrder,
onLoadMoreAfter,
onLoadMoreBefore,
hasMoreAfter,
@ -81,6 +83,7 @@ export function CardTable({
instances={templates}
onDelete={onDelete}
onSelect={onSelect}
onNewOrder={onNewOrder}
rowSelection={rowSelection}
rowSelectionHandler={rowSelectionHandler}
onLoadMoreAfter={onLoadMoreAfter}
@ -101,6 +104,7 @@ interface TableProps {
rowSelection: string[];
instances: Entity[];
onDelete: (e: Entity) => void;
onNewOrder: (e: Entity) => void;
onSelect: (e: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>;
onLoadMoreBefore?: () => void;
@ -118,6 +122,7 @@ function Table({
instances,
onLoadMoreAfter,
onDelete,
onNewOrder,
onSelect,
onLoadMoreBefore,
hasMoreAfter,
@ -164,7 +169,8 @@ function Table({
>
{i.template_description}
</td>
<td>
<td class="is-actions-cell right-sticky">
<div class="buttons is-right">
<button
class="button is-danger is-small has-tooltip-left"
data-tooltip={i18n.str`delete selected templates from the database`}
@ -172,6 +178,14 @@ function Table({
>
Delete
</button>
<button
class="button is-info is-small has-tooltip-left"
data-tooltip={i18n.str`delete selected templates from the database`}
onClick={() => onNewOrder(i)}
>
New order
</button>
</div>
</td>
</tr>
);

View File

@ -39,6 +39,7 @@ interface Props {
onNotFound: () => VNode;
onCreate: () => void;
onSelect: (id: string) => void;
onNewOrder: (id: string) => void;
}
export default function ListTemplates({
@ -46,6 +47,7 @@ export default function ListTemplates({
onLoadError,
onCreate,
onSelect,
onNewOrder,
onNotFound,
}: Props): VNode {
const [position, setPosition] = useState<string | undefined>(undefined);
@ -73,6 +75,9 @@ export default function ListTemplates({
onSelect={(e) => {
onSelect(e.template_id);
}}
onNewOrder={(e) => {
onNewOrder(e.template_id);
}}
onDelete={(e: MerchantBackend.Template.TemplateEntry) =>
deleteTemplate(e.template_id)
.then(() =>

View File

@ -65,7 +65,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
? i18n.str`can't be empty`
: state.template_contract.pay_duration.d_us === "forever"
? undefined
: state.template_contract.pay_duration.d_us < 1000
: state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second
? i18n.str`to short`
: undefined,
}),

View File

@ -0,0 +1,28 @@
/*
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 { h, VNode, FunctionalComponent } from "preact";
import { CreatePage as TestedComponent } from "./UsePage.js";
export default {
title: "Pages/Templates/Create",
component: TestedComponent,
};

View File

@ -0,0 +1,126 @@
/*
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 { 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 {
FormErrors,
FormProvider,
} from "../../../../components/form/FormProvider.js";
import { Input } from "../../../../components/form/Input.js";
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
import { MerchantBackend } from "../../../../declaration.js";
type Entity = MerchantBackend.Template.UsingTemplateDetails;
interface Props {
template: MerchantBackend.Template.TemplateDetails;
onCreateOrder: (d: Entity) => Promise<void>;
onBack?: () => void;
}
export function UsePage({ template, onCreateOrder, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
const [state, setState] = useState<Partial<Entity>>({
amount: template.template_contract.amount,
summary: template.template_contract.summary,
});
const errors: FormErrors<Entity> = {
amount:
!template.template_contract.amount && !state.amount
? i18n.str`Amount is required`
: undefined,
summary:
!template.template_contract.summary && !state.summary
? i18n.str`Order summary is required`
: undefined,
};
const hasErrors = Object.keys(errors).some(
(k) => (errors as any)[k] !== undefined,
);
const submitForm = () => {
if (hasErrors) return Promise.reject();
if (template.template_contract.amount) {
delete state.amount;
}
if (template.template_contract.summary) {
delete state.summary;
}
return onCreateOrder(state as any);
};
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={!!template.template_contract.amount}
tooltip={i18n.str`Amount of the order`}
/>
<Input<Entity>
name="summary"
inputType="multiline"
label={i18n.str`Order summary`}
readonly={!!template.template_contract.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>
)}
<AsyncButton
disabled={hasErrors}
data-tooltip={
hasErrors
? i18n.str`Need to complete marked fields`
: "confirm operation"
}
onClick={submitForm}
>
<i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
</div>
</div>
<div class="column" />
</div>
</section>
</div>
);
}

View File

@ -0,0 +1,86 @@
/*
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 { 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 { HttpError } from "../../../../utils/request.js";
import { Notification } from "../../../../utils/types.js";
import { UsePage } from "./UsePage.js";
export type Entity = MerchantBackend.Transfers.TransferInformation;
interface Props {
onBack?: () => void;
onOrderCreated: (id: string) => void;
onUnauthorized: () => VNode;
onNotFound: () => VNode;
onLoadError: (e: HttpError) => VNode;
tid: string;
}
export default function TemplateUsePage({
tid,
onOrderCreated,
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} />
<UsePage
template={result.data}
onBack={onBack}
onCreateOrder={(
request: MerchantBackend.Template.UsingTemplateDetails,
) => {
return createOrderFromTemplate(tid, request)
.then((res) => onOrderCreated(res.data.order_id))
.catch((error) => {
setNotif({
message: i18n.str`could not create order from template`,
type: "ERROR",
description: error.message,
});
});
}}
/>
</>
);
}

View File

@ -50,11 +50,11 @@ export const Example = createExample(TestedComponent, {
default_max_deposit_fee: "TESTKUDOS:2",
default_max_wire_fee: "TESTKUDOS:1",
default_pay_delay: {
d_us: 1000000,
d_us: 1000 * 1000, //one second
},
default_wire_fee_amortization: 1,
default_wire_transfer_delay: {
d_us: 100000,
d_us: 1000 * 1000, //one second
},
merchant_pub: "ASDWQEKASJDKSADJ",
},