refactor: simplify frontend and clarify module ownership #45

Merged
ksv741 merged 16 commits from codex/frontend-calendar-form-cleanup into develop 2026-10-01 21:07:51 +03:00
4 changed files with 92 additions and 0 deletions
Showing only changes of commit 6a89b08ee5 - Show all commits
+8
View File
@@ -17,6 +17,10 @@
CSF сохраняет default meta и прямые story exports; configuration exceptions
заданы в [ESLint](../../eslint.config.mjs). Не распространяй их на application code.
Тип значений формы выводится из её schema, когда input/output совпадают.
Общие props определяются у одного owner; DTO и presentation model сохраняют
собственные типы при смысловом отличии. Type re-export сохраняет public imports.
## Читаемость: ESLint и review
ESLint проверяет ограничение 80 символов (tab = 2), пустые строки между
@@ -33,6 +37,10 @@ CSS и Markdown также держи в 80 колонках там, где эт
Review проверяет то, чего синтаксис не доказывает:
Обоснованное config exception сопровождается причиной и явным ожиданием в
existing lint tests, включая разрешённый и запрещённый примеры. Это позволяет
настраивать профиль без случайного ослабления остальных правил.
- Пустая строка отделяет смысловые шаги: inputs, вычисления, действие, результат.
- Имена derived values описывают значение, например `projectedAmount`.
- Тернарник остаётся коротким выбором двух значений. Mapping, несколько веток
+6
View File
@@ -63,6 +63,12 @@ Presentation VM для form-context/UI-only state может использов
в каталоге: её граф зависимостей не достигает application data/API.
`useTranslation` и декларативный вывод коллекции допустимы во view.
ESLint ограничивает прямые data/auth/router hook imports в `*.view.tsx`;
type imports, error helpers, router components и form context разрешены.
Транзитивный fetching через local presentation hook проверяется review;
Storybook дополнительно проверяет reachable graph. Новое исключение должно
сохранять presentation-only responsibility.
JSX props получают ссылки на функции, включая `onClick`, render props,
`dateParser` и callbacks внутри объектов props. Для React Hook Form предпочитай
`useController` в VM вместо inline `Controller.render`. Значение prop с условием
+38
View File
@@ -185,6 +185,44 @@ export default defineConfig(
files: ["src/**/*.tsx", "packages/*/src/**/*.tsx"],
ignores: ["**/*.test.tsx", "**/*.stories.tsx", "src/test/**"],
},
{
files: ["src/**/*.view.tsx"],
rules: {
"no-restricted-imports": ["error", {
paths: [
{
allowTypeImports: true,
message: "Query lifecycle belongs to the data VM.",
name: "@tanstack/react-query",
},
{
allowTypeImports: true,
message: "Transport belongs to shared/api and the data VM.",
name: "openapi-fetch",
},
{
allowTypeImports: true,
message: "Transport belongs to shared/api and the data VM.",
name: "openapi-react-query",
},
],
patterns: [
{
allowTypeImports: true,
group: ["react-router", "@/entities/*", "@/features/*"],
importNamePattern: "^use",
message: "Pass route, auth and entity data from the data VM.",
},
{
allowTypeImports: true,
group: ["@/shared/api", "@/shared/api/*"],
importNamePattern: "^use|^api(?:Query)?Client$",
message: "Fetching belongs to the data VM; error helpers are allowed.",
},
],
}],
},
},
{
files: ["src/**/*.{ts,tsx}", "packages/*/src/**/*.{ts,tsx}"],
ignores: ["**/*.test.{ts,tsx}", "**/*.stories.tsx", "src/test/**"],
+40
View File
@@ -338,6 +338,46 @@ test("JSX spacing fixes preserve text interpolation and are stable", async () =>
assert.equal(again.errorCount, 0);
});
test("views accept presentation inputs but reject direct data hooks", async () => {
const eslint = new ESLint();
const filePath = "src/features/instrument-details/ui/InstrumentEvents.view.tsx";
for (const code of [
"import { useStock } from \"@/entities/stock\";",
"import { useAuth } from \"@/features/auth\";",
"import { useNavigate } from \"react-router\";",
"import { apiClient } from \"@/shared/api\";",
"import { useQuery } from \"@tanstack/react-query\";",
]) {
const [result] = await eslint.lintText(code, { filePath });
assert.equal(result.fatalErrorCount, 0);
assert.ok(result.messages.some(message =>
message.ruleId === "no-restricted-imports"));
}
for (const code of [
"import { Link } from \"react-router\";",
"import { apiErrorCode } from \"@/shared/api\";",
"import type { StockPosition } from \"@/entities/stock\";",
"import type { QueryKey } from \"@tanstack/react-query\";",
"import { useTranslation } from \"react-i18next\";",
"import { useFormContext } from \"react-hook-form\";",
]) {
const [result] = await eslint.lintText(code, { filePath });
assert.equal(result.fatalErrorCount, 0);
assert.ok(result.messages.every(message =>
message.ruleId !== "no-restricted-imports"));
}
const [vm] = await eslint.lintText(
"import { useStock } from \"@/entities/stock\";",
{ filePath: "src/pages/stock/ui/StockPage/StockPage.vm.ts" },
);
assert.ok(vm.messages.every(message =>
message.ruleId !== "no-restricted-imports"));
});
test("compact JSX and contract nulls need no local suppressions", async () => {
const eslint = new ESLint({ fix: true });
const filePath = "packages/ui/src/EntryCard/EntryCard.tsx";