backoffice ui
This commit is contained in:
parent
97d7be7503
commit
98013322db
@ -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": {
|
||||
|
@ -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]);
|
||||
|
@ -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));
|
||||
}}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
@ -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>
|
||||
|
@ -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 "";
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>()
|
||||
|
@ -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 };
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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`}
|
||||
|
@ -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>
|
||||
|
@ -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";
|
||||
|
@ -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"}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 (
|
||||
|
Loading…
Reference in New Issue
Block a user