From 695cda0456970fe3a46d09dcec718c741ee6b18d Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Thu, 27 Aug 2026 10:53:23 +0300 Subject: [PATCH] refactor(account-operations): split page state views into components Extract loading/error/loaded state views from the single AccountOperationsPage.view.tsx into dedicated files under components/. Document the "one state per view file" rule as a mandatory review convention in CONVENTIONS.md and UI-PATTERN.md. Co-Authored-By: opencode --- docs/CONVENTIONS.md | 13 ++++-- docs/UI-PATTERN.md | 10 +++-- .../ui/AccountOperationsPage.tsx | 17 ++++---- .../AccountOperationsPageError.view.tsx | 28 +++++++++++++ .../AccountOperationsPageLoaded.view.tsx} | 41 +------------------ .../AccountOperationsPageLoading.view.tsx | 17 ++++++++ 6 files changed, 73 insertions(+), 53 deletions(-) create mode 100644 src/pages/account-operations/ui/components/AccountOperationsPageError.view.tsx rename src/pages/account-operations/ui/{AccountOperationsPage.view.tsx => components/AccountOperationsPageLoaded.view.tsx} (71%) create mode 100644 src/pages/account-operations/ui/components/AccountOperationsPageLoading.view.tsx diff --git a/docs/CONVENTIONS.md b/docs/CONVENTIONS.md index 1cd06b3..e60d973 100644 --- a/docs/CONVENTIONS.md +++ b/docs/CONVENTIONS.md @@ -9,6 +9,8 @@ - Публичный импорт слайса идёт через его `index.ts`. - Тесты лежат рядом с исходником как `*.test.tsx`. - Зависимости используют exact versions; не добавлять semver ranges. +- Главный `*.view.tsx` страницы не содержит несколько state-компонентов: + каждый state/section — отдельный `*.view.tsx` в `components/` (см. File roles). - Полный локальный gate: `npm run verify`. Pre-commit запускает staged formatting, OpenAPI check и FSD dependency checks; commit message должен следовать Conventional Commits. ## React component style @@ -37,6 +39,11 @@ Mandatory review conventions: - `Component.types.ts` - props и локальные типы. - `Component.module.css` - локальные стили. -Большие view files дробить по ответственности. Если section/state можно -самостоятельно назвать, вынести его в соседний presentation component вместо -накопления смысловой нагрузки в одном файле. +State-компоненты страницы не колоцируются в одном `*.view.tsx`. Каждый +top-level state (loading, error, empty, loaded) и каждый именуемый section +выносится в отдельный `*.view.tsx` в подпапку `components/`. Имя файла +отражает state/section: `AccountOperationsPageLoaded.view.tsx`, +`AccountOperationsPageLoading.view.tsx`, `AccountOperationsPageError.view.tsx`. +Контейнер (`AccountOperationsPage.tsx`) импортирует их из `./components` и +выбирает нужный по `viewModel.state`. Нельзя держать несколько state components +внутри одного файла view — это mandatory review convention. diff --git a/docs/UI-PATTERN.md b/docs/UI-PATTERN.md index af140f6..43634c8 100644 --- a/docs/UI-PATTERN.md +++ b/docs/UI-PATTERN.md @@ -25,10 +25,12 @@ image state. Простое форматирование одного перед существующий formatter остаётся presentation concern; вычисления из всей коллекции остаются во VM. -View files дробятся по смысловой нагрузке, а не только по количеству строк. Если -section или state имеет самостоятельный смысл и может быть назван, его нужно -выносить в nearby presentation component вместо накопления внутри одного -большого `*.view.tsx`. +View files дробятся по смысловой нагрузке, а не только по количеству строк. +Каждый top-level state и каждый именуемый section страницы — отдельный +`*.view.tsx` в `components/`, а не накопление внутри одного большого +`*.view.tsx`. Контейнер импортирует state views из `./components` и выбирает +нужный по `viewModel.state`. Держать loading/error/empty/loaded components в +одном файле view нельзя. Known API errors, retry и stale snapshot остаются local views, а непойманные route errors обрабатывает общий route-keyed ErrorBoundary. diff --git a/src/pages/account-operations/ui/AccountOperationsPage.tsx b/src/pages/account-operations/ui/AccountOperationsPage.tsx index 6e1cc29..4df8ed0 100644 --- a/src/pages/account-operations/ui/AccountOperationsPage.tsx +++ b/src/pages/account-operations/ui/AccountOperationsPage.tsx @@ -1,18 +1,21 @@ import type { FC } from "react"; import { LedgerEntryRow } from "@/entities/broker-operation"; -import { - AccountOperationsPageError, - AccountOperationsPageLoaded, - AccountOperationsPageLoading, -} from "./AccountOperationsPage.view"; import { useAccountOperationsPageVm } from "./AccountOperationsPage.vm"; +import { AccountOperationsPageError } from "./components/AccountOperationsPageError.view"; +import { AccountOperationsPageLoaded } from "./components/AccountOperationsPageLoaded.view"; +import { AccountOperationsPageLoading } from "./components/AccountOperationsPageLoading.view"; const AccountOperationsPage: FC = () => { const viewModel = useAccountOperationsPageVm(); - if (viewModel.state === "loading") return ; - if (viewModel.state === "error") + if (viewModel.state === "loading") { + return ; + } + + if (viewModel.state === "error") { return ; + } + return ; }; diff --git a/src/pages/account-operations/ui/components/AccountOperationsPageError.view.tsx b/src/pages/account-operations/ui/components/AccountOperationsPageError.view.tsx new file mode 100644 index 0000000..c5987d6 --- /dev/null +++ b/src/pages/account-operations/ui/components/AccountOperationsPageError.view.tsx @@ -0,0 +1,28 @@ +import { Button } from "@mantine/core"; +import type { FC } from "react"; +import { useTranslation } from "react-i18next"; +import { StateView } from "@/shared/ui"; +import type { AccountOperationsPageViewModel } from "../AccountOperationsPage.vm"; + +type AccountOperationsPageErrorProps = Extract< + AccountOperationsPageViewModel, + { state: "error" } +>; + +const AccountOperationsPageError: FC = ( + props, +) => { + const { retry } = props; + const { t } = useTranslation(); + + return ( + {t("ledger.retry")}} + /> + ); +}; + +export { AccountOperationsPageError }; diff --git a/src/pages/account-operations/ui/AccountOperationsPage.view.tsx b/src/pages/account-operations/ui/components/AccountOperationsPageLoaded.view.tsx similarity index 71% rename from src/pages/account-operations/ui/AccountOperationsPage.view.tsx rename to src/pages/account-operations/ui/components/AccountOperationsPageLoaded.view.tsx index 3800cc0..f22ef1c 100644 --- a/src/pages/account-operations/ui/AccountOperationsPage.view.tsx +++ b/src/pages/account-operations/ui/components/AccountOperationsPageLoaded.view.tsx @@ -14,46 +14,13 @@ import { operationCategories, } from "@/entities/broker-operation"; import { StateView } from "@/shared/ui"; -import type { AccountOperationsPageViewModel } from "./AccountOperationsPage.vm"; +import type { AccountOperationsPageViewModel } from "../AccountOperationsPage.vm"; type AccountOperationsPageLoadedProps = Extract< AccountOperationsPageViewModel, { state: "loaded" } >; -type AccountOperationsPageErrorProps = Extract< - AccountOperationsPageViewModel, - { state: "error" } ->; - -const AccountOperationsPageLoading: FC = () => { - const { t } = useTranslation(); - - return ( - - ); -}; - -const AccountOperationsPageError: FC = ( - props, -) => { - const { retry } = props; - const { t } = useTranslation(); - - return ( - {t("ledger.retry")}} - /> - ); -}; - const AccountOperationsPageLoaded: FC = ( props, ) => { @@ -128,8 +95,4 @@ const AccountOperationsPageLoaded: FC = ( ); }; -export { - AccountOperationsPageError, - AccountOperationsPageLoaded, - AccountOperationsPageLoading, -}; +export { AccountOperationsPageLoaded }; diff --git a/src/pages/account-operations/ui/components/AccountOperationsPageLoading.view.tsx b/src/pages/account-operations/ui/components/AccountOperationsPageLoading.view.tsx new file mode 100644 index 0000000..7fb6c1c --- /dev/null +++ b/src/pages/account-operations/ui/components/AccountOperationsPageLoading.view.tsx @@ -0,0 +1,17 @@ +import type { FC } from "react"; +import { useTranslation } from "react-i18next"; +import { StateView } from "@/shared/ui"; + +const AccountOperationsPageLoading: FC = () => { + const { t } = useTranslation(); + + return ( + + ); +}; + +export { AccountOperationsPageLoading };