more ui: pagination
This commit is contained in:
parent
0b2c03dc5e
commit
4041a76a58
@ -17,7 +17,7 @@
|
|||||||
import { createHashHistory } from "history";
|
import { createHashHistory } from "history";
|
||||||
import { VNode, h } from "preact";
|
import { VNode, h } from "preact";
|
||||||
import { Route, Router, route } from "preact-router";
|
import { Route, Router, route } from "preact-router";
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect, useErrorBoundary } from "preact/hooks";
|
||||||
import { BankFrame } from "../pages/BankFrame.js";
|
import { BankFrame } from "../pages/BankFrame.js";
|
||||||
import { BusinessAccount } from "../pages/business/Home.js";
|
import { BusinessAccount } from "../pages/business/Home.js";
|
||||||
import { HomePage, WithdrawalOperationPage } from "../pages/HomePage.js";
|
import { HomePage, WithdrawalOperationPage } from "../pages/HomePage.js";
|
||||||
@ -27,6 +27,7 @@ import { useBackendContext } from "../context/backend.js";
|
|||||||
import { LoginForm } from "../pages/LoginForm.js";
|
import { LoginForm } from "../pages/LoginForm.js";
|
||||||
import { AdminHome } from "../pages/admin/Home.js";
|
import { AdminHome } from "../pages/admin/Home.js";
|
||||||
import { bankUiSettings } from "../settings.js";
|
import { bankUiSettings } from "../settings.js";
|
||||||
|
import { notifyError } from "@gnu-taler/web-util/browser";
|
||||||
|
|
||||||
export function Routing(): VNode {
|
export function Routing(): VNode {
|
||||||
const history = createHashHistory();
|
const history = createHashHistory();
|
||||||
|
@ -46,6 +46,8 @@ export namespace State {
|
|||||||
status: "ready";
|
status: "ready";
|
||||||
error: undefined;
|
error: undefined;
|
||||||
transactions: Transaction[];
|
transactions: Transaction[];
|
||||||
|
onPrev?: () => void;
|
||||||
|
onNext?: () => void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util";
|
import { AbsoluteTime, Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
|
||||||
import { useTransactions } from "../../hooks/access.js";
|
import { useTransactions } from "../../hooks/access.js";
|
||||||
import { Props, State, Transaction } from "./index.js";
|
import { Props, State, Transaction } from "./index.js";
|
||||||
|
|
||||||
@ -34,45 +34,19 @@ export function useComponentState({ account }: Props): State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const transactions = result.data.transactions
|
const transactions = result.data.transactions
|
||||||
.map((item: unknown) => {
|
.map((tx) => {
|
||||||
if (
|
|
||||||
!item ||
|
|
||||||
typeof item !== "object" ||
|
|
||||||
!("direction" in item) ||
|
|
||||||
!("creditorIban" in item) ||
|
|
||||||
!("debtorIban" in item) ||
|
|
||||||
!("date" in item) ||
|
|
||||||
!("subject" in item) ||
|
|
||||||
!("currency" in item) ||
|
|
||||||
!("amount" in item)
|
|
||||||
) {
|
|
||||||
//not valid
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const anyItem = item as any;
|
|
||||||
if (
|
|
||||||
!(typeof anyItem.creditorIban === "string") ||
|
|
||||||
!(typeof anyItem.debtorIban === "string") ||
|
|
||||||
!(typeof anyItem.date === "string") ||
|
|
||||||
!(typeof anyItem.subject === "string") ||
|
|
||||||
!(typeof anyItem.currency === "string") ||
|
|
||||||
!(typeof anyItem.amount === "string")
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const negative = anyItem.direction === "DEBIT";
|
const negative = tx.direction === "debit";
|
||||||
const counterpart = negative ? anyItem.creditorIban : anyItem.debtorIban;
|
const cp = parsePaytoUri(negative ? tx.creditor_payto_uri : tx.debtor_payto_uri);
|
||||||
|
const counterpart = (cp === undefined || !cp.isKnown ? undefined :
|
||||||
|
cp.targetType === "iban" ? cp.iban :
|
||||||
|
cp.targetType === "x-taler-bank" ? cp.account :
|
||||||
|
cp.targetType === "bitcoin" ? `${cp.targetPath.substring(0, 6)}...` : undefined) ??
|
||||||
|
"unkown";
|
||||||
|
|
||||||
let date = anyItem.date ? parseInt(anyItem.date, 10) : 0;
|
const when = AbsoluteTime.fromMilliseconds(tx.date / 1000);
|
||||||
if (isNaN(date) || !isFinite(date)) {
|
const amount = Amounts.parse(tx.amount);
|
||||||
date = 0;
|
const subject = tx.subject;
|
||||||
}
|
|
||||||
const when: AbsoluteTime = !date
|
|
||||||
? AbsoluteTime.never()
|
|
||||||
: AbsoluteTime.fromMilliseconds(date);
|
|
||||||
const amount = Amounts.parse(`${anyItem.currency}:${anyItem.amount}`);
|
|
||||||
const subject = anyItem.subject;
|
|
||||||
return {
|
return {
|
||||||
negative,
|
negative,
|
||||||
counterpart,
|
counterpart,
|
||||||
@ -87,5 +61,7 @@ export function useComponentState({ account }: Props): State {
|
|||||||
status: "ready",
|
status: "ready",
|
||||||
error: undefined,
|
error: undefined,
|
||||||
transactions,
|
transactions,
|
||||||
|
onNext: result.isReachingEnd ? undefined : result.loadMore,
|
||||||
|
onPrev: result.isReachingStart ? undefined : result.loadMorePrev,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReadyView({ transactions }: State.Ready): VNode {
|
export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
if (!transactions.length) return <div />
|
if (!transactions.length) return <div />
|
||||||
const txByDate = transactions.reduce((prev, cur) => {
|
const txByDate = transactions.reduce((prev, cur) => {
|
||||||
@ -50,7 +50,7 @@ export function ReadyView({ transactions }: State.Ready): VNode {
|
|||||||
<h1 class="text-base font-semibold leading-6 text-gray-900"><i18n.Translate>Latest transactions</i18n.Translate></h1>
|
<h1 class="text-base font-semibold leading-6 text-gray-900"><i18n.Translate>Latest transactions</i18n.Translate></h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="-mx-4 mt-5 ring-1 ring-gray-300 sm:mx-0 sm:rounded-lg min-w-fit bg-white">
|
<div class="-mx-4 mt-5 ring-1 ring-gray-300 sm:mx-0 rounded-lg min-w-fit bg-white">
|
||||||
<table class="min-w-full divide-y divide-gray-300">
|
<table class="min-w-full divide-y divide-gray-300">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -89,9 +89,30 @@ export function ReadyView({ transactions }: State.Ready): VNode {
|
|||||||
</tr>)
|
</tr>)
|
||||||
})}
|
})}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<nav class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg" aria-label="Pagination">
|
||||||
|
<div class="flex flex-1 justify-between sm:justify-end">
|
||||||
|
<button
|
||||||
|
class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
|
||||||
|
disabled={!onPrev}
|
||||||
|
onClick={onPrev}
|
||||||
|
>
|
||||||
|
<i18n.Translate>First page</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
|
||||||
|
disabled={!onNext}
|
||||||
|
onClick={onNext}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Next</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
21
packages/demobank-ui/src/declaration.d.ts
vendored
21
packages/demobank-ui/src/declaration.d.ts
vendored
@ -194,28 +194,25 @@ namespace SandboxBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface BankAccountTransactionInfo {
|
interface BankAccountTransactionInfo {
|
||||||
creditorIban: string;
|
creditor_payto_uri: string;
|
||||||
creditorBic: string; // Optional
|
debtor_payto_uri: string;
|
||||||
creditorName: string;
|
|
||||||
|
|
||||||
debtorIban: string;
|
amount: Amount;
|
||||||
debtorBic: string;
|
direction: "debit" | "credit";
|
||||||
debtorName: string;
|
|
||||||
|
|
||||||
amount: number;
|
|
||||||
currency: string;
|
|
||||||
subject: string;
|
subject: string;
|
||||||
|
|
||||||
// Transaction unique ID. Matches
|
// Transaction unique ID. Matches
|
||||||
// $transaction_id from the URI.
|
// $transaction_id from the URI.
|
||||||
uid: string;
|
row_id: number;
|
||||||
direction: "DBIT" | "CRDT";
|
date: number;
|
||||||
date: string; // milliseconds since the Unix epoch
|
// date: Timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateBankAccountTransactionCreate {
|
interface CreateBankAccountTransactionCreate {
|
||||||
// Address in the Payto format of the wire transfer receiver.
|
// Address in the Payto format of the wire transfer receiver.
|
||||||
// It needs at least the 'message' query string parameter.
|
// It needs at least the 'message' query string parameter.
|
||||||
paytoUri: string;
|
payto_uri: string;
|
||||||
|
|
||||||
// Transaction amount (in the $currency:x.y format), optional.
|
// Transaction amount (in the $currency:x.y format), optional.
|
||||||
// However, when not given, its value must occupy the 'amount'
|
// However, when not given, its value must occupy the 'amount'
|
||||||
|
@ -255,7 +255,7 @@ export function useTransactionDetails(
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface PaginationFilter {
|
interface PaginationFilter {
|
||||||
page: number;
|
// page: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePublicAccounts(
|
export function usePublicAccounts(
|
||||||
@ -275,7 +275,7 @@ export function usePublicAccounts(
|
|||||||
} = useSWR<
|
} = useSWR<
|
||||||
HttpResponseOk<SandboxBackend.CoreBank.PublicAccountsResponse>,
|
HttpResponseOk<SandboxBackend.CoreBank.PublicAccountsResponse>,
|
||||||
RequestError<SandboxBackend.SandboxError>
|
RequestError<SandboxBackend.SandboxError>
|
||||||
>([`public-accounts`, args?.page, PAGE_SIZE], paginatedFetcher);
|
>([`public-accounts`, page, PAGE_SIZE], paginatedFetcher);
|
||||||
|
|
||||||
const [lastAfter, setLastAfter] = useState<
|
const [lastAfter, setLastAfter] = useState<
|
||||||
HttpResponse<
|
HttpResponse<
|
||||||
@ -334,7 +334,7 @@ export function useTransactions(
|
|||||||
> {
|
> {
|
||||||
const { paginatedFetcher } = useAuthenticatedBackend();
|
const { paginatedFetcher } = useAuthenticatedBackend();
|
||||||
|
|
||||||
const [page, setPage] = useState(1);
|
const [start, setStart] = useState<string>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: afterData,
|
data: afterData,
|
||||||
@ -344,7 +344,7 @@ export function useTransactions(
|
|||||||
HttpResponseOk<SandboxBackend.CoreBank.BankAccountTransactionsResponse>,
|
HttpResponseOk<SandboxBackend.CoreBank.BankAccountTransactionsResponse>,
|
||||||
RequestError<SandboxBackend.SandboxError>
|
RequestError<SandboxBackend.SandboxError>
|
||||||
>(
|
>(
|
||||||
[`accounts/${account}/transactions`, args?.page, PAGE_SIZE],
|
[`accounts/${account}/transactions`, start, PAGE_SIZE],
|
||||||
paginatedFetcher, {
|
paginatedFetcher, {
|
||||||
refreshInterval: 0,
|
refreshInterval: 0,
|
||||||
refreshWhenHidden: false,
|
refreshWhenHidden: false,
|
||||||
@ -374,19 +374,24 @@ export function useTransactions(
|
|||||||
// 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.transactions.length < PAGE_SIZE;
|
afterData && afterData.data.transactions.length < PAGE_SIZE;
|
||||||
const isReachingStart = false;
|
const isReachingStart = start == undefined;
|
||||||
|
|
||||||
const pagination = {
|
const pagination = {
|
||||||
isReachingEnd,
|
isReachingEnd,
|
||||||
isReachingStart,
|
isReachingStart,
|
||||||
loadMore: () => {
|
loadMore: () => {
|
||||||
if (!afterData || isReachingEnd) return;
|
if (!afterData || isReachingEnd) return;
|
||||||
if (afterData.data.transactions.length < MAX_RESULT_SIZE) {
|
// if (afterData.data.transactions.length < MAX_RESULT_SIZE) {
|
||||||
setPage(page + 1);
|
// console.log("load more", page)
|
||||||
}
|
const l = afterData.data.transactions[afterData.data.transactions.length-1]
|
||||||
|
setStart(String(l.row_id));
|
||||||
|
// }
|
||||||
},
|
},
|
||||||
loadMorePrev: () => {
|
loadMorePrev: () => {
|
||||||
null;
|
if (!afterData || isReachingStart) return;
|
||||||
|
// if (afterData.data.transactions.length < MAX_RESULT_SIZE) {
|
||||||
|
setStart(undefined)
|
||||||
|
// }
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ interface useBackendType {
|
|||||||
fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
|
fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
|
||||||
multiFetcher: <T>(endpoint: string[][]) => Promise<HttpResponseOk<T>[]>;
|
multiFetcher: <T>(endpoint: string[][]) => Promise<HttpResponseOk<T>[]>;
|
||||||
paginatedFetcher: <T>(
|
paginatedFetcher: <T>(
|
||||||
args: [string, number, number],
|
args: [string, string | undefined, number],
|
||||||
) => Promise<HttpResponseOk<T>>;
|
) => Promise<HttpResponseOk<T>>;
|
||||||
sandboxAccountsFetcher: <T>(
|
sandboxAccountsFetcher: <T>(
|
||||||
args: [string, number, number, string],
|
args: [string, number, number, string],
|
||||||
@ -188,13 +188,15 @@ export function usePublicBackend(): useBackendType {
|
|||||||
[baseUrl],
|
[baseUrl],
|
||||||
);
|
);
|
||||||
const paginatedFetcher = useCallback(
|
const paginatedFetcher = useCallback(
|
||||||
function fetcherImpl<T>([endpoint, page, size]: [
|
function fetcherImpl<T>([endpoint, start, size]: [
|
||||||
string,
|
string,
|
||||||
number,
|
string | undefined,
|
||||||
number,
|
number,
|
||||||
]): Promise<HttpResponseOk<T>> {
|
]): Promise<HttpResponseOk<T>> {
|
||||||
|
const delta = -1 * size //descending order
|
||||||
|
const params = start ? { delta, start } : {delta}
|
||||||
return requestHandler<T>(baseUrl, endpoint, {
|
return requestHandler<T>(baseUrl, endpoint, {
|
||||||
params: { page: page || 1, size },
|
params,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[baseUrl],
|
[baseUrl],
|
||||||
@ -280,14 +282,16 @@ export function useAuthenticatedBackend(): useBackendType {
|
|||||||
[baseUrl, creds],
|
[baseUrl, creds],
|
||||||
);
|
);
|
||||||
const paginatedFetcher = useCallback(
|
const paginatedFetcher = useCallback(
|
||||||
function fetcherImpl<T>([endpoint, page = 1, size]: [
|
function fetcherImpl<T>([endpoint, start, size]: [
|
||||||
string,
|
string,
|
||||||
number,
|
string | undefined,
|
||||||
number,
|
number,
|
||||||
]): Promise<HttpResponseOk<T>> {
|
]): Promise<HttpResponseOk<T>> {
|
||||||
|
const delta = -1 * size //descending order
|
||||||
|
const params = start ? { delta, start } : {delta}
|
||||||
return requestHandler<T>(baseUrl, endpoint, {
|
return requestHandler<T>(baseUrl, endpoint, {
|
||||||
token: creds,
|
token: creds,
|
||||||
params: { delta: size, start: size * page },
|
params,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[baseUrl, creds],
|
[baseUrl, creds],
|
||||||
|
@ -49,7 +49,7 @@ export function useComponentState({ account, goToBusinessAccount, goToConfirmOpe
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (result.status === HttpStatusCode.Unauthorized) {
|
if (result.status === HttpStatusCode.Unauthorized) {
|
||||||
notifyError(i18n.str`Require login`, undefined);
|
notifyError(i18n.str`Authorization denied`, i18n.str`Maybe the session has expired, login again.`);
|
||||||
return {
|
return {
|
||||||
status: "error-user-not-found",
|
status: "error-user-not-found",
|
||||||
error: result,
|
error: result,
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Amounts, Logger, PaytoUriIBAN, TranslatedString, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
|
import { Amounts, Logger, PaytoUriIBAN, TranslatedString, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
|
||||||
import { useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { notifyError, useNotifications, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { ComponentChildren, Fragment, h, VNode } from "preact";
|
import { ComponentChildren, Fragment, h, VNode } from "preact";
|
||||||
import { StateUpdater, useEffect, useState } from "preact/hooks";
|
import { StateUpdater, useEffect, useErrorBoundary, useState } from "preact/hooks";
|
||||||
import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js";
|
import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { useBackendContext } from "../context/backend.js";
|
||||||
import { useBusinessAccountDetails } from "../hooks/circuit.js";
|
import { useBusinessAccountDetails } from "../hooks/circuit.js";
|
||||||
@ -50,6 +50,15 @@ export function BankFrame({
|
|||||||
const [settings, updateSettings] = useSettings();
|
const [settings, updateSettings] = useSettings();
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const [error, resetError] = useErrorBoundary();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
notifyError(i18n.str`Internal error, please report.`, (error instanceof Error ? error.message : String(error)) as TranslatedString)
|
||||||
|
resetError()
|
||||||
|
}
|
||||||
|
}, [error])
|
||||||
|
|
||||||
const demo_sites = [];
|
const demo_sites = [];
|
||||||
for (const i in bankUiSettings.demoSites)
|
for (const i in bankUiSettings.demoSites)
|
||||||
demo_sites.push(
|
demo_sites.push(
|
||||||
@ -355,7 +364,9 @@ function StatusBanner(): VNode {
|
|||||||
</div>
|
</div>
|
||||||
<div class="ml-3 flex-1 md:flex md:justify-between">
|
<div class="ml-3 flex-1 md:flex md:justify-between">
|
||||||
<p class="text-sm font-medium text-red-800">{n.message.title}</p>
|
<p class="text-sm font-medium text-red-800">{n.message.title}</p>
|
||||||
<p class="mt-3 text-sm md:ml-6 md:mt-0">
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm">
|
||||||
<button type="button" class="inline-flex font-semibold items-center rounded bg-white px-2 py-1 text-xs text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
|
<button type="button" class="inline-flex font-semibold items-center rounded bg-white px-2 py-1 text-xs text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -68,7 +68,7 @@ export function HomePage({
|
|||||||
account={account}
|
account={account}
|
||||||
goToConfirmOperation={goToConfirmOperation}
|
goToConfirmOperation={goToConfirmOperation}
|
||||||
goToBusinessAccount={goToBusinessAccount}
|
goToBusinessAccount={goToBusinessAccount}
|
||||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
onLoadNotOk={handleNotOkResult(i18n)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -112,7 +112,6 @@ export function WithdrawalOperationPage({
|
|||||||
|
|
||||||
export function handleNotOkResult(
|
export function handleNotOkResult(
|
||||||
i18n: ReturnType<typeof useTranslationContext>["i18n"],
|
i18n: ReturnType<typeof useTranslationContext>["i18n"],
|
||||||
onRegister?: () => void,
|
|
||||||
): <T>(
|
): <T>(
|
||||||
result:
|
result:
|
||||||
| HttpResponsePaginated<T, SandboxBackend.SandboxError>
|
| HttpResponsePaginated<T, SandboxBackend.SandboxError>
|
||||||
@ -133,7 +132,7 @@ export function handleNotOkResult(
|
|||||||
case ErrorType.CLIENT: {
|
case ErrorType.CLIENT: {
|
||||||
if (result.status === HttpStatusCode.Unauthorized) {
|
if (result.status === HttpStatusCode.Unauthorized) {
|
||||||
notifyError(i18n.str`Wrong credentials`, undefined);
|
notifyError(i18n.str`Wrong credentials`, undefined);
|
||||||
return <LoginForm onRegister={onRegister} />;
|
return <LoginForm />;
|
||||||
}
|
}
|
||||||
const errorData = result.payload;
|
const errorData = result.payload;
|
||||||
notify({
|
notify({
|
||||||
|
@ -24,6 +24,7 @@ import { useCredentialsChecker } from "../hooks/useCredentialsChecker.js";
|
|||||||
import { bankUiSettings } from "../settings.js";
|
import { bankUiSettings } from "../settings.js";
|
||||||
import { undefinedIfEmpty } from "../utils.js";
|
import { undefinedIfEmpty } from "../utils.js";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect and submit login data.
|
* Collect and submit login data.
|
||||||
*/
|
*/
|
||||||
@ -35,7 +36,6 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
|
|||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const { requestNewLoginToken, refreshLoginToken } = useCredentialsChecker();
|
const { requestNewLoginToken, refreshLoginToken } = useCredentialsChecker();
|
||||||
|
|
||||||
// const testLogin = useCredentialsCheckerOld();
|
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
useEffect(function focusInput() {
|
useEffect(function focusInput() {
|
||||||
ref.current?.focus();
|
ref.current?.focus();
|
||||||
@ -120,6 +120,13 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
|
|||||||
setBusy(undefined)
|
setBusy(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register form may be shown in the initialization step.
|
||||||
|
* If this is an error when usgin the app the registration
|
||||||
|
* callback is not set
|
||||||
|
*/
|
||||||
|
const isSessionExpired = !onRegister
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<h1 class="nav"></h1>
|
<h1 class="nav"></h1>
|
||||||
@ -128,9 +135,9 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
|
|||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
<div class="flex min-h-full flex-col justify-center">
|
<div class="flex min-h-full flex-col justify-center">
|
||||||
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
{/* <div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
<h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h2>
|
<h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h2>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
<form class="space-y-6" noValidate
|
<form class="space-y-6" noValidate
|
||||||
@ -151,8 +158,9 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
|
|||||||
type="text"
|
type="text"
|
||||||
name="username"
|
name="username"
|
||||||
id="username"
|
id="username"
|
||||||
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
value={username ?? ""}
|
value={username ?? ""}
|
||||||
|
disabled={isSessionExpired}
|
||||||
enterkeyhint="next"
|
enterkeyhint="next"
|
||||||
placeholder="identification"
|
placeholder="identification"
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
@ -194,7 +202,28 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{isSessionExpired ? <div class="flex justify-between">
|
||||||
|
<button type="submit"
|
||||||
|
class="rounded-md bg-white-600 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
doLogin()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Cancel</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="submit"
|
||||||
|
class="rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
|
disabled={!!errors}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
doLogin()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Renew session</i18n.Translate>
|
||||||
|
</button>
|
||||||
|
</div> : <div>
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="flex w-full justify-center rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
class="flex w-full justify-center rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
disabled={!!errors}
|
disabled={!!errors}
|
||||||
@ -205,7 +234,7 @@ export function LoginForm({ onRegister }: { onRegister?: () => void }): VNode {
|
|||||||
>
|
>
|
||||||
<i18n.Translate>Log in</i18n.Translate>
|
<i18n.Translate>Log in</i18n.Translate>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{bankUiSettings.allowRegistrations && onRegister &&
|
{bankUiSettings.allowRegistrations && onRegister &&
|
||||||
|
@ -317,8 +317,9 @@ export function WithdrawalConfirmationQuestion({
|
|||||||
</div>
|
</div>
|
||||||
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
<dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
|
<dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
|
||||||
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">To be added</dd>
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||||
{/* Amounts.stringifyValue(details.amount) */}
|
{Amounts.stringifyValue(details.amount)}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,7 +13,7 @@ export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode
|
|||||||
const result = useAccountDetails(account);
|
const result = useAccountDetails(account);
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
return handleNotOkResult(i18n, onRegister)(result);
|
return handleNotOkResult(i18n)(result);
|
||||||
}
|
}
|
||||||
const { data } = result;
|
const { data } = result;
|
||||||
|
|
||||||
|
@ -8,17 +8,16 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
|||||||
interface Props {
|
interface Props {
|
||||||
onAction: (type: AccountAction, account: string) => void;
|
onAction: (type: AccountAction, account: string) => void;
|
||||||
account: string | undefined;
|
account: string | undefined;
|
||||||
onRegister: () => void;
|
|
||||||
onCreateAccount: () => void;
|
onCreateAccount: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AccountList({ account, onAction, onCreateAccount, onRegister }: Props): VNode {
|
export function AccountList({ account, onAction, onCreateAccount }: Props): VNode {
|
||||||
const result = useBusinessAccounts({ account });
|
const result = useBusinessAccounts({ account });
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
if (result.loading) return <div />;
|
if (result.loading) return <div />;
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
return handleNotOkResult(i18n, onRegister)(result);
|
return handleNotOkResult(i18n)(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { customers } = result.data;
|
const { customers } = result.data;
|
||||||
|
@ -37,7 +37,7 @@ export function AdminHome({ onRegister }: Props): VNode {
|
|||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "show-cashouts-details": return <ShowCashoutDetails
|
case "show-cashouts-details": return <ShowCashoutDetails
|
||||||
id={action.account}
|
id={action.account}
|
||||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
onLoadNotOk={handleNotOkResult(i18n)}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setAction(undefined);
|
setAction(undefined);
|
||||||
}}
|
}}
|
||||||
@ -73,7 +73,7 @@ export function AdminHome({ onRegister }: Props): VNode {
|
|||||||
)
|
)
|
||||||
case "update-password": return <UpdateAccountPassword
|
case "update-password": return <UpdateAccountPassword
|
||||||
account={action.account}
|
account={action.account}
|
||||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
onLoadNotOk={handleNotOkResult(i18n)}
|
||||||
onUpdateSuccess={() => {
|
onUpdateSuccess={() => {
|
||||||
notifyInfo(i18n.str`Password changed`);
|
notifyInfo(i18n.str`Password changed`);
|
||||||
setAction(undefined);
|
setAction(undefined);
|
||||||
@ -84,7 +84,7 @@ export function AdminHome({ onRegister }: Props): VNode {
|
|||||||
/>
|
/>
|
||||||
case "remove-account": return <RemoveAccount
|
case "remove-account": return <RemoveAccount
|
||||||
account={action.account}
|
account={action.account}
|
||||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
onLoadNotOk={handleNotOkResult(i18n)}
|
||||||
onUpdateSuccess={() => {
|
onUpdateSuccess={() => {
|
||||||
notifyInfo(i18n.str`Account removed`);
|
notifyInfo(i18n.str`Account removed`);
|
||||||
setAction(undefined);
|
setAction(undefined);
|
||||||
@ -95,7 +95,7 @@ export function AdminHome({ onRegister }: Props): VNode {
|
|||||||
/>
|
/>
|
||||||
case "show-details": return <ShowAccountDetails
|
case "show-details": return <ShowAccountDetails
|
||||||
account={action.account}
|
account={action.account}
|
||||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
onLoadNotOk={handleNotOkResult(i18n)}
|
||||||
onChangePassword={() => {
|
onChangePassword={() => {
|
||||||
setAction({
|
setAction({
|
||||||
type: "update-password",
|
type: "update-password",
|
||||||
|
@ -75,7 +75,7 @@ export function BusinessAccount({
|
|||||||
return (
|
return (
|
||||||
<CreateCashout
|
<CreateCashout
|
||||||
account={account}
|
account={account}
|
||||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
onLoadNotOk={handleNotOkResult(i18n)}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setNewcashout(false);
|
setNewcashout(false);
|
||||||
}}
|
}}
|
||||||
@ -93,7 +93,7 @@ export function BusinessAccount({
|
|||||||
return (
|
return (
|
||||||
<ShowCashoutDetails
|
<ShowCashoutDetails
|
||||||
id={showCashoutDetails}
|
id={showCashoutDetails}
|
||||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
onLoadNotOk={handleNotOkResult(i18n)}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setShowCashoutDetails(undefined);
|
setShowCashoutDetails(undefined);
|
||||||
}}
|
}}
|
||||||
@ -104,7 +104,7 @@ export function BusinessAccount({
|
|||||||
return (
|
return (
|
||||||
<UpdateAccountPassword
|
<UpdateAccountPassword
|
||||||
account={account}
|
account={account}
|
||||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
onLoadNotOk={handleNotOkResult(i18n)}
|
||||||
onUpdateSuccess={() => {
|
onUpdateSuccess={() => {
|
||||||
notifyInfo(i18n.str`Password changed`);
|
notifyInfo(i18n.str`Password changed`);
|
||||||
setUpdatePassword(false);
|
setUpdatePassword(false);
|
||||||
@ -119,7 +119,7 @@ export function BusinessAccount({
|
|||||||
<div>
|
<div>
|
||||||
<ShowAccountDetails
|
<ShowAccountDetails
|
||||||
account={account}
|
account={account}
|
||||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
onLoadNotOk={handleNotOkResult(i18n)}
|
||||||
onUpdateSuccess={() => {
|
onUpdateSuccess={() => {
|
||||||
notifyInfo(i18n.str`Account updated`);
|
notifyInfo(i18n.str`Account updated`);
|
||||||
}}
|
}}
|
||||||
|
Loading…
Reference in New Issue
Block a user