refactor: enforce frontend view model boundaries
This commit is contained in:
@@ -8,19 +8,24 @@ import { BreadcrumbTrail } from "./BreadcrumbTrail";
|
||||
|
||||
type AccountBreadcrumbsProps = {
|
||||
accountId: string;
|
||||
section?: "events" | "income" | "operations";
|
||||
section?: "events"
|
||||
| "income"
|
||||
| "operations";
|
||||
};
|
||||
|
||||
const AccountBreadcrumbs: FC<AccountBreadcrumbsProps> = (props) => {
|
||||
const { accountId, section } = props;
|
||||
const { data: accounts } = useBrokerageAccounts();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const accountName
|
||||
= accountId === "all"
|
||||
? t("accounts.all")
|
||||
: (accounts?.find(account => account.id === accountId)?.name
|
||||
?? t("breadcrumbs.accountFallback"));
|
||||
|
||||
const accountPath = `/accounts/${accountId}`;
|
||||
|
||||
const items = [
|
||||
{ label: t("breadcrumbs.accounts"), to: "/accounts" },
|
||||
section ? { label: accountName, to: accountPath } : { label: accountName },
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { Select } from "@mantine/core";
|
||||
|
||||
import { useAccountScopeSwitchVm } from "./AccountScopeSwitch.vm";
|
||||
|
||||
type AccountScopeSwitchProps = { accountId: string; section?: string };
|
||||
|
||||
const AccountScopeSwitch: FC<AccountScopeSwitchProps> = (props) => {
|
||||
const { accountId, data, onChange, t } = useAccountScopeSwitchVm(props);
|
||||
|
||||
return (
|
||||
<Select
|
||||
allowDeselect={false}
|
||||
data={data}
|
||||
label={t("accounts.scope")}
|
||||
maw={360}
|
||||
onChange={onChange}
|
||||
value={accountId}
|
||||
w="100%"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { AccountScopeSwitch };
|
||||
+19
-25
@@ -1,6 +1,3 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { Select } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
|
||||
@@ -8,12 +5,13 @@ import { useBrokerageAccounts } from "@/entities/brokerage-account";
|
||||
|
||||
type AccountScopeSwitchProps = { accountId: string; section?: string };
|
||||
|
||||
const AccountScopeSwitch: FC<AccountScopeSwitchProps> = (props) => {
|
||||
function useAccountScopeSwitchVm(props: AccountScopeSwitchProps) {
|
||||
const { accountId, section } = props;
|
||||
const { data: accounts } = useBrokerageAccounts();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { search } = useLocation();
|
||||
|
||||
const data = [
|
||||
{ label: t("accounts.all"), value: "all" },
|
||||
...(accounts ?? []).map(account => ({
|
||||
@@ -21,27 +19,23 @@ const AccountScopeSwitch: FC<AccountScopeSwitchProps> = (props) => {
|
||||
value: account.id,
|
||||
})),
|
||||
];
|
||||
|
||||
if (data.every(item => item.value !== accountId))
|
||||
data.push({ label: t("breadcrumbs.accountFallback"), value: accountId });
|
||||
return (
|
||||
<Select
|
||||
allowDeselect={false}
|
||||
data={data}
|
||||
label={t("accounts.scope")}
|
||||
maw={360}
|
||||
onChange={(value) => {
|
||||
if (!value || value === accountId) return;
|
||||
const filters = new URLSearchParams(search);
|
||||
filters.delete("cursor");
|
||||
void navigate({
|
||||
pathname: ["/accounts", encodeURIComponent(value), section].filter(Boolean).join("/"),
|
||||
search: filters.toString(),
|
||||
});
|
||||
}}
|
||||
value={accountId}
|
||||
w="100%"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { AccountScopeSwitch };
|
||||
const onChange: (value: null | string) => void = (value) => {
|
||||
if (!value || value === accountId) return;
|
||||
const filters = new URLSearchParams(search);
|
||||
filters.delete("cursor");
|
||||
void navigate({
|
||||
pathname: ["/accounts", encodeURIComponent(value), section]
|
||||
.filter(Boolean)
|
||||
.join("/"),
|
||||
search: filters.toString(),
|
||||
});
|
||||
};
|
||||
|
||||
return { accountId, data, onChange, t };
|
||||
}
|
||||
|
||||
export { useAccountScopeSwitchVm };
|
||||
@@ -16,6 +16,7 @@ function renderBreadcrumbs(
|
||||
accounts?: { id: string; name: string }[],
|
||||
) {
|
||||
useBrokerageAccounts.mockReturnValue({ data: accounts });
|
||||
|
||||
return render(
|
||||
<MantineProvider>
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
@@ -70,6 +71,7 @@ describe("AppBreadcrumbs", () => {
|
||||
"page",
|
||||
);
|
||||
expect(screen.getByRole("navigation")).toHaveAttribute("data-depth", "3");
|
||||
|
||||
for (const separator of screen.getAllByText("→")) {
|
||||
expect(separator).toHaveAttribute("aria-hidden", "true");
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useMatch } from "react-router";
|
||||
|
||||
import { AccountBreadcrumbs } from "./AccountBreadcrumbs";
|
||||
import { AccountScopeSwitch } from "./AccountScopeSwitch";
|
||||
import { AccountScopeSwitch } from "./AccountScopeSwitch/AccountScopeSwitch";
|
||||
import { BondBreadcrumbs } from "./BondBreadcrumbs";
|
||||
import { BreadcrumbTrail } from "./BreadcrumbTrail";
|
||||
import { CardBreadcrumbs } from "./CardBreadcrumbs";
|
||||
@@ -13,10 +13,12 @@ import { StockBreadcrumbs } from "./StockBreadcrumbs";
|
||||
|
||||
const AppBreadcrumbs: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const operations = useMatch({
|
||||
end: true,
|
||||
path: "/accounts/:accountId/operations",
|
||||
});
|
||||
|
||||
const events = useMatch({ end: true, path: "/accounts/:accountId/events" });
|
||||
const income = useMatch({ end: true, path: "/accounts/:accountId/income" });
|
||||
const account = useMatch({ end: true, path: "/accounts/:accountId" });
|
||||
@@ -37,6 +39,7 @@ const AppBreadcrumbs: FC = () => {
|
||||
accountId={operations.params.accountId ?? ""}
|
||||
section="operations"
|
||||
/>
|
||||
|
||||
<AccountScopeSwitch
|
||||
accountId={operations.params.accountId ?? ""}
|
||||
section="operations"
|
||||
@@ -50,6 +53,7 @@ const AppBreadcrumbs: FC = () => {
|
||||
accountId={events.params.accountId ?? ""}
|
||||
section="events"
|
||||
/>
|
||||
|
||||
<AccountScopeSwitch
|
||||
accountId={events.params.accountId ?? ""}
|
||||
section="events"
|
||||
@@ -63,6 +67,7 @@ const AppBreadcrumbs: FC = () => {
|
||||
accountId={income.params.accountId ?? ""}
|
||||
section="income"
|
||||
/>
|
||||
|
||||
<AccountScopeSwitch
|
||||
accountId={income.params.accountId ?? ""}
|
||||
section="income"
|
||||
@@ -73,6 +78,7 @@ const AppBreadcrumbs: FC = () => {
|
||||
return (
|
||||
<Stack gap={0} mb="md">
|
||||
<AccountBreadcrumbs accountId={account.params.accountId ?? ""} />
|
||||
|
||||
<AccountScopeSwitch accountId={account.params.accountId ?? ""} />
|
||||
</Stack>
|
||||
);
|
||||
@@ -80,7 +86,12 @@ const AppBreadcrumbs: FC = () => {
|
||||
if (cards) return <BreadcrumbTrail items={[{ label: t("bank.title") }]} />;
|
||||
if (accounts)
|
||||
return <BreadcrumbTrail items={[{ label: t("breadcrumbs.accounts") }]} />;
|
||||
return settings ? <BreadcrumbTrail items={[{ label: t("breadcrumbs.settings") }]} /> : undefined;
|
||||
|
||||
return settings
|
||||
? (
|
||||
<BreadcrumbTrail items={[{ label: t("breadcrumbs.settings") }]} />
|
||||
)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
export { AppBreadcrumbs };
|
||||
|
||||
@@ -7,9 +7,11 @@ import { useBond } from "@/entities/bond";
|
||||
import { BreadcrumbTrail } from "./BreadcrumbTrail";
|
||||
|
||||
type BondBreadcrumbsProps = { identifier: string };
|
||||
|
||||
const BondBreadcrumbs: FC<BondBreadcrumbsProps> = (props) => {
|
||||
const { data } = useBond(props.identifier);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<BreadcrumbTrail
|
||||
items={[
|
||||
|
||||
@@ -7,11 +7,13 @@ import { Link } from "react-router";
|
||||
import classes from "./AppBreadcrumbs.module.css";
|
||||
|
||||
type BreadcrumbTrailProps = { items: Crumb[] };
|
||||
|
||||
type Crumb = { label: string; to?: string };
|
||||
|
||||
const BreadcrumbTrail: FC<BreadcrumbTrailProps> = (props) => {
|
||||
const { items } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Breadcrumbs
|
||||
aria-label={t("breadcrumbs.label")}
|
||||
|
||||
@@ -7,9 +7,11 @@ import { useBankCard } from "@/entities/bank-card";
|
||||
import { BreadcrumbTrail } from "./BreadcrumbTrail";
|
||||
|
||||
type CardBreadcrumbsProps = { identifier: string };
|
||||
|
||||
const CardBreadcrumbs: FC<CardBreadcrumbsProps> = (props) => {
|
||||
const { data } = useBankCard(props.identifier);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<BreadcrumbTrail
|
||||
items={[
|
||||
|
||||
@@ -7,9 +7,11 @@ import { useStock } from "@/entities/stock";
|
||||
import { BreadcrumbTrail } from "./BreadcrumbTrail";
|
||||
|
||||
type StockBreadcrumbsProps = { identifier: string };
|
||||
|
||||
const StockBreadcrumbs: FC<StockBreadcrumbsProps> = (props) => {
|
||||
const { data } = useStock(props.identifier);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<BreadcrumbTrail
|
||||
items={[
|
||||
|
||||
Reference in New Issue
Block a user