feat: add bond pages and portfolio navigation
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
export { section } from "./model/section";
|
||||
export { InstrumentChart } from "./ui/InstrumentChart.view";
|
||||
export { InstrumentEvents } from "./ui/InstrumentEvents.view";
|
||||
export { InstrumentPosition } from "./ui/InstrumentPosition.view";
|
||||
export { InstrumentSection } from "./ui/InstrumentSection.view";
|
||||
@@ -0,0 +1,25 @@
|
||||
type SectionState = {
|
||||
hasData: boolean;
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
refreshing: boolean;
|
||||
retry: () => void;
|
||||
};
|
||||
function section(query: {
|
||||
data: unknown;
|
||||
isError: boolean;
|
||||
isFetching: boolean;
|
||||
refetch: () => unknown;
|
||||
}): SectionState {
|
||||
return {
|
||||
hasData: Boolean(query.data),
|
||||
loading: !query.data && !query.isError,
|
||||
error: query.isError,
|
||||
refreshing: Boolean(query.data) && query.isFetching,
|
||||
retry: () => {
|
||||
void query.refetch();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { type SectionState, section };
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { BrokerageOperation } from "@/entities/broker-operation";
|
||||
import type { paths } from "@/shared/api";
|
||||
import type { SectionState } from "./section";
|
||||
|
||||
type Position =
|
||||
paths["/api/v1/stocks/{uid}/position"]["get"]["responses"][200]["content"]["application/json"];
|
||||
type Candles =
|
||||
paths["/api/v1/stocks/{uid}/candles"]["get"]["responses"][200]["content"]["application/json"];
|
||||
type ChartRow = Candles["candles"][number] & { date: string; price: number };
|
||||
type InstrumentPositionVm = {
|
||||
position?: Position;
|
||||
positionState: SectionState;
|
||||
};
|
||||
type InstrumentChartVm = {
|
||||
candles?: Candles;
|
||||
candlesState: SectionState;
|
||||
chartData: ChartRow[];
|
||||
priceRows: ChartRow[];
|
||||
period: string;
|
||||
setPeriod: (value: string) => void;
|
||||
averageLine: number | null;
|
||||
hasIncompleteCandle: boolean;
|
||||
position?: Position;
|
||||
};
|
||||
type InstrumentEventsVm = {
|
||||
operations?: { hasNext: boolean };
|
||||
operationsState: SectionState;
|
||||
filter: string;
|
||||
setFilter: (value: string) => void;
|
||||
eventRows: BrokerageOperation[];
|
||||
hasPrevious: boolean;
|
||||
onPrevious: () => void;
|
||||
onNext: () => void;
|
||||
};
|
||||
|
||||
export type { InstrumentChartVm, InstrumentEventsVm, InstrumentPositionVm };
|
||||
@@ -0,0 +1,187 @@
|
||||
import { AreaChart } from "@mantine/charts";
|
||||
import {
|
||||
Accordion,
|
||||
Badge,
|
||||
ScrollArea,
|
||||
SegmentedControl,
|
||||
Table,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatDecimal, formatMoney } from "@/shared/lib";
|
||||
import type { InstrumentChartVm } from "../model/types";
|
||||
import classes from "./InstrumentDetails.module.css";
|
||||
import { InstrumentSection } from "./InstrumentSection.view";
|
||||
|
||||
type InstrumentChartProps = { vm: InstrumentChartVm; priceUnit?: "percent" };
|
||||
const InstrumentChart: FC<InstrumentChartProps> = (props) => {
|
||||
const { vm } = props;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<InstrumentSection
|
||||
title={t("stock.chart")}
|
||||
state={vm.candlesState}
|
||||
large
|
||||
controls={
|
||||
<div className={classes.controls}>
|
||||
<SegmentedControl
|
||||
aria-label={t("stock.period")}
|
||||
value={vm.period}
|
||||
onChange={vm.setPeriod}
|
||||
data={["month", "halfYear", "year", "all"].map((value) => ({
|
||||
value,
|
||||
label: t(`stock.periods.${value}`),
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{vm.candles ? (
|
||||
vm.chartData.length ? (
|
||||
<>
|
||||
<AreaChart
|
||||
h={310}
|
||||
data={vm.chartData}
|
||||
dataKey="date"
|
||||
series={[
|
||||
{ name: "price", color: "blue.6", label: t("stock.price") },
|
||||
]}
|
||||
curveType="linear"
|
||||
withDots={vm.chartData.length === 1}
|
||||
withGradient
|
||||
gridAxis="y"
|
||||
tickLine="none"
|
||||
tooltipAnimationDuration={0}
|
||||
areaProps={{ isAnimationActive: false }}
|
||||
yAxisProps={{ domain: ["auto", "auto"], width: 64 }}
|
||||
referenceLines={
|
||||
vm.averageLine === null
|
||||
? []
|
||||
: [
|
||||
{
|
||||
y: vm.averageLine,
|
||||
ifOverflow: "extendDomain",
|
||||
color: "orange.7",
|
||||
label: t("stock.averageLine"),
|
||||
},
|
||||
]
|
||||
}
|
||||
valueFormatter={(value) =>
|
||||
new Intl.NumberFormat("ru-RU", {
|
||||
maximumFractionDigits: 4,
|
||||
}).format(value)
|
||||
}
|
||||
aria-label={t(
|
||||
props.priceUnit === "percent"
|
||||
? "bond.chartLabel"
|
||||
: "stock.chartLabel",
|
||||
{
|
||||
currency:
|
||||
props.priceUnit === "percent"
|
||||
? t("bond.percentOfNominal")
|
||||
: (vm.candles.currency ?? ""),
|
||||
},
|
||||
)}
|
||||
/>
|
||||
<Text size="xs" c="dimmed" mt="sm">
|
||||
{t(
|
||||
props.priceUnit === "percent"
|
||||
? "bond.chartNote"
|
||||
: "stock.chartNote",
|
||||
)}
|
||||
</Text>
|
||||
{vm.hasIncompleteCandle ? (
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("stock.incomplete")}
|
||||
</Text>
|
||||
) : null}
|
||||
{vm.averageLine !== null ? (
|
||||
<Text size="sm" mt="xs">
|
||||
{t("stock.averageLine")}:{" "}
|
||||
{formatMoney(vm.position?.averagePurchasePrice ?? null)}
|
||||
</Text>
|
||||
) : null}
|
||||
<Accordion
|
||||
className={classes.priceTable}
|
||||
variant="contained"
|
||||
radius="md"
|
||||
>
|
||||
<Accordion.Item value="prices">
|
||||
<Accordion.Control>
|
||||
<Text fw={600}>{t("stock.priceTable")}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("stock.priceTableHint", { count: vm.chartData.length })}
|
||||
</Text>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<ScrollArea.Autosize
|
||||
mah={360}
|
||||
type="auto"
|
||||
viewportProps={{
|
||||
tabIndex: 0,
|
||||
role: "region",
|
||||
"aria-label": t("stock.priceTable"),
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
striped
|
||||
highlightOnHover
|
||||
stickyHeader
|
||||
verticalSpacing="sm"
|
||||
horizontalSpacing="md"
|
||||
>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t("stock.date")}</Table.Th>
|
||||
<Table.Th>{t("stock.price")}</Table.Th>
|
||||
<Table.Th>{t("stock.candle")}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{vm.priceRows.map((candle) => (
|
||||
<Table.Tr key={candle.time}>
|
||||
<Table.Td>{candle.date}</Table.Td>
|
||||
<Table.Td>
|
||||
{props.priceUnit === "percent"
|
||||
? `${formatDecimal(candle.close)} %`
|
||||
: formatMoney(
|
||||
vm.candles?.currency
|
||||
? {
|
||||
value: candle.close,
|
||||
currency: vm.candles.currency,
|
||||
}
|
||||
: null,
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={candle.isComplete ? "gray" : "blue"}
|
||||
size="sm"
|
||||
>
|
||||
{t(
|
||||
candle.isComplete
|
||||
? "stock.complete"
|
||||
: "stock.forming",
|
||||
)}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</ScrollArea.Autosize>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</>
|
||||
) : (
|
||||
<Text c="dimmed">{t("stock.noCandles")}</Text>
|
||||
)
|
||||
) : null}
|
||||
</InstrumentSection>
|
||||
);
|
||||
};
|
||||
|
||||
export { InstrumentChart };
|
||||
@@ -0,0 +1,62 @@
|
||||
.section {
|
||||
padding: var(--mantine-spacing-lg);
|
||||
border-radius: var(--mantine-radius-lg);
|
||||
min-width: 0;
|
||||
}
|
||||
.metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: var(--mantine-spacing-lg);
|
||||
}
|
||||
.metricValue {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: var(--mantine-font-size-xl);
|
||||
font-weight: 600;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.holding {
|
||||
background: var(--mantine-color-blue-0);
|
||||
padding: var(--mantine-spacing-lg);
|
||||
border-radius: var(--mantine-radius-md);
|
||||
}
|
||||
.accountRow {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr repeat(4, minmax(0, 1fr));
|
||||
gap: var(--mantine-spacing-sm);
|
||||
padding: var(--mantine-spacing-md) 0;
|
||||
border-top: 1px solid var(--wl-color-border);
|
||||
align-items: start;
|
||||
}
|
||||
.controls {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.embeddedSection {
|
||||
margin-top: var(--mantine-spacing-lg);
|
||||
padding-top: var(--mantine-spacing-lg);
|
||||
border-top: 1px solid var(--wl-color-border);
|
||||
}
|
||||
.priceTable {
|
||||
margin-top: var(--mantine-spacing-md);
|
||||
}
|
||||
.priceTable table {
|
||||
min-width: 430px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.priceTable th:nth-child(2),
|
||||
.priceTable td:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
@media (max-width: 48em) {
|
||||
.section {
|
||||
padding: var(--mantine-spacing-md);
|
||||
}
|
||||
.metrics,
|
||||
.accountRow {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.accountRow > :first-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Button, Group, SegmentedControl, Stack, Text } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LedgerEntryRow } from "@/entities/broker-operation";
|
||||
import type { InstrumentEventsVm } from "../model/types";
|
||||
import classes from "./InstrumentDetails.module.css";
|
||||
import { InstrumentSection } from "./InstrumentSection.view";
|
||||
|
||||
type InstrumentEventsProps = {
|
||||
vm: InstrumentEventsVm;
|
||||
incomeLabel?: string;
|
||||
incomeFilter?: string;
|
||||
historyNote?: string;
|
||||
};
|
||||
|
||||
const InstrumentEvents: FC<InstrumentEventsProps> = (props) => {
|
||||
const { vm } = props;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<InstrumentSection
|
||||
title={t("stock.history")}
|
||||
state={vm.operationsState}
|
||||
controls={
|
||||
<div className={classes.controls}>
|
||||
<SegmentedControl
|
||||
aria-label={t("stock.eventFilter")}
|
||||
value={vm.filter}
|
||||
onChange={vm.setFilter}
|
||||
data={[
|
||||
"all",
|
||||
"trades",
|
||||
props.incomeFilter ?? "dividends",
|
||||
"other",
|
||||
].map((value) => ({
|
||||
value,
|
||||
label:
|
||||
value === (props.incomeFilter ?? "dividends") &&
|
||||
props.incomeLabel
|
||||
? props.incomeLabel
|
||||
: t(`stock.filters.${value}`),
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{vm.operations ? (
|
||||
<>
|
||||
<Text size="xs" c="dimmed">
|
||||
{props.historyNote ?? t("stock.historyNote")}
|
||||
</Text>
|
||||
{vm.eventRows.length ? (
|
||||
<Stack gap="sm" mt="md">
|
||||
{vm.eventRows.map((event) => (
|
||||
<LedgerEntryRow
|
||||
key={`${event.account?.id}:${event.id}`}
|
||||
operation={event}
|
||||
withDetails
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Text c="dimmed" py="lg">
|
||||
{t("stock.noOperations")}
|
||||
</Text>
|
||||
)}
|
||||
<Group justify="space-between" mt="md">
|
||||
<Button
|
||||
variant="default"
|
||||
disabled={!vm.hasPrevious || vm.operationsState.refreshing}
|
||||
onClick={vm.onPrevious}
|
||||
>
|
||||
{t("stock.previous")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
disabled={!vm.operations.hasNext || vm.operationsState.refreshing}
|
||||
onClick={vm.onNext}
|
||||
>
|
||||
{t("stock.next")}
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
) : null}
|
||||
</InstrumentSection>
|
||||
);
|
||||
};
|
||||
|
||||
export { InstrumentEvents };
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Alert, Anchor, Stack, Text } from "@mantine/core";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import { formatDecimal, formatMoney, formatSnapshotTime } from "@/shared/lib";
|
||||
import type { InstrumentPositionVm } from "../model/types";
|
||||
import classes from "./InstrumentDetails.module.css";
|
||||
import { InstrumentSection } from "./InstrumentSection.view";
|
||||
|
||||
type InstrumentPositionProps = {
|
||||
vm: InstrumentPositionVm;
|
||||
returnNote?: string;
|
||||
quantityLabel?: string;
|
||||
};
|
||||
const InstrumentPosition: FC<InstrumentPositionProps> = (props) => {
|
||||
const { vm } = props;
|
||||
const { t } = useTranslation();
|
||||
if (vm.position && !vm.position.accounts.length) return null;
|
||||
return (
|
||||
<InstrumentSection title={t("stock.position")} state={vm.positionState}>
|
||||
{vm.position ? (
|
||||
<Stack gap="md">
|
||||
{vm.position.isStale ? (
|
||||
<Alert color="orange">{t("portfolio.staleWarning")}</Alert>
|
||||
) : null}
|
||||
<div className={`${classes.metrics} ${classes.holding}`}>
|
||||
<div>
|
||||
<Text size="sm" c="dimmed">
|
||||
{props.quantityLabel ?? t("stock.quantity")}
|
||||
</Text>
|
||||
<Text className={classes.metricValue}>
|
||||
{formatDecimal(vm.position.quantity)}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("stock.value")}
|
||||
</Text>
|
||||
<Text className={classes.metricValue}>
|
||||
{formatMoney(vm.position.currentValue)}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("stock.average")}
|
||||
</Text>
|
||||
<Text className={classes.metricValue}>
|
||||
{formatMoney(vm.position.averagePurchasePrice)}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("stock.openReturn")}
|
||||
</Text>
|
||||
<Text
|
||||
className={classes.metricValue}
|
||||
c={
|
||||
vm.position.absoluteReturn?.value.startsWith("-")
|
||||
? "red.8"
|
||||
: "green.8"
|
||||
}
|
||||
>
|
||||
{formatMoney(vm.position.absoluteReturn)}
|
||||
</Text>
|
||||
{vm.position.returnPercent !== null ? (
|
||||
<Text size="sm">
|
||||
{formatDecimal(vm.position.returnPercent)} %
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Text size="xs" c="dimmed">
|
||||
{props.returnNote ?? t("stock.openReturnNote")}
|
||||
</Text>
|
||||
<div>
|
||||
{vm.position.accounts.map((account) => (
|
||||
<div className={classes.accountRow} key={account.account?.id}>
|
||||
{account.account ? (
|
||||
<Anchor
|
||||
component={Link}
|
||||
to={`/accounts/${encodeURIComponent(account.account.id)}`}
|
||||
>
|
||||
{account.account.name}
|
||||
</Anchor>
|
||||
) : (
|
||||
<Text>{t("stock.portfolio")}</Text>
|
||||
)}
|
||||
<div>
|
||||
<Text size="xs" c="dimmed">
|
||||
{props.quantityLabel ?? t("stock.quantity")}
|
||||
</Text>
|
||||
<Text>{formatDecimal(account.quantity)}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("stock.average")}
|
||||
</Text>
|
||||
<Text>{formatMoney(account.averagePurchasePrice)}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("stock.value")}
|
||||
</Text>
|
||||
<Text>{formatMoney(account.currentValue)}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("stock.openReturn")}
|
||||
</Text>
|
||||
<Text
|
||||
c={
|
||||
account.absoluteReturn?.value.startsWith("-")
|
||||
? "red.8"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{formatMoney(account.absoluteReturn)}
|
||||
</Text>
|
||||
{account.returnPercent !== null ? (
|
||||
<Text size="xs">
|
||||
{formatDecimal(account.returnPercent)} %
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("stock.snapshotAt", {
|
||||
date: formatSnapshotTime(vm.position.observedAt),
|
||||
})}
|
||||
</Text>
|
||||
</Stack>
|
||||
) : null}
|
||||
</InstrumentSection>
|
||||
);
|
||||
};
|
||||
|
||||
export { InstrumentPosition };
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Group,
|
||||
Paper,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import type { FC, PropsWithChildren, ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LoadingSkeleton } from "@/shared/ui";
|
||||
import type { SectionState } from "../model/section";
|
||||
import classes from "./InstrumentDetails.module.css";
|
||||
|
||||
type InstrumentSectionProps = PropsWithChildren<{
|
||||
title: string;
|
||||
state: SectionState;
|
||||
controls?: ReactNode;
|
||||
large?: boolean;
|
||||
embedded?: boolean;
|
||||
}>;
|
||||
const InstrumentSection: FC<InstrumentSectionProps> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Paper
|
||||
component="section"
|
||||
className={props.embedded ? classes.embeddedSection : classes.section}
|
||||
withBorder={!props.embedded}
|
||||
>
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={2} size="h3">
|
||||
{props.title}
|
||||
</Title>
|
||||
{props.controls}
|
||||
</Group>
|
||||
{props.state.refreshing ? (
|
||||
<Text role="status" size="xs" c="dimmed">
|
||||
{t("accounts.refreshing")}
|
||||
</Text>
|
||||
) : null}
|
||||
{props.state.error ? (
|
||||
<Alert color="orange" mb="sm" role="alert">
|
||||
<Group justify="space-between">
|
||||
<Text size="sm">
|
||||
{props.state.hasData
|
||||
? t("accounts.refreshError")
|
||||
: t("stock.sectionError")}
|
||||
</Text>
|
||||
<Button size="xs" variant="default" onClick={props.state.retry}>
|
||||
{t("accounts.retry")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Alert>
|
||||
) : null}
|
||||
{props.state.loading ? (
|
||||
<LoadingSkeleton
|
||||
label={t("stock.loadingSection", { section: props.title })}
|
||||
>
|
||||
<Stack>
|
||||
<Skeleton h={props.large ? 280 : 65} />
|
||||
<Skeleton h={14} w="60%" />
|
||||
</Stack>
|
||||
</LoadingSkeleton>
|
||||
) : (
|
||||
props.children
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export { InstrumentSection };
|
||||
@@ -22,7 +22,9 @@ function commitTbankConnectionBoundary(
|
||||
return (
|
||||
method === "get" &&
|
||||
typeof path === "string" &&
|
||||
(path === "/api/v1/stocks" ||
|
||||
(path === "/api/v1/bonds" ||
|
||||
path.startsWith("/api/v1/bonds/") ||
|
||||
path === "/api/v1/stocks" ||
|
||||
path.startsWith("/api/v1/stocks/") ||
|
||||
path === brokerageAccountsPath ||
|
||||
path.startsWith(`${brokerageAccountsPath}/`))
|
||||
|
||||
@@ -27,6 +27,11 @@ const stockQueryKey = apiQueryClient.queryOptions(
|
||||
"/api/v1/stocks/{uid}/position",
|
||||
{ params: { path: { uid: "e6123145-9665-43e0-8413-cd61b8aa9b13" } } },
|
||||
).queryKey;
|
||||
const bondQueryKey = apiQueryClient.queryOptions(
|
||||
"get",
|
||||
"/api/v1/bonds/{uid}/position",
|
||||
{ params: { path: { uid: "e6123145-9665-43e0-8413-cd61b8aa9b13" } } },
|
||||
).queryKey;
|
||||
const unrelatedQueryKey = apiQueryClient.queryOptions(
|
||||
"get",
|
||||
"/api/v1/auth/me",
|
||||
@@ -59,6 +64,7 @@ function seededClient() {
|
||||
]);
|
||||
client.setQueryData(portfolioQueryKey, { source: "previous-connection" });
|
||||
client.setQueryData(stockQueryKey, { source: "previous-connection" });
|
||||
client.setQueryData(bondQueryKey, { source: "previous-connection" });
|
||||
client.setQueryData(unrelatedQueryKey, { id: "user-1" });
|
||||
return client;
|
||||
}
|
||||
@@ -82,6 +88,7 @@ describe("TbankConnectionSection", () => {
|
||||
expect(client.getQueryData(brokerageAccountsQueryKey)).toBeUndefined();
|
||||
expect(client.getQueryData(portfolioQueryKey)).toBeUndefined();
|
||||
expect(client.getQueryData(stockQueryKey)).toBeUndefined();
|
||||
expect(client.getQueryData(bondQueryKey)).toBeUndefined();
|
||||
expect(client.getQueryData(unrelatedQueryKey)).toEqual({ id: "user-1" });
|
||||
});
|
||||
|
||||
@@ -134,6 +141,7 @@ describe("TbankConnectionSection", () => {
|
||||
expect(client.getQueryData(brokerageAccountsQueryKey)).toBeUndefined();
|
||||
expect(client.getQueryData(portfolioQueryKey)).toBeUndefined();
|
||||
expect(client.getQueryData(stockQueryKey)).toBeUndefined();
|
||||
expect(client.getQueryData(bondQueryKey)).toBeUndefined();
|
||||
expect(client.getQueryData(unrelatedQueryKey)).toEqual({ id: "user-1" });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user