backoffice ui

This commit is contained in:
Sebastian 2023-10-06 10:38:09 -03:00
parent 97d7be7503
commit 98013322db
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
26 changed files with 256 additions and 232 deletions

View File

@ -1,7 +1,7 @@
{
"private": true,
"name": "@gnu-taler/merchant-backoffice-ui",
"version": "0.1.0",
"version": "0.9.3-dev.27",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {

View File

@ -64,7 +64,7 @@ function ApplicationStatusRoutes(): VNode {
const result = useBackendConfig();
const { i18n } = useTranslationContext();
const { currency, version } = result.ok
const { currency, version } = result.ok && result.data
? result.data
: { currency: "unknown", version: "unknown" };
const ctx = useMemo(() => ({ currency, version }), [currency, version]);

View File

@ -80,7 +80,7 @@ import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js";
export enum InstancePaths {
error = "/error",
server = "/server",
settings = "/settings",
token = "/token",
bank_list = "/bank",
@ -118,7 +118,7 @@ export enum InstancePaths {
validators_update = "/validators/:vid/update",
validators_new = "/validators/new",
settings = "/interface",
interface = "/interface",
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
@ -255,7 +255,7 @@ export function InstanceRoutes({
instance={id}
admin={admin}
onShowSettings={() => {
route(InstancePaths.settings)
route(InstancePaths.interface)
}}
path={path}
onLogout={clearTokenAndGoToRoot}
@ -320,7 +320,7 @@ export function InstanceRoutes({
* Update instance page
*/}
<Route
path={InstancePaths.server}
path={InstancePaths.settings}
component={InstanceUpdatePage}
onBack={() => {
route(`/`);
@ -353,7 +353,7 @@ export function InstanceRoutes({
path={InstancePaths.inventory_list}
component={ProductListPage}
onUnauthorized={LoginPageAccessDenied}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => {
route(InstancePaths.inventory_new);
}}
@ -392,7 +392,7 @@ export function InstanceRoutes({
path={InstancePaths.bank_list}
component={BankAccountListPage}
onUnauthorized={LoginPageAccessDenied}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => {
route(InstancePaths.bank_new);
}}
@ -437,7 +437,7 @@ export function InstanceRoutes({
route(InstancePaths.order_details.replace(":oid", id));
}}
onUnauthorized={LoginPageAccessDenied}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
/>
<Route
@ -468,7 +468,7 @@ export function InstanceRoutes({
component={TransferListPage}
onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => {
route(InstancePaths.transfers_new);
}}
@ -491,7 +491,7 @@ export function InstanceRoutes({
component={WebhookListPage}
onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => {
route(InstancePaths.webhooks_new);
}}
@ -530,7 +530,7 @@ export function InstanceRoutes({
component={ValidatorListPage}
onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => {
route(InstancePaths.validators_new);
}}
@ -569,7 +569,7 @@ export function InstanceRoutes({
component={TemplateListPage}
onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => {
route(InstancePaths.templates_new);
}}
@ -638,7 +638,7 @@ export function InstanceRoutes({
component={ReservesListPage}
onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onSelect={(id: string) => {
route(InstancePaths.reserves_details.replace(":rid", id));
}}

View File

@ -107,8 +107,9 @@ export function InputWithAddon<T>({
{error && <p class="help is-danger">{error}</p>}
<span class="has-text-grey">{help}</span>
</div>
{side}
{expand ? <div>{side}</div> : side}
</div>
</div>
);
}

View File

@ -0,0 +1,59 @@
import { TranslatedString } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
export function JumpToElementById({ testIfExist, onSelect, palceholder, description }: { palceholder: TranslatedString, description: TranslatedString, testIfExist: (id: string) => Promise<any>, onSelect: (id: string) => void }): VNode {
const { i18n } = useTranslationContext()
const [error, setError] = useState<string | undefined>(
undefined,
);
const [id, setId] = useState<string>()
async function check(currentId: string | undefined): Promise<void> {
if (!currentId) {
setError(i18n.str`missing id`);
return;
}
try {
await testIfExist(currentId);
onSelect(currentId);
setError(undefined);
} catch {
setError(i18n.str`not found`);
}
}
return <div class="level">
<div class="level-left">
<div class="level-item">
<div class="field has-addons">
<div class="control">
<input
class={error ? "input is-danger" : "input"}
type="text"
value={id ?? ""}
onChange={(e) => setId(e.currentTarget.value)}
placeholder={palceholder}
/>
{error && <p class="help is-danger">{error}</p>}
</div>
<span
class="has-tooltip-bottom"
data-tooltip={description}
>
<button
class="button"
onClick={(e) => check(id)}
>
<span class="icon">
<i class="mdi mdi-arrow-right" />
</span>
</button>
</span>
</div>
</div>
</div>
</div>
}

View File

@ -177,12 +177,12 @@ export function Sidebar({
</a>
</li>
<li>
<a href={"/server"} class="has-icon">
<a href={"/settings"} class="has-icon">
<span class="icon">
<i class="mdi mdi-square-edit-outline" />
</span>
<span class="menu-item-label">
<i18n.Translate>Server</i18n.Translate>
<i18n.Translate>Settings</i18n.Translate>
</span>
</a>
</li>

View File

@ -24,7 +24,7 @@ import { Sidebar } from "./SideBar.js";
function getInstanceTitle(path: string, id: string): string {
switch (path) {
case InstancePaths.server:
case InstancePaths.settings:
return `${id}: Settings`;
case InstancePaths.order_list:
return `${id}: Orders`;
@ -64,9 +64,7 @@ function getInstanceTitle(path: string, id: string): string {
return `${id}: Templates`;
case InstancePaths.templates_use:
return `${id}: Use template`;
case InstancePaths.settings:
return `${id}: Interface`;
case InstancePaths.settings:
case InstancePaths.interface:
return `${id}: Interface`;
default:
return "";

View File

@ -91,7 +91,7 @@ const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000;
const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000;
export function useBackendConfig(): HttpResponse<
MerchantBackend.VersionResponse,
MerchantBackend.VersionResponse | undefined,
RequestError<MerchantBackend.ErrorDetail>
> {
const { request } = useBackendBaseRequest();
@ -340,6 +340,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
if (refunded !== undefined) params.refunded = refunded;
if (wired !== undefined) params.wired = wired;
if (date_s !== undefined) params.date_s = date_s;
if (delta === 0) {
//in this case we can already assume the response
//and avoid network
return Promise.resolve({
ok: true,
data: {orders:[]} as T,
})
}
return requestHandler<T>(baseUrl, endpoint, { params, token });
},
[baseUrl, token],
@ -385,6 +393,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
const params: any = {};
if (payto_uri !== undefined) params.payto_uri = payto_uri;
if (verified !== undefined) params.verified = verified;
if (delta === 0) {
//in this case we can already assume the response
//and avoid network
return Promise.resolve({
ok: true,
data: {transfers:[]} as T,
})
}
if (delta !== undefined) {
params.limit = delta;
}
@ -403,6 +419,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
): Promise<HttpResponseOk<T>> {
const [endpoint, position, delta] = args
const params: any = {};
if (delta === 0) {
//in this case we can already assume the response
//and avoid network
return Promise.resolve({
ok: true,
data: {templates:[]} as T,
})
}
if (delta !== undefined) {
params.limit = delta;
}
@ -421,6 +445,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
): Promise<HttpResponseOk<T>> {
const [endpoint, position, delta] = args
const params: any = {};
if (delta === 0) {
//in this case we can already assume the response
//and avoid network
return Promise.resolve({
ok: true,
data: {webhooks:[]} as T,
})
}
if (delta !== undefined) {
params.limit = delta;
}

View File

@ -32,7 +32,13 @@ const calculateRootPath = () => {
? window.location.origin + window.location.pathname
: "/";
return rootPath.replace("webui/","");
/**
* By default, merchant backend serves the html content
* from the /webui root. This should cover most of the
* cases and the rootPath will be the merchant backend
* URL where the instances are
*/
return rootPath.replace("/webui/", "");
};
const loginTokenCodec = buildCodecForObject<LoginToken>()

View File

@ -82,10 +82,19 @@ export function useTemplateAPI(): TemplateAPI {
return res;
};
const testTemplateExist = async (
templateId: string,
): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`/private/templates/${templateId}`, { method: "GET", });
return res;
};
return {
createTemplate,
updateTemplate,
deleteTemplate,
testTemplateExist,
createOrderFromTemplate,
};
}
@ -98,6 +107,9 @@ export interface TemplateAPI {
id: string,
data: MerchantBackend.Template.TemplatePatchDetails,
) => Promise<HttpResponseOk<void>>;
testTemplateExist: (
id: string
) => Promise<HttpResponseOk<void>>;
deleteTemplate: (id: string) => Promise<HttpResponseOk<void>>;
createOrderFromTemplate: (
id: string,
@ -119,11 +131,11 @@ export function useInstanceTemplates(
> {
const { templateFetcher } = useBackendInstanceRequest();
// const [pageBefore, setPageBefore] = useState(1);
const [pageBefore, setPageBefore] = useState(1);
const [pageAfter, setPageAfter] = useState(1);
const totalAfter = pageAfter * PAGE_SIZE;
// const totalBefore = args?.position !== undefined ? pageBefore * PAGE_SIZE : 0;
const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0;
/**
* FIXME: this can be cleaned up a little
@ -131,20 +143,20 @@ export function useInstanceTemplates(
* the logic of double query should be inside the orderFetch so from the hook perspective and cache
* is just one query and one error status
*/
// const {
// data: beforeData,
// error: beforeError,
// isValidating: loadingBefore,
// } = useSWR<HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>, HttpError>(
// [
// `/private/templates`,
// token,
// url,
// args?.position,
// totalBefore,
// ],
// templateFetcher,
// );
const {
data: beforeData,
error: beforeError,
isValidating: loadingBefore,
} = useSWR<
HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>,
RequestError<MerchantBackend.ErrorDetail>>(
[
`/private/templates`,
args?.position,
totalBefore,
],
templateFetcher,
);
const {
data: afterData,
error: afterError,
@ -155,9 +167,13 @@ export function useInstanceTemplates(
>([`/private/templates`, args?.position, -totalAfter], templateFetcher);
//this will save last result
// const [lastBefore, setLastBefore] = useState<
// HttpResponse<MerchantBackend.Template.TemplateSummaryResponse, MerchantBackend.ErrorDetail>
// >({ loading: true });
const [lastBefore, setLastBefore] = useState<
HttpResponse<
MerchantBackend.Template.TemplateSummaryResponse,
MerchantBackend.ErrorDetail
>
>({ loading: true });
const [lastAfter, setLastAfter] = useState<
HttpResponse<
MerchantBackend.Template.TemplateSummaryResponse,
@ -166,19 +182,18 @@ export function useInstanceTemplates(
>({ loading: true });
useEffect(() => {
if (afterData) setLastAfter(afterData);
// if (beforeData) setLastBefore(beforeData);
}, [afterData /*, beforeData*/]);
if (beforeData) setLastBefore(beforeData);
}, [afterData, beforeData]);
// if (beforeError) return beforeError;
if (beforeError) return beforeError.cause;
if (afterError) return afterError.cause;
// if the query returns less that we ask, then we have reach the end or beginning
const isReachingEnd =
afterData && afterData.data.templates.length < totalAfter;
const isReachingStart = false;
// args?.position === undefined
// ||
// (beforeData && beforeData.data.templates.length < totalBefore);
const isReachingStart = args?.position === undefined
||
(beforeData && beforeData.data.templates.length < totalBefore);
const pagination = {
isReachingEnd,
@ -188,37 +203,36 @@ export function useInstanceTemplates(
if (afterData.data.templates.length < MAX_RESULT_SIZE) {
setPageAfter(pageAfter + 1);
} else {
const from = `${
afterData.data.templates[afterData.data.templates.length - 1]
const from = `${afterData.data.templates[afterData.data.templates.length - 1]
.template_id
}`;
if (from && updatePosition) updatePosition(from);
}
},
loadMorePrev: () => {
// if (!beforeData || isReachingStart) return;
// if (beforeData.data.templates.length < MAX_RESULT_SIZE) {
// setPageBefore(pageBefore + 1);
// } else if (beforeData) {
// const from = `${beforeData.data.templates[beforeData.data.templates.length - 1]
// .template_id
// }`;
// if (from && updatePosition) updatePosition(from);
// }
if (!beforeData || isReachingStart) return;
if (beforeData.data.templates.length < MAX_RESULT_SIZE) {
setPageBefore(pageBefore + 1);
} else if (beforeData) {
const from = `${beforeData.data.templates[beforeData.data.templates.length - 1]
.template_id
}`;
if (from && updatePosition) updatePosition(from);
}
},
};
const templates = !afterData ? [] : (afterData || lastAfter).data.templates;
// const templates =
// !beforeData || !afterData
// ? []
// : (beforeData || lastBefore).data.templates
// .slice()
// .reverse()
// .concat((afterData || lastAfter).data.templates);
if (loadingAfter /* || loadingBefore */)
// const templates = !afterData ? [] : (afterData || lastAfter).data.templates;
const templates =
!beforeData || !afterData
? []
: (beforeData || lastBefore).data.templates
.slice()
.reverse()
.concat((afterData || lastAfter).data.templates);
if (loadingAfter || loadingBefore)
return { loading: true, data: { templates } };
if (/*beforeData &&*/ afterData) {
if (beforeData && afterData) {
return { ok: true, data: { templates }, ...pagination };
}
return { loading: true };

View File

@ -93,9 +93,11 @@ export function useInstanceWebhooks(
> {
const { webhookFetcher } = useBackendInstanceRequest();
const [pageBefore, setPageBefore] = useState(1);
const [pageAfter, setPageAfter] = useState(1);
const totalAfter = pageAfter * PAGE_SIZE;
const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0;
const {
data: afterData,
@ -120,7 +122,7 @@ export function useInstanceWebhooks(
const isReachingEnd =
afterData && afterData.data.webhooks.length < totalAfter;
const isReachingStart = false;
const isReachingStart = true;
const pagination = {
isReachingEnd,

View File

@ -21,7 +21,7 @@
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { h, VNode } from "preact";
import { h, VNode, Fragment } from "preact";
import { useState } from "preact/hooks";
import { DatePicker } from "../../../../components/picker/DatePicker.js";
import { MerchantBackend, WithId } from "../../../../declaration.js";
@ -29,8 +29,6 @@ import { CardTable } from "./Table.js";
import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
export interface ListPageProps {
errorOrderId: string | undefined;
onShowAll: () => void;
onShowNotPaid: () => void;
onShowPaid: () => void;
@ -56,17 +54,18 @@ export interface ListPageProps {
onSelectOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
onRefundOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
onSearchOrderById: (id: string) => void;
onCreate: () => void;
}
export function ListPage({
hasMoreAfter,
hasMoreBefore,
onLoadMoreAfter,
onLoadMoreBefore,
orders,
errorOrderId,
isAllActive,
onSelectOrder,
onRefundOrder,
onSearchOrderById,
jumpToDate,
onCopyURL,
onShowAll,
@ -86,42 +85,10 @@ export function ListPage({
const { i18n } = useTranslationContext();
const dateTooltip = i18n.str`select date to show nearby orders`;
const [pickDate, setPickDate] = useState(false);
const [orderId, setOrderId] = useState<string>("");
const [settings] = useSettings();
return (
<section class="section is-main-section">
<div class="level">
<div class="level-left">
<div class="level-item">
<div class="field has-addons">
<div class="control">
<input
class={errorOrderId ? "input is-danger" : "input"}
type="text"
value={orderId}
onChange={(e) => setOrderId(e.currentTarget.value)}
placeholder={i18n.str`order id`}
/>
{errorOrderId && <p class="help is-danger">{errorOrderId}</p>}
</div>
<span
class="has-tooltip-bottom"
data-tooltip={i18n.str`jump to order with the given order ID`}
>
<button
class="button"
onClick={(e) => onSearchOrderById(orderId)}
>
<span class="icon">
<i class="mdi mdi-arrow-right" />
</span>
</button>
</span>
</div>
</div>
</div>
</div>
<Fragment>
<div class="columns">
<div class="column is-two-thirds">
<div class="tabs" style={{ overflow: "inherit" }}>
@ -249,7 +216,11 @@ export function ListPage({
onCopyURL={onCopyURL}
onSelect={onSelectOrder}
onRefund={onRefundOrder}
hasMoreAfter={hasMoreAfter}
hasMoreBefore={hasMoreBefore}
onLoadMoreAfter={onLoadMoreAfter}
onLoadMoreBefore={onLoadMoreBefore}
/>
</section>
</Fragment>
);
}

View File

@ -140,10 +140,9 @@ function Table({
const [settings] = useSettings();
return (
<div class="table-container">
{onLoadMoreBefore && (
{hasMoreBefore && (
<button
class="button is-fullwidth"
disabled={!hasMoreBefore}
onClick={onLoadMoreBefore}
>
<i18n.Translate>load newer orders</i18n.Translate>
@ -218,10 +217,9 @@ function Table({
})}
</tbody>
</table>
{onLoadMoreAfter && (
{hasMoreAfter && (
<button
class="button is-fullwidth"
disabled={!hasMoreAfter}
onClick={onLoadMoreAfter}
>
<i18n.Translate>load older orders</i18n.Translate>

View File

@ -39,6 +39,7 @@ import { Notification } from "../../../../utils/types.js";
import { ListPage } from "./ListPage.js";
import { RefundModal } from "./Table.js";
import { HttpStatusCode } from "@gnu-taler/taler-util";
import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
interface Props {
onUnauthorized: () => VNode;
@ -69,9 +70,6 @@ export default function OrderList({
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
const [errorOrderId, setErrorOrderId] = useState<string | undefined>(
undefined,
);
if (result.loading) return <Loading />;
if (!result.ok) {
@ -100,24 +98,17 @@ export default function OrderList({
? "is-active"
: "";
async function testIfOrderExistAndSelect(orderId: string): Promise<void> {
if (!orderId) {
setErrorOrderId(i18n.str`Enter an order id`);
return;
}
try {
await getPaymentURL(orderId);
onSelect(orderId);
setErrorOrderId(undefined);
} catch {
setErrorOrderId(i18n.str`order not found`);
}
}
return (
<Fragment>
<section class="section is-main-section">
<NotificationCard notification={notif} />
<JumpToElementById
testIfExist={getPaymentURL}
onSelect={onSelect}
description={i18n.str`jump to order with the given product ID`}
palceholder={i18n.str`order id`}
/>
<ListPage
orders={result.data.orders.map((o) => ({ ...o, id: o.order_id }))}
onLoadMoreBefore={result.loadMorePrev}
@ -126,7 +117,6 @@ export default function OrderList({
hasMoreAfter={!result.isReachingEnd}
onSelectOrder={(order) => onSelect(order.id)}
onRefundOrder={(value) => setOrderToBeRefunded(value)}
errorOrderId={errorOrderId}
isAllActive={isAllActive}
isNotWiredActive={isNotWiredActive}
isWiredActive={isWiredActive}
@ -138,7 +128,6 @@ export default function OrderList({
getPaymentURL(id).then((resp) => copyToClipboard(resp.data))
}
onCreate={onCreate}
onSearchOrderById={testIfOrderExistAndSelect}
onSelectDate={setNewDate}
onShowAll={() => setFilter({})}
onShowNotPaid={() => setFilter({ paid: "no" })}
@ -190,7 +179,7 @@ export default function OrderList({
}}
/>
)}
</Fragment>
</section>
);
}

View File

@ -244,7 +244,10 @@ function Table({
}
style={{ cursor: "pointer" }}
>
<span style={{"whiteSpace":"nowrap"}}>
{i.total_sold} {i.unit}
</span>
</td>
<td class="is-actions-cell right-sticky">
<div class="buttons is-right">
@ -341,7 +344,7 @@ function FastProductWithInfiniteStockUpdateForm({
<div class="buttons mt-5">
<button class="button " onClick={onCancel}>
<button class="button mt-5" onClick={onCancel}>
<i18n.Translate>Clone</i18n.Translate>
</button>
</div>

View File

@ -37,6 +37,7 @@ import { Notification } from "../../../../utils/types.js";
import { CardTable } from "./Table.js";
import { HttpStatusCode } from "@gnu-taler/taler-util";
import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js";
import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
interface Props {
onUnauthorized: () => VNode;
@ -74,60 +75,17 @@ export default function ProductList({
return onNotFound();
return onLoadError(result);
}
const [errorId, setErrorId] = useState<string | undefined>(
undefined,
);
const [productId, setProductId] = useState<string>()
async function testIfProductExistAndSelect(orderId: string | undefined): Promise<void> {
if (!orderId) {
setErrorId(i18n.str`Enter a product id`);
return;
}
try {
await getProduct(orderId);
onSelect(orderId);
setErrorId(undefined);
} catch {
setErrorId(i18n.str`product not found`);
}
}
return (
<section class="section is-main-section">
<NotificationCard notification={notif} />
<div class="level">
<div class="level-left">
<div class="level-item">
<div class="field has-addons">
<div class="control">
<input
class={errorId ? "input is-danger" : "input"}
type="text"
value={productId ?? ""}
onChange={(e) => setProductId(e.currentTarget.value)}
placeholder={i18n.str`product id`}
<JumpToElementById
testIfExist={getProduct}
onSelect={onSelect}
description={i18n.str`jump to product with the given product ID`}
palceholder={i18n.str`product id`}
/>
{errorId && <p class="help is-danger">{errorId}</p>}
</div>
<span
class="has-tooltip-bottom"
data-tooltip={i18n.str`jump to product with the given product ID`}
>
<button
class="button"
onClick={(e) => testIfProductExistAndSelect(productId)}
>
<span class="icon">
<i class="mdi mdi-arrow-right" />
</span>
</button>
</span>
</div>
</div>
</div>
</div>
<CardTable
instances={result.data}

View File

@ -49,7 +49,6 @@ export function ListPage({
const { i18n } = useTranslationContext();
return (
<section class="section is-main-section">
<CardTable
templates={templates.map((o) => ({
...o,
@ -65,6 +64,5 @@ export function ListPage({
onLoadMoreAfter={onLoadMoreAfter}
hasMoreAfter={!onLoadMoreAfter}
/>
</section>
);
}

View File

@ -136,11 +136,10 @@ function Table({
const { i18n } = useTranslationContext();
return (
<div class="table-container">
{onLoadMoreBefore && (
{hasMoreBefore && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more templates before the first one`}
disabled={!hasMoreBefore}
onClick={onLoadMoreBefore}
>
<i18n.Translate>load newer templates</i18n.Translate>
@ -188,7 +187,7 @@ function Table({
data-tooltip={i18n.str`use template to create new order`}
onClick={() => onNewOrder(i)}
>
New order
Use template
</button>
<button
class="button is-info is-small has-tooltip-left"
@ -204,11 +203,10 @@ function Table({
})}
</tbody>
</table>
{onLoadMoreAfter && (
{hasMoreAfter && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more templates after the last one`}
disabled={!hasMoreAfter}
onClick={onLoadMoreAfter}
>
<i18n.Translate>load older templates</i18n.Translate>

View File

@ -35,8 +35,9 @@ import {
} from "../../../../hooks/templates.js";
import { Notification } from "../../../../utils/types.js";
import { ListPage } from "./ListPage.js";
import { HttpStatusCode } from "@gnu-taler/taler-util";
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
import { ConfirmModal } from "../../../../components/modal/index.js";
import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
interface Props {
onUnauthorized: () => VNode;
@ -60,7 +61,7 @@ export default function ListTemplates({
const [position, setPosition] = useState<string | undefined>(undefined);
const { i18n } = useTranslationContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { deleteTemplate } = useTemplateAPI();
const { deleteTemplate, testTemplateExist } = useTemplateAPI();
const result = useInstanceTemplates({ position }, (id) => setPosition(id));
const [deleting, setDeleting] =
useState<MerchantBackend.Template.TemplateEntry | null>(null);
@ -81,9 +82,16 @@ export default function ListTemplates({
}
return (
<Fragment>
<section class="section is-main-section">
<NotificationCard notification={notif} />
<JumpToElementById
testIfExist={testTemplateExist}
onSelect={onSelect}
description={i18n.str`jump to template with the given template ID`}
palceholder={i18n.str`template id`}
/>
<ListPage
templates={result.data.templates}
onLoadMoreBefore={
@ -139,6 +147,6 @@ export default function ListTemplates({
</p>
</ConfirmModal>
)}
</Fragment>
</section>
);
}

View File

@ -73,7 +73,7 @@ export function ListPage({
>
<InputSelector
name="payto_uri"
label={i18n.str`Address`}
label={i18n.str`Account URI`}
values={accounts}
placeholder={i18n.str`Select one account`}
tooltip={i18n.str`filter by account address`}

View File

@ -125,11 +125,10 @@ function Table({
const [settings] = useSettings();
return (
<div class="table-container">
{onLoadMoreBefore && (
{hasMoreBefore && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more transfers before the first one`}
disabled={!hasMoreBefore}
onClick={onLoadMoreBefore}
>
<i18n.Translate>load newer transfers</i18n.Translate>
@ -198,11 +197,10 @@ function Table({
})}
</tbody>
</table>
{onLoadMoreAfter && (
{hasMoreAfter && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more transfer after the last one`}
disabled={!hasMoreAfter}
onClick={onLoadMoreAfter}
>
<i18n.Translate>load older transfers</i18n.Translate>

View File

@ -21,7 +21,7 @@
import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
import { Loading } from "../../../../components/exception/loading.js";
import { MerchantBackend } from "../../../../declaration.js";
import { useInstanceDetails } from "../../../../hooks/instance.js";
@ -47,7 +47,6 @@ export default function ListTransfer({
onCreate,
onNotFound,
}: Props): VNode {
const [form, setForm] = useState<Form>({ payto_uri: "" });
const setFilter = (s?: "yes" | "no") => setForm({ ...form, verified: s });
const [position, setPosition] = useState<string | undefined>(undefined);
@ -56,6 +55,14 @@ export default function ListTransfer({
const accounts = !instance.ok
? []
: instance.data.accounts.map((a) => a.payto_uri);
const [form, setForm] = useState<Form>({ payto_uri: "" });
const shoulUseDefaultAccount = accounts.length === 1
useEffect(() => {
if (shoulUseDefaultAccount) {
setForm({...form, payto_uri: accounts[0]})
}
}, [shoulUseDefaultAccount])
const isVerifiedTransfers = form.verified === "yes";
const isNonVerifiedTransfers = form.verified === "no";

View File

@ -118,6 +118,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
{state.otp_algorithm && state.otp_algorithm > 0 ? (
<Fragment>
<InputWithAddon<Entity>
expand
name="otp_key"
label={i18n.str`Device key`}
inputType={showKey ? "text" : "password"}

View File

@ -126,11 +126,10 @@ function Table({
const { i18n } = useTranslationContext();
return (
<div class="table-container">
{onLoadMoreBefore && (
{hasMoreBefore && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more devices before the first one`}
disabled={!hasMoreBefore}
onClick={onLoadMoreBefore}
>
<i18n.Translate>load newer devices</i18n.Translate>
@ -180,11 +179,10 @@ function Table({
})}
</tbody>
</table>
{onLoadMoreAfter && (
{hasMoreAfter && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more devices after the last one`}
disabled={!hasMoreAfter}
onClick={onLoadMoreAfter}
>
<i18n.Translate>load older devices</i18n.Translate>

View File

@ -126,11 +126,10 @@ function Table({
const { i18n } = useTranslationContext();
return (
<div class="table-container">
{onLoadMoreBefore && (
{hasMoreBefore && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more webhooks before the first one`}
disabled={!hasMoreBefore}
onClick={onLoadMoreBefore}
>
<i18n.Translate>load newer webhooks</i18n.Translate>
@ -187,11 +186,10 @@ function Table({
})}
</tbody>
</table>
{onLoadMoreAfter && (
{hasMoreAfter && (
<button
class="button is-fullwidth"
data-tooltip={i18n.str`load more webhooks after the last one`}
disabled={!hasMoreAfter}
onClick={onLoadMoreAfter}
>
<i18n.Translate>load older webhooks</i18n.Translate>

View File

@ -26,31 +26,15 @@ import { useBackendContext } from "../../context/backend.js";
import { useInstanceContext } from "../../context/instance.js";
import { AccessToken, LoginToken } from "../../declaration.js";
import { useCredentialsChecker } from "../../hooks/backend.js";
import { useBackendURL } from "../../hooks/index.js";
interface Props {
onConfirm: (token: LoginToken | undefined) => void;
}
function getTokenValuePart(t: string): string {
if (!t) return t;
const match = /secret-token:(.*)/.exec(t);
if (!match || !match[1]) return "";
return match[1];
}
function normalizeToken(r: string): AccessToken {
return `secret-token:${r}` as AccessToken;
}
function cleanUp(s: string): string {
let result = s;
if (result.indexOf("webui/") !== -1) {
result = result.substring(0, result.indexOf("webui/"));
}
return result;
}
export function LoginPage({ onConfirm }: Props): VNode {
const { url: backendURL, changeBackend, resetBackend } = useBackendContext();
const { admin, id } = useInstanceContext();
@ -245,11 +229,14 @@ function AsyncButton({ onClick, disabled, type = "", children }: { type?: string
export function ConnectionPage({ onConfirm }: { onConfirm: (s: string) => void }): VNode {
const { url: backendURL } = useBackendContext()
const [url, setURL] = useState(cleanUp(backendURL));
const [error, setError] = useState<string>();
const [url, setURL] = useState(backendURL ?? "");
const { i18n } = useTranslationContext();
async function doConnect() {
onConfirm(url)
const withHttp = url.startsWith("http") ? url : "https://" + url
onConfirm(withHttp)
}
return (