refactor: normalize view models and share breadcrumbs and titles (WealthLedger/frontend#143)
This commit is contained in:
@@ -26,7 +26,7 @@ import { resource } from "@/test/resource";
|
||||
import { server } from "@/test/server";
|
||||
|
||||
import { AccountEventsPage } from "./AccountEventsPage";
|
||||
import { useAccountEventsPageViewModel } from "./AccountEventsPage.vm";
|
||||
import { useAccountEventsPageVm } from "./AccountEventsPage.vm";
|
||||
|
||||
const Navigation: FC = () => {
|
||||
const location = useLocation();
|
||||
@@ -100,7 +100,7 @@ describe("Upcoming event date filters", () => {
|
||||
);
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const { result } = renderHook(() => useAccountEventsPageViewModel(), {
|
||||
const { result } = renderHook(() => useAccountEventsPageVm(), {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={["?from=2099-02-01"]}>
|
||||
@@ -146,7 +146,7 @@ describe("Upcoming event date filters", () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useAccountEventsPageViewModel(),
|
||||
() => useAccountEventsPageVm(),
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -361,7 +361,7 @@ it("retains the previous complete forecast when its source refresh fails", async
|
||||
);
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const { result } = renderHook(() => useAccountEventsPageViewModel(), {
|
||||
const { result } = renderHook(() => useAccountEventsPageVm(), {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={["/accounts/all/events"]}>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { useAccountEventsPageViewModel } from "./AccountEventsPage.vm";
|
||||
import { useAccountEventsPageVm } from "./AccountEventsPage.vm";
|
||||
import { AccountEventsPageError } from "./components/AccountEventsPageError.view";
|
||||
import { AccountEventsPageLoaded } from "./components/AccountEventsPageLoaded.view";
|
||||
import { AccountEventsPageLoading } from "./components/AccountEventsPageLoading.view";
|
||||
|
||||
const AccountEventsPage: FC = () => {
|
||||
const viewModel = useAccountEventsPageViewModel();
|
||||
if (viewModel.state === "loading") return <AccountEventsPageLoading />;
|
||||
const vm = useAccountEventsPageVm();
|
||||
if (vm.state === "loading") return <AccountEventsPageLoading />;
|
||||
|
||||
return viewModel.state === "error"
|
||||
? <AccountEventsPageError {...viewModel} />
|
||||
: <AccountEventsPageLoaded {...viewModel} />;
|
||||
return vm.state === "error"
|
||||
? <AccountEventsPageError {...vm} />
|
||||
: <AccountEventsPageLoaded {...vm} />;
|
||||
};
|
||||
|
||||
export { AccountEventsPage };
|
||||
|
||||
@@ -39,7 +39,7 @@ type AccountEventsPageViewModel
|
||||
}
|
||||
| { state: "loading" };
|
||||
|
||||
function useAccountEventsPageViewModel(): AccountEventsPageViewModel {
|
||||
function useAccountEventsPageVm(): AccountEventsPageViewModel {
|
||||
const { accountId = "" } = useParams();
|
||||
|
||||
const {
|
||||
@@ -136,4 +136,4 @@ function useAccountEventsPageViewModel(): AccountEventsPageViewModel {
|
||||
};
|
||||
}
|
||||
|
||||
export { type AccountEventsPageViewModel, useAccountEventsPageViewModel };
|
||||
export { type AccountEventsPageViewModel, useAccountEventsPageVm };
|
||||
|
||||
@@ -34,7 +34,7 @@ import { server } from "@/test/server";
|
||||
import { toIncomeDisplayModel } from "../../model/display";
|
||||
import { AccountIncomePage } from "./AccountIncomePage";
|
||||
import {
|
||||
useAccountIncomePageViewModel,
|
||||
useAccountIncomePageVm,
|
||||
} from "./AccountIncomePage.vm";
|
||||
import { IncomePayments } from "./components/IncomePayments.view";
|
||||
|
||||
@@ -171,7 +171,7 @@ describe("AccountIncomePage", () => {
|
||||
);
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const { result } = renderHook(() => useAccountIncomePageViewModel(), {
|
||||
const { result } = renderHook(() => useAccountIncomePageVm(), {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={["?from=2024-02-29&to=2024-03-01"]}>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { useAccountIncomePageViewModel } from "./AccountIncomePage.vm";
|
||||
import { useAccountIncomePageVm } from "./AccountIncomePage.vm";
|
||||
import { AccountIncomePageError } from "./components/AccountIncomePageError.view";
|
||||
import { AccountIncomePageLoaded } from "./components/AccountIncomePageLoaded.view";
|
||||
import { AccountIncomePageLoading } from "./components/AccountIncomePageLoading.view";
|
||||
|
||||
const AccountIncomePage: FC = () => {
|
||||
const viewModel = useAccountIncomePageViewModel();
|
||||
if (viewModel.state === "loading") return <AccountIncomePageLoading />;
|
||||
const vm = useAccountIncomePageVm();
|
||||
if (vm.state === "loading") return <AccountIncomePageLoading />;
|
||||
|
||||
return viewModel.state === "error"
|
||||
? <AccountIncomePageError {...viewModel} />
|
||||
: <AccountIncomePageLoaded {...viewModel} />;
|
||||
return vm.state === "error"
|
||||
? <AccountIncomePageError {...vm} />
|
||||
: <AccountIncomePageLoaded {...vm} />;
|
||||
};
|
||||
|
||||
export { AccountIncomePage };
|
||||
|
||||
@@ -43,7 +43,7 @@ type AccountIncomePageViewModel
|
||||
}
|
||||
| { state: "loading" };
|
||||
|
||||
const useAccountIncomePageViewModel = (): AccountIncomePageViewModel => {
|
||||
const useAccountIncomePageVm = (): AccountIncomePageViewModel => {
|
||||
const { accountId = "" } = useParams();
|
||||
const [searchParameters, setSearchParameters] = useSearchParams();
|
||||
|
||||
@@ -146,5 +146,5 @@ function isValidIncomePeriod(period: IncomePeriod): boolean {
|
||||
|
||||
export {
|
||||
type AccountIncomePageViewModel,
|
||||
useAccountIncomePageViewModel,
|
||||
useAccountIncomePageVm,
|
||||
};
|
||||
|
||||
@@ -6,15 +6,15 @@ import { AccountOperationsPageLoaded } from "./components/AccountOperationsPageL
|
||||
import { AccountOperationsPageLoading } from "./components/AccountOperationsPageLoading.view";
|
||||
|
||||
const AccountOperationsPage: FC = () => {
|
||||
const viewModel = useAccountOperationsPageVm();
|
||||
const vm = useAccountOperationsPageVm();
|
||||
|
||||
if (viewModel.state === "loading") {
|
||||
if (vm.state === "loading") {
|
||||
return <AccountOperationsPageLoading />;
|
||||
}
|
||||
|
||||
return viewModel.state === "error"
|
||||
? <AccountOperationsPageError {...viewModel} />
|
||||
: <AccountOperationsPageLoaded {...viewModel} />;
|
||||
return vm.state === "error"
|
||||
? <AccountOperationsPageError {...vm} />
|
||||
: <AccountOperationsPageLoaded {...vm} />;
|
||||
};
|
||||
|
||||
export { AccountOperationsPage };
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { useAccountPageViewModel } from "./AccountPage.vm";
|
||||
import { useAccountPageVm } from "./AccountPage.vm";
|
||||
import { AccountPageEmpty } from "./components/AccountPageEmpty.view";
|
||||
import { AccountPageError } from "./components/AccountPageError.view";
|
||||
import { AccountPageLoaded } from "./components/AccountPageLoaded.view";
|
||||
import { AccountPageLoading } from "./components/AccountPageLoading/AccountPageLoading.view";
|
||||
|
||||
const AccountPage: FC = () => {
|
||||
const viewModel = useAccountPageViewModel();
|
||||
if (viewModel.state === "loading") return <AccountPageLoading />;
|
||||
if (viewModel.state === "error") return <AccountPageError {...viewModel} />;
|
||||
if (viewModel.state === "empty")
|
||||
const vm = useAccountPageVm();
|
||||
if (vm.state === "loading") return <AccountPageLoading />;
|
||||
if (vm.state === "error") return <AccountPageError {...vm} />;
|
||||
if (vm.state === "empty")
|
||||
return (
|
||||
<AccountPageEmpty
|
||||
account={viewModel.account}
|
||||
accountId={viewModel.accountId}
|
||||
incomeSummary={viewModel.incomeSummary}
|
||||
readRetry={viewModel.readRetry}
|
||||
recentEvents={viewModel.recentEvents}
|
||||
recentOperations={viewModel.recentOperations}
|
||||
source={viewModel.source}
|
||||
account={vm.account}
|
||||
accountId={vm.accountId}
|
||||
incomeSummary={vm.incomeSummary}
|
||||
readRetry={vm.readRetry}
|
||||
recentEvents={vm.recentEvents}
|
||||
recentOperations={vm.recentOperations}
|
||||
source={vm.source}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<AccountPageLoaded
|
||||
account={viewModel.account}
|
||||
accountId={viewModel.accountId}
|
||||
concentration={viewModel.concentration}
|
||||
currency={viewModel.currency}
|
||||
incomeSummary={viewModel.incomeSummary}
|
||||
onCurrencyChange={viewModel.onCurrencyChange}
|
||||
onSliceChange={viewModel.onSliceChange}
|
||||
onTabChange={viewModel.onTabChange}
|
||||
partial={viewModel.partial}
|
||||
portfolio={viewModel.portfolio}
|
||||
positionSections={viewModel.positionSections}
|
||||
readRetry={viewModel.readRetry}
|
||||
recentEvents={viewModel.recentEvents}
|
||||
recentOperations={viewModel.recentOperations}
|
||||
slice={viewModel.slice}
|
||||
source={viewModel.source}
|
||||
tab={viewModel.tab}
|
||||
unrealizedReturns={viewModel.unrealizedReturns}
|
||||
account={vm.account}
|
||||
accountId={vm.accountId}
|
||||
concentration={vm.concentration}
|
||||
currency={vm.currency}
|
||||
incomeSummary={vm.incomeSummary}
|
||||
onCurrencyChange={vm.onCurrencyChange}
|
||||
onSliceChange={vm.onSliceChange}
|
||||
onTabChange={vm.onTabChange}
|
||||
partial={vm.partial}
|
||||
portfolio={vm.portfolio}
|
||||
positionSections={vm.positionSections}
|
||||
readRetry={vm.readRetry}
|
||||
recentEvents={vm.recentEvents}
|
||||
recentOperations={vm.recentOperations}
|
||||
slice={vm.slice}
|
||||
source={vm.source}
|
||||
tab={vm.tab}
|
||||
unrealizedReturns={vm.unrealizedReturns}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import { resource } from "@/test/resource";
|
||||
import { server } from "@/test/server";
|
||||
|
||||
import { portfolio } from "../../model/positions.fixtures";
|
||||
import { useAccountPageViewModel } from "./AccountPage.vm";
|
||||
import { useAccountPageVm } from "./AccountPage.vm";
|
||||
|
||||
function configureTestAuth() {
|
||||
configureAuthClient({
|
||||
@@ -102,12 +102,12 @@ function mockLoadedAccountPage() {
|
||||
);
|
||||
}
|
||||
|
||||
describe("useAccountPageViewModel", () => {
|
||||
describe("useAccountPageVm", () => {
|
||||
it("prepares recent operations and events state for presentation views", async () => {
|
||||
configureTestAuth();
|
||||
mockLoadedAccountPage();
|
||||
|
||||
const { result } = renderHook(() => useAccountPageViewModel(), {
|
||||
const { result } = renderHook(() => useAccountPageVm(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
@@ -154,7 +154,7 @@ describe("useAccountPageViewModel", () => {
|
||||
),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useAccountPageViewModel(), {
|
||||
const { result } = renderHook(() => useAccountPageVm(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
@@ -194,7 +194,7 @@ it("keeps overview sections independent when their source data is incomplete", a
|
||||
),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useAccountPageViewModel(), {
|
||||
const { result } = renderHook(() => useAccountPageVm(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
@@ -232,7 +232,7 @@ it("keeps overview sections loaded without exposing technical retry", async () =
|
||||
),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useAccountPageViewModel(), {
|
||||
const { result } = renderHook(() => useAccountPageVm(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ type AccountPageViewModel
|
||||
| { code?: string; readRetry: AccountPageReadRetry; state: "error" }
|
||||
| { state: "loading" };
|
||||
|
||||
function useAccountPageViewModel(): AccountPageViewModel {
|
||||
function useAccountPageVm(): AccountPageViewModel {
|
||||
const { accountId } = useParams();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -199,4 +199,4 @@ function useAccountPageViewModel(): AccountPageViewModel {
|
||||
};
|
||||
}
|
||||
|
||||
export { useAccountPageViewModel };
|
||||
export { useAccountPageVm };
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { useAccountsPageViewModel } from "./AccountsPage.vm";
|
||||
import { useAccountsPageVm } from "./AccountsPage.vm";
|
||||
import { AccountsPageEmpty } from "./components/AccountsPageEmpty.view";
|
||||
import { AccountsPageError } from "./components/AccountsPageError.view";
|
||||
import { AccountsPageLoaded } from "./components/AccountsPageLoaded.view";
|
||||
import { AccountsPageLoading } from "./components/AccountsPageLoading.view";
|
||||
|
||||
const AccountsPage: FC = () => {
|
||||
const viewModel = useAccountsPageViewModel();
|
||||
if (viewModel.state === "loading") return <AccountsPageLoading />;
|
||||
if (viewModel.state === "error") return <AccountsPageError {...viewModel} />;
|
||||
if (viewModel.state === "empty") return <AccountsPageEmpty />;
|
||||
const vm = useAccountsPageVm();
|
||||
if (vm.state === "loading") return <AccountsPageLoading />;
|
||||
if (vm.state === "error") return <AccountsPageError {...vm} />;
|
||||
if (vm.state === "empty") return <AccountsPageEmpty />;
|
||||
|
||||
return (
|
||||
<AccountsPageLoaded
|
||||
accounts={viewModel.accounts}
|
||||
meta={viewModel.meta}
|
||||
readRetry={viewModel.readRetry}
|
||||
accounts={vm.accounts}
|
||||
meta={vm.meta}
|
||||
readRetry={vm.readRetry}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ function isAccountsAccessFailure(code?: string) {
|
||||
|| isApiErrorCode(code, ApiErrorCode.TbankNotConnected);
|
||||
}
|
||||
|
||||
function useAccountsPageViewModel(): AccountsPageViewModel {
|
||||
function useAccountsPageVm(): AccountsPageViewModel {
|
||||
const accounts = useBrokerageAccounts();
|
||||
useScreenSync({ screen: "accounts" }, [accounts]);
|
||||
|
||||
@@ -73,4 +73,4 @@ function useAccountsPageViewModel(): AccountsPageViewModel {
|
||||
};
|
||||
}
|
||||
|
||||
export { useAccountsPageViewModel };
|
||||
export { useAccountsPageVm };
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { useDocumentTitle } from "@mantine/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { AuthPageLayout, LoginForm } from "@/features/auth";
|
||||
import { useAppDocumentTitle } from "@/shared/ui";
|
||||
|
||||
const LoginPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
useDocumentTitle(`${t("auth.login")} — ${t("appName")}`);
|
||||
useAppDocumentTitle(t("auth.login"));
|
||||
|
||||
return (
|
||||
<AuthPageLayout title={t("auth.login")}>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { Button, Stack, Title } from "@mantine/core";
|
||||
import { useDocumentTitle } from "@mantine/hooks";
|
||||
import { StateView } from "@wealthledger/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import { useAppDocumentTitle } from "@/shared/ui";
|
||||
|
||||
const NotFoundPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
useDocumentTitle(`${t("notFound.title")} — ${t("appName")}`);
|
||||
useAppDocumentTitle(t("notFound.title"));
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { useDocumentTitle } from "@mantine/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { AuthPageLayout, RegisterForm } from "@/features/auth";
|
||||
import { useAppDocumentTitle } from "@/shared/ui";
|
||||
|
||||
const RegisterPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
useDocumentTitle(`${t("auth.register")} — ${t("appName")}`);
|
||||
useAppDocumentTitle(t("auth.register"));
|
||||
|
||||
return (
|
||||
<AuthPageLayout title={t("auth.register")}>
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import type { FC } from "react";
|
||||
|
||||
import { Button, Stack, Title } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useAuth } from "@/features/auth";
|
||||
import { TbankConnectionSection } from "@/features/tbank-connection";
|
||||
|
||||
import { useSettingsPageVm } from "./SettingsPage.vm";
|
||||
|
||||
const SettingsPage: FC = () => {
|
||||
const { onLogout, t } = useSettingsPageVm();
|
||||
const { t } = useTranslation();
|
||||
const { logout } = useAuth();
|
||||
|
||||
// eslint-disable-next-line frontend/readability -- the page VM was removed as degenerate (frontend#143); a one-off error-swallowing logout is not a ViewModel.
|
||||
const onLogout = async () => {
|
||||
try {
|
||||
await logout();
|
||||
}
|
||||
catch {
|
||||
// The session is already cleared; the route guard owns navigation.
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useAuth } from "@/features/auth";
|
||||
|
||||
function useSettingsPageVm() {
|
||||
const { t } = useTranslation();
|
||||
const { logout } = useAuth();
|
||||
|
||||
const onLogout = async () => {
|
||||
try {
|
||||
await logout();
|
||||
}
|
||||
catch {
|
||||
// The session is already cleared; the route guard owns navigation.
|
||||
}
|
||||
};
|
||||
|
||||
return { onLogout, t };
|
||||
}
|
||||
|
||||
export { useSettingsPageVm };
|
||||
Reference in New Issue
Block a user