feat: add quiet reading with rare confirmations
Part of the quiet-user-feedback change; docs and specs land in their own repositories.
This commit is contained in:
@@ -147,7 +147,7 @@ describe("banking API", () => {
|
||||
expect(url.searchParams.get("from")).toBe("2026-09-01");
|
||||
expect(url.searchParams.get("to")).toBe("2026-09-30");
|
||||
if (isFail && page === 2)
|
||||
return HttpResponse.json({ code: "FAILED" }, { status: 500 });
|
||||
return HttpResponse.json({ code: "NOT_FOUND" }, { status: 404 });
|
||||
|
||||
return HttpResponse.json({
|
||||
hasNext: page === 1,
|
||||
@@ -299,7 +299,7 @@ describe("banking API", () => {
|
||||
expect(result.current.query.data?.balance.value).toBe("100");
|
||||
server.use(
|
||||
http.get("*/api/v1/bank-cards/a", () =>
|
||||
HttpResponse.json({ code: "INTERNAL_ERROR" }, { status: 500 }),
|
||||
HttpResponse.json({ code: "NOT_FOUND" }, { status: 404 }),
|
||||
),
|
||||
);
|
||||
await act(() => result.current.query.refetch());
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -71,7 +72,7 @@ function useBankCardFormVm(props: BankCardFormProps) {
|
||||
openingDate: values.openingDate,
|
||||
};
|
||||
|
||||
if (props.card)
|
||||
if (props.card) {
|
||||
await updateCard.mutateAsync({
|
||||
body: {
|
||||
...metadata,
|
||||
@@ -80,12 +81,19 @@ function useBankCardFormVm(props: BankCardFormProps) {
|
||||
},
|
||||
id: props.card.id,
|
||||
});
|
||||
else
|
||||
}
|
||||
else {
|
||||
await cardCreation.mutateAsync({
|
||||
id: intent,
|
||||
...metadata,
|
||||
...financial,
|
||||
});
|
||||
notifications.show({
|
||||
id: "bank-card-created-" + intent,
|
||||
message: t("bank.cardSaved"),
|
||||
});
|
||||
}
|
||||
|
||||
setOpened(false);
|
||||
}
|
||||
catch (error_) {
|
||||
|
||||
@@ -47,7 +47,6 @@ const meta
|
||||
error: false,
|
||||
fetching: false,
|
||||
loading: false,
|
||||
retry: () => {},
|
||||
},
|
||||
component: BankSummaryView,
|
||||
parameters: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CSSProperties, FC } from "react";
|
||||
|
||||
import { Button, Group, Skeleton, Stack, Text, Title } from "@mantine/core";
|
||||
import { Skeleton, Stack, Text, Title } from "@mantine/core";
|
||||
import { OperationIcon } from "@wealthledger/icons";
|
||||
import { LoadingSkeleton } from "@wealthledger/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -20,7 +20,6 @@ type BankSummaryViewProps = {
|
||||
error: boolean;
|
||||
fetching: boolean;
|
||||
loading: boolean;
|
||||
retry: () => void;
|
||||
};
|
||||
|
||||
const BankSummaryViewCategories: FC<BankSummaryViewProps> = (props) => {
|
||||
@@ -101,17 +100,7 @@ const BankSummaryView: FC<BankSummaryViewProps> = (props) => {
|
||||
|
||||
<BankSummaryViewCategories {...props} />
|
||||
|
||||
{props.error && (
|
||||
<Group role="alert">
|
||||
<Text>{t(props.data ? "bank.refreshError" : "bank.error")}</Text>
|
||||
|
||||
<Button onClick={props.retry} variant="default">
|
||||
{t("bank.retry")}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{props.data && props.fetching && <Text role="status">{t("bank.refreshing")}</Text>}
|
||||
{props.error && !props.data && <Text role="alert">{t("bank.error")}</Text>}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { hashKey, useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "@/shared/api";
|
||||
|
||||
@@ -29,15 +29,28 @@ function useBrokerageOperationHistory(
|
||||
cursor: cursors.at(-1),
|
||||
});
|
||||
|
||||
const isInvalidCursor = isApiErrorCode(
|
||||
apiErrorCode(operations.error),
|
||||
ApiErrorCode.InvalidOperationsCursor,
|
||||
);
|
||||
|
||||
const isRecoveringInvalidCursor = cursors.length > 0 && isInvalidCursor;
|
||||
|
||||
const reset = () => setPagination({ cursors: [], scope });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRecoveringInvalidCursor) return;
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
setPagination({ cursors: [], scope });
|
||||
void client.invalidateQueries({ exact: true, queryKey });
|
||||
}, 0);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [client, isRecoveringInvalidCursor, queryKey, scope]);
|
||||
|
||||
const retry = (retryScreen: () => unknown) => {
|
||||
if (
|
||||
isApiErrorCode(
|
||||
apiErrorCode(operations.error),
|
||||
ApiErrorCode.InvalidOperationsCursor,
|
||||
)
|
||||
) {
|
||||
if (isInvalidCursor) {
|
||||
reset();
|
||||
void client.invalidateQueries({
|
||||
exact: true,
|
||||
@@ -49,6 +62,7 @@ function useBrokerageOperationHistory(
|
||||
|
||||
return {
|
||||
hasPrevious: cursors.length > 0,
|
||||
isRecoveringInvalidCursor,
|
||||
onNext: () => {
|
||||
if (operations.data?.nextCursor)
|
||||
setPagination({
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { Alert, Button, Skeleton, Stack, Text } from "@mantine/core";
|
||||
import { Button, Skeleton, Stack, Text } from "@mantine/core";
|
||||
import { LoadingSkeleton } from "@wealthledger/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import { SourceSyncStatus } from "@/shared/ui";
|
||||
|
||||
import type { BrokerageEvent } from "../../api/useBrokerageEvents";
|
||||
|
||||
import { brokerageEventKey } from "../../model/event-key";
|
||||
@@ -19,9 +17,6 @@ type BrokerageEventsPreviewProps = {
|
||||
type BrokerageEventsPreviewState
|
||||
= | {
|
||||
items: BrokerageEvent[];
|
||||
lastSuccessfulAt?: null | string;
|
||||
refreshError?: boolean;
|
||||
stale?: boolean;
|
||||
state: "loaded" | "partial";
|
||||
to: string;
|
||||
}
|
||||
@@ -33,28 +28,15 @@ const BrokerageEventsPreview: FC<BrokerageEventsPreviewProps> = (props) => {
|
||||
const { events } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const errorText
|
||||
= events.state === "loaded" && events.refreshError
|
||||
? t("accounts.refreshError")
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
{events.state === "loaded" && (
|
||||
<SourceSyncStatus
|
||||
errorText={errorText}
|
||||
lastSuccessfulAt={events.lastSuccessfulAt}
|
||||
stale={events.stale}
|
||||
/>
|
||||
)}
|
||||
|
||||
{events.state === "loading" && (
|
||||
<LoadingSkeleton label={t("events.recentLoading")}>
|
||||
{["first", "second", "third"].map(placeholder => <Skeleton height={48} key={placeholder} radius="sm" />)}
|
||||
</LoadingSkeleton>
|
||||
)}
|
||||
|
||||
{events.state === "error" && <Alert color="red">{t("events.recentError")}</Alert>}
|
||||
{events.state === "error" && <Text c="dimmed">{t("states.unavailableText")}</Text>}
|
||||
|
||||
{events.state === "empty" && <Text c="dimmed">{t("events.recentEmpty")}</Text>}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user