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, "private": true,
"name": "@gnu-taler/merchant-backoffice-ui", "name": "@gnu-taler/merchant-backoffice-ui",
"version": "0.1.0", "version": "0.9.3-dev.27",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

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

View File

@ -80,7 +80,7 @@ import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js";
export enum InstancePaths { export enum InstancePaths {
error = "/error", error = "/error",
server = "/server", settings = "/settings",
token = "/token", token = "/token",
bank_list = "/bank", bank_list = "/bank",
@ -118,7 +118,7 @@ export enum InstancePaths {
validators_update = "/validators/:vid/update", validators_update = "/validators/:vid/update",
validators_new = "/validators/new", validators_new = "/validators/new",
settings = "/interface", interface = "/interface",
} }
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
@ -255,7 +255,7 @@ export function InstanceRoutes({
instance={id} instance={id}
admin={admin} admin={admin}
onShowSettings={() => { onShowSettings={() => {
route(InstancePaths.settings) route(InstancePaths.interface)
}} }}
path={path} path={path}
onLogout={clearTokenAndGoToRoot} onLogout={clearTokenAndGoToRoot}
@ -320,7 +320,7 @@ export function InstanceRoutes({
* Update instance page * Update instance page
*/} */}
<Route <Route
path={InstancePaths.server} path={InstancePaths.settings}
component={InstanceUpdatePage} component={InstanceUpdatePage}
onBack={() => { onBack={() => {
route(`/`); route(`/`);
@ -353,7 +353,7 @@ export function InstanceRoutes({
path={InstancePaths.inventory_list} path={InstancePaths.inventory_list}
component={ProductListPage} component={ProductListPage}
onUnauthorized={LoginPageAccessDenied} onUnauthorized={LoginPageAccessDenied}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => { onCreate={() => {
route(InstancePaths.inventory_new); route(InstancePaths.inventory_new);
}} }}
@ -392,7 +392,7 @@ export function InstanceRoutes({
path={InstancePaths.bank_list} path={InstancePaths.bank_list}
component={BankAccountListPage} component={BankAccountListPage}
onUnauthorized={LoginPageAccessDenied} onUnauthorized={LoginPageAccessDenied}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => { onCreate={() => {
route(InstancePaths.bank_new); route(InstancePaths.bank_new);
}} }}
@ -437,7 +437,7 @@ export function InstanceRoutes({
route(InstancePaths.order_details.replace(":oid", id)); route(InstancePaths.order_details.replace(":oid", id));
}} }}
onUnauthorized={LoginPageAccessDenied} onUnauthorized={LoginPageAccessDenied}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
/> />
<Route <Route
@ -468,7 +468,7 @@ export function InstanceRoutes({
component={TransferListPage} component={TransferListPage}
onUnauthorized={LoginPageAccessDenied} onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => { onCreate={() => {
route(InstancePaths.transfers_new); route(InstancePaths.transfers_new);
}} }}
@ -491,7 +491,7 @@ export function InstanceRoutes({
component={WebhookListPage} component={WebhookListPage}
onUnauthorized={LoginPageAccessDenied} onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => { onCreate={() => {
route(InstancePaths.webhooks_new); route(InstancePaths.webhooks_new);
}} }}
@ -530,7 +530,7 @@ export function InstanceRoutes({
component={ValidatorListPage} component={ValidatorListPage}
onUnauthorized={LoginPageAccessDenied} onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => { onCreate={() => {
route(InstancePaths.validators_new); route(InstancePaths.validators_new);
}} }}
@ -569,7 +569,7 @@ export function InstanceRoutes({
component={TemplateListPage} component={TemplateListPage}
onUnauthorized={LoginPageAccessDenied} onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onCreate={() => { onCreate={() => {
route(InstancePaths.templates_new); route(InstancePaths.templates_new);
}} }}
@ -638,7 +638,7 @@ export function InstanceRoutes({
component={ReservesListPage} component={ReservesListPage}
onUnauthorized={LoginPageAccessDenied} onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onLoadError={ServerErrorRedirectTo(InstancePaths.settings)}
onSelect={(id: string) => { onSelect={(id: string) => {
route(InstancePaths.reserves_details.replace(":rid", id)); 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>} {error && <p class="help is-danger">{error}</p>}
<span class="has-text-grey">{help}</span> <span class="has-text-grey">{help}</span>
</div> </div>
{side} {expand ? <div>{side}</div> : side}
</div> </div>
</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> </a>
</li> </li>
<li> <li>
<a href={"/server"} class="has-icon"> <a href={"/settings"} class="has-icon">
<span class="icon"> <span class="icon">
<i class="mdi mdi-square-edit-outline" /> <i class="mdi mdi-square-edit-outline" />
</span> </span>
<span class="menu-item-label"> <span class="menu-item-label">
<i18n.Translate>Server</i18n.Translate> <i18n.Translate>Settings</i18n.Translate>
</span> </span>
</a> </a>
</li> </li>

View File

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

View File

@ -91,7 +91,7 @@ const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000;
const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000; const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000;
export function useBackendConfig(): HttpResponse< export function useBackendConfig(): HttpResponse<
MerchantBackend.VersionResponse, MerchantBackend.VersionResponse | undefined,
RequestError<MerchantBackend.ErrorDetail> RequestError<MerchantBackend.ErrorDetail>
> { > {
const { request } = useBackendBaseRequest(); const { request } = useBackendBaseRequest();
@ -340,6 +340,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
if (refunded !== undefined) params.refunded = refunded; if (refunded !== undefined) params.refunded = refunded;
if (wired !== undefined) params.wired = wired; if (wired !== undefined) params.wired = wired;
if (date_s !== undefined) params.date_s = date_s; 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 }); return requestHandler<T>(baseUrl, endpoint, { params, token });
}, },
[baseUrl, token], [baseUrl, token],
@ -385,6 +393,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
const params: any = {}; const params: any = {};
if (payto_uri !== undefined) params.payto_uri = payto_uri; if (payto_uri !== undefined) params.payto_uri = payto_uri;
if (verified !== undefined) params.verified = verified; 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) { if (delta !== undefined) {
params.limit = delta; params.limit = delta;
} }
@ -403,6 +419,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
): Promise<HttpResponseOk<T>> { ): Promise<HttpResponseOk<T>> {
const [endpoint, position, delta] = args const [endpoint, position, delta] = args
const params: any = {}; 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) { if (delta !== undefined) {
params.limit = delta; params.limit = delta;
} }
@ -421,6 +445,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
): Promise<HttpResponseOk<T>> { ): Promise<HttpResponseOk<T>> {
const [endpoint, position, delta] = args const [endpoint, position, delta] = args
const params: any = {}; 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) { if (delta !== undefined) {
params.limit = delta; params.limit = delta;
} }

View File

@ -32,7 +32,13 @@ const calculateRootPath = () => {
? window.location.origin + window.location.pathname ? 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>() const loginTokenCodec = buildCodecForObject<LoginToken>()

View File

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

View File

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

View File

@ -21,7 +21,7 @@
import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns"; import { format } from "date-fns";
import { h, VNode } from "preact"; import { h, VNode, Fragment } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { DatePicker } from "../../../../components/picker/DatePicker.js"; import { DatePicker } from "../../../../components/picker/DatePicker.js";
import { MerchantBackend, WithId } from "../../../../declaration.js"; import { MerchantBackend, WithId } from "../../../../declaration.js";
@ -29,8 +29,6 @@ import { CardTable } from "./Table.js";
import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
export interface ListPageProps { export interface ListPageProps {
errorOrderId: string | undefined;
onShowAll: () => void; onShowAll: () => void;
onShowNotPaid: () => void; onShowNotPaid: () => void;
onShowPaid: () => void; onShowPaid: () => void;
@ -56,17 +54,18 @@ export interface ListPageProps {
onSelectOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void; onSelectOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
onRefundOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void; onRefundOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void;
onSearchOrderById: (id: string) => void;
onCreate: () => void; onCreate: () => void;
} }
export function ListPage({ export function ListPage({
hasMoreAfter,
hasMoreBefore,
onLoadMoreAfter,
onLoadMoreBefore,
orders, orders,
errorOrderId,
isAllActive, isAllActive,
onSelectOrder, onSelectOrder,
onRefundOrder, onRefundOrder,
onSearchOrderById,
jumpToDate, jumpToDate,
onCopyURL, onCopyURL,
onShowAll, onShowAll,
@ -86,42 +85,10 @@ export function ListPage({
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const dateTooltip = i18n.str`select date to show nearby orders`; const dateTooltip = i18n.str`select date to show nearby orders`;
const [pickDate, setPickDate] = useState(false); const [pickDate, setPickDate] = useState(false);
const [orderId, setOrderId] = useState<string>("");
const [settings] = useSettings(); const [settings] = useSettings();
return ( return (
<section class="section is-main-section"> <Fragment>
<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>
<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" }}>
@ -249,7 +216,11 @@ export function ListPage({
onCopyURL={onCopyURL} onCopyURL={onCopyURL}
onSelect={onSelectOrder} onSelect={onSelectOrder}
onRefund={onRefundOrder} onRefund={onRefundOrder}
hasMoreAfter={hasMoreAfter}
hasMoreBefore={hasMoreBefore}
onLoadMoreAfter={onLoadMoreAfter}
onLoadMoreBefore={onLoadMoreBefore}
/> />
</section> </Fragment>
); );
} }

View File

@ -140,10 +140,9 @@ function Table({
const [settings] = useSettings(); const [settings] = useSettings();
return ( return (
<div class="table-container"> <div class="table-container">
{onLoadMoreBefore && ( {hasMoreBefore && (
<button <button
class="button is-fullwidth" class="button is-fullwidth"
disabled={!hasMoreBefore}
onClick={onLoadMoreBefore} onClick={onLoadMoreBefore}
> >
<i18n.Translate>load newer orders</i18n.Translate> <i18n.Translate>load newer orders</i18n.Translate>
@ -218,10 +217,9 @@ function Table({
})} })}
</tbody> </tbody>
</table> </table>
{onLoadMoreAfter && ( {hasMoreAfter && (
<button <button
class="button is-fullwidth" class="button is-fullwidth"
disabled={!hasMoreAfter}
onClick={onLoadMoreAfter} onClick={onLoadMoreAfter}
> >
<i18n.Translate>load older orders</i18n.Translate> <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 { ListPage } from "./ListPage.js";
import { RefundModal } from "./Table.js"; import { RefundModal } from "./Table.js";
import { HttpStatusCode } from "@gnu-taler/taler-util"; import { HttpStatusCode } from "@gnu-taler/taler-util";
import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
interface Props { interface Props {
onUnauthorized: () => VNode; onUnauthorized: () => VNode;
@ -69,9 +70,6 @@ export default function OrderList({
const [notif, setNotif] = useState<Notification | undefined>(undefined); const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const [errorOrderId, setErrorOrderId] = useState<string | undefined>(
undefined,
);
if (result.loading) return <Loading />; if (result.loading) return <Loading />;
if (!result.ok) { if (!result.ok) {
@ -100,24 +98,17 @@ export default function OrderList({
? "is-active" ? "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 ( return (
<Fragment> <section class="section is-main-section">
<NotificationCard notification={notif} /> <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 <ListPage
orders={result.data.orders.map((o) => ({ ...o, id: o.order_id }))} orders={result.data.orders.map((o) => ({ ...o, id: o.order_id }))}
onLoadMoreBefore={result.loadMorePrev} onLoadMoreBefore={result.loadMorePrev}
@ -126,7 +117,6 @@ export default function OrderList({
hasMoreAfter={!result.isReachingEnd} hasMoreAfter={!result.isReachingEnd}
onSelectOrder={(order) => onSelect(order.id)} onSelectOrder={(order) => onSelect(order.id)}
onRefundOrder={(value) => setOrderToBeRefunded(value)} onRefundOrder={(value) => setOrderToBeRefunded(value)}
errorOrderId={errorOrderId}
isAllActive={isAllActive} isAllActive={isAllActive}
isNotWiredActive={isNotWiredActive} isNotWiredActive={isNotWiredActive}
isWiredActive={isWiredActive} isWiredActive={isWiredActive}
@ -138,7 +128,6 @@ export default function OrderList({
getPaymentURL(id).then((resp) => copyToClipboard(resp.data)) getPaymentURL(id).then((resp) => copyToClipboard(resp.data))
} }
onCreate={onCreate} onCreate={onCreate}
onSearchOrderById={testIfOrderExistAndSelect}
onSelectDate={setNewDate} onSelectDate={setNewDate}
onShowAll={() => setFilter({})} onShowAll={() => setFilter({})}
onShowNotPaid={() => setFilter({ paid: "no" })} 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" }} style={{ cursor: "pointer" }}
> >
<span style={{"whiteSpace":"nowrap"}}>
{i.total_sold} {i.unit} {i.total_sold} {i.unit}
</span>
</td> </td>
<td class="is-actions-cell right-sticky"> <td class="is-actions-cell right-sticky">
<div class="buttons is-right"> <div class="buttons is-right">
@ -341,7 +344,7 @@ function FastProductWithInfiniteStockUpdateForm({
<div class="buttons mt-5"> <div class="buttons mt-5">
<button class="button " onClick={onCancel}> <button class="button mt-5" onClick={onCancel}>
<i18n.Translate>Clone</i18n.Translate> <i18n.Translate>Clone</i18n.Translate>
</button> </button>
</div> </div>

View File

@ -37,6 +37,7 @@ import { Notification } from "../../../../utils/types.js";
import { CardTable } from "./Table.js"; import { CardTable } from "./Table.js";
import { HttpStatusCode } from "@gnu-taler/taler-util"; import { HttpStatusCode } from "@gnu-taler/taler-util";
import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js"; import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js";
import { JumpToElementById } from "../../../../components/form/JumpToElementById.js";
interface Props { interface Props {
onUnauthorized: () => VNode; onUnauthorized: () => VNode;
@ -74,60 +75,17 @@ export default function ProductList({
return onNotFound(); return onNotFound();
return onLoadError(result); 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 ( return (
<section class="section is-main-section"> <section class="section is-main-section">
<NotificationCard notification={notif} /> <NotificationCard notification={notif} />
<div class="level"> <JumpToElementById
<div class="level-left"> testIfExist={getProduct}
<div class="level-item"> onSelect={onSelect}
<div class="field has-addons"> description={i18n.str`jump to product with the given product ID`}
<div class="control"> palceholder={i18n.str`product id`}
<input />
class={errorId ? "input is-danger" : "input"}
type="text"
value={productId ?? ""}
onChange={(e) => setProductId(e.currentTarget.value)}
placeholder={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 <CardTable
instances={result.data} instances={result.data}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@
import { ErrorType, HttpError } from "@gnu-taler/web-util/browser"; import { ErrorType, HttpError } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { Loading } from "../../../../components/exception/loading.js"; import { Loading } from "../../../../components/exception/loading.js";
import { MerchantBackend } from "../../../../declaration.js"; import { MerchantBackend } from "../../../../declaration.js";
import { useInstanceDetails } from "../../../../hooks/instance.js"; import { useInstanceDetails } from "../../../../hooks/instance.js";
@ -47,7 +47,6 @@ export default function ListTransfer({
onCreate, onCreate,
onNotFound, onNotFound,
}: Props): VNode { }: Props): VNode {
const [form, setForm] = useState<Form>({ payto_uri: "" });
const setFilter = (s?: "yes" | "no") => setForm({ ...form, verified: s }); const setFilter = (s?: "yes" | "no") => setForm({ ...form, verified: s });
const [position, setPosition] = useState<string | undefined>(undefined); const [position, setPosition] = useState<string | undefined>(undefined);
@ -56,6 +55,14 @@ export default function ListTransfer({
const accounts = !instance.ok const accounts = !instance.ok
? [] ? []
: instance.data.accounts.map((a) => a.payto_uri); : 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 isVerifiedTransfers = form.verified === "yes";
const isNonVerifiedTransfers = form.verified === "no"; 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 ? ( {state.otp_algorithm && state.otp_algorithm > 0 ? (
<Fragment> <Fragment>
<InputWithAddon<Entity> <InputWithAddon<Entity>
expand
name="otp_key" name="otp_key"
label={i18n.str`Device key`} label={i18n.str`Device key`}
inputType={showKey ? "text" : "password"} inputType={showKey ? "text" : "password"}

View File

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

View File

@ -126,11 +126,10 @@ function Table({
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
return ( return (
<div class="table-container"> <div class="table-container">
{onLoadMoreBefore && ( {hasMoreBefore && (
<button <button
class="button is-fullwidth" class="button is-fullwidth"
data-tooltip={i18n.str`load more webhooks before the first one`} data-tooltip={i18n.str`load more webhooks before the first one`}
disabled={!hasMoreBefore}
onClick={onLoadMoreBefore} onClick={onLoadMoreBefore}
> >
<i18n.Translate>load newer webhooks</i18n.Translate> <i18n.Translate>load newer webhooks</i18n.Translate>
@ -187,11 +186,10 @@ function Table({
})} })}
</tbody> </tbody>
</table> </table>
{onLoadMoreAfter && ( {hasMoreAfter && (
<button <button
class="button is-fullwidth" class="button is-fullwidth"
data-tooltip={i18n.str`load more webhooks after the last one`} data-tooltip={i18n.str`load more webhooks after the last one`}
disabled={!hasMoreAfter}
onClick={onLoadMoreAfter} onClick={onLoadMoreAfter}
> >
<i18n.Translate>load older webhooks</i18n.Translate> <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 { useInstanceContext } from "../../context/instance.js";
import { AccessToken, LoginToken } from "../../declaration.js"; import { AccessToken, LoginToken } from "../../declaration.js";
import { useCredentialsChecker } from "../../hooks/backend.js"; import { useCredentialsChecker } from "../../hooks/backend.js";
import { useBackendURL } from "../../hooks/index.js";
interface Props { interface Props {
onConfirm: (token: LoginToken | undefined) => void; 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 { function normalizeToken(r: string): AccessToken {
return `secret-token:${r}` as 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 { export function LoginPage({ onConfirm }: Props): VNode {
const { url: backendURL, changeBackend, resetBackend } = useBackendContext(); const { url: backendURL, changeBackend, resetBackend } = useBackendContext();
const { admin, id } = useInstanceContext(); 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 { export function ConnectionPage({ onConfirm }: { onConfirm: (s: string) => void }): VNode {
const { url: backendURL } = useBackendContext() const { url: backendURL } = useBackendContext()
const [url, setURL] = useState(cleanUp(backendURL)); const [error, setError] = useState<string>();
const [url, setURL] = useState(backendURL ?? "");
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
async function doConnect() { async function doConnect() {
onConfirm(url) const withHttp = url.startsWith("http") ? url : "https://" + url
onConfirm(withHttp)
} }
return ( return (