refactor: address frontend review feedback
Frontend verify / commits (pull_request) Successful in 29s
Frontend verify / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 8m54s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 12s
Frontend verify / verify (push) Successful in 9m59s

This commit was merged in pull request #43.
This commit is contained in:
2026-09-28 19:20:32 +03:00
parent ca0ad4f824
commit a4046dbe87
139 changed files with 1549 additions and 941 deletions
+3
View File
@@ -26,6 +26,9 @@ JSX siblings и вокруг многострочных declarations/control-flo
сложные union types. Generated artifacts не форматируй вручную. сложные union types. Generated artifacts не форматируй вручную.
URL, строковые/template/regex literals и комментарии исключены из `max-len`; URL, строковые/template/regex literals и комментарии исключены из `max-len`;
это исключение для содержимого, а не повод собирать длинные expressions. это исключение для содержимого, а не повод собирать длинные expressions.
Однострочный JSX не требует обрамляющих скобок; многострочный — требует.
`unicorn/no-null` отключён: API DTO, React refs и Mantine используют `null`.
Сохраняй различие `null`/`undefined` из контракта вместо массовых suppressions.
CSS и Markdown также держи в 80 колонках там, где это не ломает данные/ссылки. CSS и Markdown также держи в 80 колонках там, где это не ломает данные/ссылки.
Review проверяет то, чего синтаксис не доказывает: Review проверяет то, чего синтаксис не доказывает:
+15 -1
View File
@@ -13,7 +13,13 @@ Mantine component и общую тему. Для выпадающего выбо
Icons импортируй из `@wealthledger/icons`; геометрия, SVG assets и animation Icons импортируй из `@wealthledger/icons`; геометрия, SVG assets и animation
lifecycle принадлежат этому пакету. Domain/category lookup возвращает параметры lifecycle принадлежат этому пакету. Domain/category lookup возвращает параметры
иконки из model сущности. SVG декоративны по умолчанию; доступное имя action иконки из model сущности. Каждый SVG хранится цельным декларативным
компонентом: не разбивай path-строки и не собирай геометрию в runtime.
Русские category names из API — данные для нормализованного сопоставления,
не ключи реестра иконок; неизвестные/переименованные категории имеют fallback.
Известные названия банков принадлежат enum `BankName` в bank-card model;
свободный ввод пользовательского названия остаётся строкой.
SVG декоративны по умолчанию; доступное имя action
принадлежит кнопке. Meaningful icon получает `aria-hidden={false}`, `role="img"` принадлежит кнопке. Meaningful icon получает `aria-hidden={false}`, `role="img"`
и доступное имя. Приложение и Storybook подключают CSS пакета. и доступное имя. Приложение и Storybook подключают CSS пакета.
@@ -68,6 +74,14 @@ JSX props получают ссылки на функции, включая `onC
адаптации UI inputs выноси в `shared/lib`, передавая явные аргументы. Domain адаптации UI inputs выноси в `shared/lib`, передавая явные аргументы. Domain
преобразование остаётся в model своего owner; shared не импортирует entities/pages. преобразование остаётся в model своего owner; shared не импортирует entities/pages.
Closure над состоянием, mutations и refs является действием VM, а не shared helper. Closure над состоянием, mutations и refs является действием VM, а не shared helper.
VM собирает связные предметные hooks, когда внутри смешиваются независимые
сценарии. Например, `useOpeningDateField` владеет draft/date parsing и событиями
даты, `usePaymentSystemField` — выбором платёжной системы. Каждый такой hook
возвращает данные и действия своей ответственности, VM объединяет их для view.
Не группируй несвязанные функции только по префиксу `on*` и не создавай
passthrough hook над одним вызовом ради формального дробления.
Render-helper замени самостоятельным presentation component у того же owner. Render-helper замени самостоятельным presentation component у того же owner.
Отдельный state/section с самостоятельной responsibility выноси в Отдельный state/section с самостоятельной responsibility выноси в
+7 -2
View File
@@ -5,11 +5,16 @@ module.exports = {
forbidden: [ forbidden: [
...base.forbidden, ...base.forbidden,
{ {
from: { path: String.raw`\.stories\.tsx$|^\.storybook/preview\.tsx$` }, from: { path: "[.]stories[.]tsx$|^[.]storybook/preview[.]tsx$" },
name: "stories-must-not-reach-application-data", name: "stories-must-not-reach-application-data",
severity: "error", severity: "error",
to: { to: {
path: String.raw`^src/(shared/api|features/auth|app/routes|app/ui)/|^src/(entities|features)/[^/]+/api/|^src/pages/[^/]+/ui/.*Page\.tsx$|node_modules/(@tanstack/react-query|openapi-fetch|openapi-react-query)/`, path: [
"^src/(shared/api|features/auth|app/routes|app/ui)/",
"^src/(entities|features)/[^/]+/api/",
"^src/pages/[^/]+/ui/.*Page[.]tsx$",
"node_modules/(@tanstack/react-query|openapi-fetch|openapi-react-query)/",
],
reachable: true, reachable: true,
}, },
}, },
+2 -2
View File
@@ -126,8 +126,8 @@ export default defineConfig(
"error", "error",
{ replacements: { props: false, ref: false } }, { replacements: { props: false, ref: false } },
], ],
// React, Mantine and Fetch accept null at their documented call boundaries. // API DTOs, React refs and Mantine use null as part of their contracts.
"unicorn/no-null": ["error", { checkArguments: false }], "unicorn/no-null": "off",
"unicorn/prefer-ternary": ["error", "only-single-line"], "unicorn/prefer-ternary": ["error", "only-single-line"],
}, },
settings: { settings: {
@@ -1,4 +1,5 @@
.icon { .icon {
flex-shrink: 0;
overflow: visible; overflow: visible;
& path { & path {
@@ -1,86 +1,72 @@
import type { FC } from "react"; import type { FC } from "react";
import { bindIconAnimation } from "../bind-icon-animation"; import { BasketIcon } from "./svg/BasketIcon";
import classes from "./OperationIcon.module.css"; import { CarIcon } from "./svg/CarIcon";
import { CashWithdrawalIcon } from "./svg/CashWithdrawalIcon";
import { CoffeeIcon } from "./svg/CoffeeIcon";
import { DepositIcon } from "./svg/DepositIcon";
import { DiceIcon } from "./svg/DiceIcon";
import { DownloadIcon } from "./svg/DownloadIcon";
import { EducationIcon } from "./svg/EducationIcon";
import { GridIcon } from "./svg/GridIcon";
import { GrowthIcon } from "./svg/GrowthIcon";
import { HomeIcon } from "./svg/HomeIcon";
import { MedicalIcon } from "./svg/MedicalIcon";
import { MusicIcon } from "./svg/MusicIcon";
import { RefundIcon } from "./svg/RefundIcon";
import { RepaymentIcon } from "./svg/RepaymentIcon";
import { ServerIcon } from "./svg/ServerIcon";
import { ShieldIcon } from "./svg/ShieldIcon";
import { ShoppingIcon } from "./svg/ShoppingIcon";
import { SmokingIcon } from "./svg/SmokingIcon";
import { SubscriptionsIcon } from "./svg/SubscriptionsIcon";
import { TaxiIcon } from "./svg/TaxiIcon";
import { TopupIcon } from "./svg/TopupIcon";
import { TransferIcon } from "./svg/TransferIcon";
import { TravelIcon } from "./svg/TravelIcon";
import { WifiIcon } from "./svg/WifiIcon";
import { WineIcon } from "./svg/WineIcon";
const paths = { const icons = {
basket: basket: BasketIcon,
"M3 9h18l-2 11H5L3 9Z M8 9l4-6 4 6 M9 13v3 M15 13v3 M10 6a2 2 0 1 0 4 0a2 2 0 1 0-4 0", car: CarIcon,
car: cashWithdrawal: CashWithdrawalIcon,
"M4 16H2v-4l3-1 3-4h6l4 4 4 1v4h-2 M8 16h8 M6 11h12m-6-4v4 M4 16a2 2 0 1 0 4 0a2 2 0 1 0-4 0m2-2v4m-2-2h4 M16 16a2 2 0 1 0 4 0a2 2 0 1 0-4 0m2-2v4m-2-2h4", coffee: CoffeeIcon,
cashWithdrawal: "M4 10V3h16v7 M7 7h10v14H7V7Z M7 11h10 M10 16h4", deposit: DepositIcon,
coffee: dice: DiceIcon,
"M4 8h12v7a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V8Z M16 9h2a3 3 0 0 1 0 6h-2 M8 6c-2-1 2-2 0-4 M12 6c-2-1 2-2 0-4 M3 22h15", download: DownloadIcon,
deposit: education: EducationIcon,
"M6 9a6 6 0 1 0 12 0a6 6 0 1 0-12 0 M10.5 12V6h2a1.5 1.5 0 0 1 0 3h-3m0 1.5h3 M4 16c0 4 16 4 16 0 M4 19c0 4 16 4 16 0", grid: GridIcon,
dice: growth: GrowthIcon,
"M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z M7 7h.01 M17 7h.01 M12 12h.01 M7 17h.01 M17 17h.01", home: HomeIcon,
download: "M3 3h18v13H3V3Z M8 21h8 M12 16v5 M12 6v7 M9 10l3 3 3-3", medical: MedicalIcon,
education: "M2 8l10-5 10 5-10 5L2 8Z M6 10v7c4 3 8 3 12 0v-7 M22 8v9", music: MusicIcon,
grid: "M4 4h6v6H4V4Z M14 4h6v6h-6V4Z M4 14h6v6H4v-6Z M14 14h6v6h-6v-6Z", refund: RefundIcon,
growth: "M3 3v18h18 M6 15l5-5 4 3 6-8 M16 5h5v5", repayment: RepaymentIcon,
home: "M3 11l9-8 9 8 M5 10v11h14V10 M9 21v-8h6v8", server: ServerIcon,
medical: "M9 3h6v6h6v6h-6v6H9v-6H3V9h6V3Z", shield: ShieldIcon,
music: "M9 5v14a3 3 0 1 1-3-3h3 M9 5l11-2v14a3 3 0 1 1-3-3h3 M9 9l11-2", shopping: ShoppingIcon,
refund: "M8 4L3 9l5 5 M3 9h11a7 7 0 0 1 0 14", smoking: SmokingIcon,
repayment: "M3 5h18v14H3V5Z M3 9h18 M10 18v-7h3a2 2 0 0 1 0 4H9m0 1.5h4", subscriptions: SubscriptionsIcon,
server: taxi: TaxiIcon,
"M3 3h18v7H3V3Z M3 14h18v7H3v-7Z M7 6.5h.01 M7 17.5h.01 M15 6.5h3 M15 17.5h3", topup: TopupIcon,
shield: "M12 2l8 4v6c0 5-8 10-8 10S4 17 4 12V6l8-4Z M8 12l3 3 5-6", transfer: TransferIcon,
shopping: "M5 7h14l2 14H3L5 7Z M8 8V6a4 4 0 0 1 8 0v2", travel: TravelIcon,
smoking: wifi: WifiIcon,
"M3 14h18v5H3v-5Z M16 14v5 M8 10V8c0-2 3-2 3-4V2 M15 10V8c0-2 3-2 3-4", wine: WineIcon,
subscriptions: };
"M4 10a8 8 0 0 1 13-6l3 3 M20 3v4h-4 M20 14a8 8 0 0 1-13 6l-3-3 M4 21v-4h4",
taxi:
"M4 10l2-6h12l2 6 M3 10h18v8H3v-8Z M6 18v3 M18 18v3 M6 14h2 M16 14h2 M10 4V2h4v2",
topup:
"M20 8V5H5a2 2 0 0 1 0-4h13v4 M3 3v16a2 2 0 0 0 2 2h15V8H5 M16 12v5m-2.5-2.5h5",
transfer: "M3 7h18l-4-4 M21 17H3l4 4",
travel: "M10 3h4v7l7 5v3l-7-3v5l2 1v1l-4-1-4 1v-1l2-1v-5l-7 3v-3l7-5V3Z",
wifi:
"M3 7a14 14 0 0 1 18 0 M6 11a9 9 0 0 1 12 0 M9 15a4 4 0 0 1 6 0 M12 19h.01",
wine:
"M7 3h10v5a5 5 0 0 1-10 0V3Z M7 7h10v1a5 5 0 0 1-10 0V7Z M12 13v8 M8 21h8",
} as const;
type OperationIconProps = { type OperationIconProps = {
animation?: string;
color: string; color: string;
variant: keyof typeof paths; variant: keyof typeof icons;
}; };
const OperationIcon: FC<OperationIconProps> = (props) => { const OperationIcon: FC<OperationIconProps> = (props) => {
const { animation, color, variant } = props; const { color, variant } = props;
// eslint-disable-next-line security/detect-object-injection -- Animation keys select CSS module classes only. // eslint-disable-next-line security/detect-object-injection -- Variant is a closed key of the static icon registry.
const animationClass = animation ? classes[animation] : ""; const Icon = icons[variant];
// eslint-disable-next-line security/detect-object-injection -- Variant is a closed keyof the package geometry map. return <Icon style={{ color: `var(--mantine-color-${color}-7)` }} />;
const parts = paths[variant]
.split(" M")
.map((part, index) => ({ d: index ? `M${part}` : part, key: part }));
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${animationClass}`}
fill="none"
focusable="false"
height="24"
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
style={{ color: `var(--mantine-color-${color}-7)`, flexShrink: 0 }}
viewBox="0 0 24 24"
width="24"
>
{parts.map(part => (
<path d={part.d} key={part.key} pathLength={1} />
))}
</svg>
);
}; };
export { OperationIcon, type OperationIconProps }; export { OperationIcon, type OperationIconProps };
@@ -0,0 +1,40 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const BasketIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.basket}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 9h18l-2 11H5L3 9Z" pathLength={1} />
<path d="M8 9l4-6 4 6" pathLength={1} />
<path d="M9 13v3" pathLength={1} />
<path d="M15 13v3" pathLength={1} />
<path d="M10 6a2 2 0 1 0 4 0a2 2 0 1 0-4 0" pathLength={1} />
</svg>
);
};
export { BasketIcon };
@@ -0,0 +1,40 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const CarIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.car}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M4 16H2v-4l3-1 3-4h6l4 4 4 1v4h-2" pathLength={1} />
<path d="M8 16h8" pathLength={1} />
<path d="M6 11h12m-6-4v4" pathLength={1} />
<path d="M4 16a2 2 0 1 0 4 0a2 2 0 1 0-4 0m2-2v4m-2-2h4" pathLength={1} />
<path d="M16 16a2 2 0 1 0 4 0a2 2 0 1 0-4 0m2-2v4m-2-2h4" pathLength={1} />
</svg>
);
};
export { CarIcon };
@@ -0,0 +1,38 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const CashWithdrawalIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M4 10V3h16v7" pathLength={1} />
<path d="M7 7h10v14H7V7Z" pathLength={1} />
<path d="M7 11h10" pathLength={1} />
<path d="M10 16h4" pathLength={1} />
</svg>
);
};
export { CashWithdrawalIcon };
@@ -0,0 +1,40 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const CoffeeIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.coffee}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M4 8h12v7a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V8Z" pathLength={1} />
<path d="M16 9h2a3 3 0 0 1 0 6h-2" pathLength={1} />
<path d="M8 6c-2-1 2-2 0-4" pathLength={1} />
<path d="M12 6c-2-1 2-2 0-4" pathLength={1} />
<path d="M3 22h15" pathLength={1} />
</svg>
);
};
export { CoffeeIcon };
@@ -0,0 +1,38 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const DepositIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.deposit}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M6 9a6 6 0 1 0 12 0a6 6 0 1 0-12 0" pathLength={1} />
<path d="M10.5 12V6h2a1.5 1.5 0 0 1 0 3h-3m0 1.5h3" pathLength={1} />
<path d="M4 16c0 4 16 4 16 0" pathLength={1} />
<path d="M4 19c0 4 16 4 16 0" pathLength={1} />
</svg>
);
};
export { DepositIcon };
@@ -0,0 +1,42 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const DiceIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z" pathLength={1} />
<path d="M7 7h.01" pathLength={1} />
<path d="M17 7h.01" pathLength={1} />
<path d="M12 12h.01" pathLength={1} />
<path d="M7 17h.01" pathLength={1} />
<path d="M17 17h.01" pathLength={1} />
</svg>
);
};
export { DiceIcon };
@@ -0,0 +1,40 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const DownloadIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 3h18v13H3V3Z" pathLength={1} />
<path d="M8 21h8" pathLength={1} />
<path d="M12 16v5" pathLength={1} />
<path d="M12 6v7" pathLength={1} />
<path d="M9 10l3 3 3-3" pathLength={1} />
</svg>
);
};
export { DownloadIcon };
@@ -0,0 +1,36 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const EducationIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M2 8l10-5 10 5-10 5L2 8Z" pathLength={1} />
<path d="M6 10v7c4 3 8 3 12 0v-7" pathLength={1} />
<path d="M22 8v9" pathLength={1} />
</svg>
);
};
export { EducationIcon };
@@ -0,0 +1,38 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const GridIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.grid}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M4 4h6v6H4V4Z" pathLength={1} />
<path d="M14 4h6v6h-6V4Z" pathLength={1} />
<path d="M4 14h6v6H4v-6Z" pathLength={1} />
<path d="M14 14h6v6h-6v-6Z" pathLength={1} />
</svg>
);
};
export { GridIcon };
@@ -0,0 +1,36 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const GrowthIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.growth}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 3v18h18" pathLength={1} />
<path d="M6 15l5-5 4 3 6-8" pathLength={1} />
<path d="M16 5h5v5" pathLength={1} />
</svg>
);
};
export { GrowthIcon };
@@ -0,0 +1,36 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const HomeIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 11l9-8 9 8" pathLength={1} />
<path d="M5 10v11h14V10" pathLength={1} />
<path d="M9 21v-8h6v8" pathLength={1} />
</svg>
);
};
export { HomeIcon };
@@ -0,0 +1,32 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const MedicalIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M9 3h6v6h6v6h-6v6H9v-6H3V9h6V3Z" pathLength={1} />
</svg>
);
};
export { MedicalIcon };
@@ -0,0 +1,36 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const MusicIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M9 5v14a3 3 0 1 1-3-3h3" pathLength={1} />
<path d="M9 5l11-2v14a3 3 0 1 1-3-3h3" pathLength={1} />
<path d="M9 9l11-2" pathLength={1} />
</svg>
);
};
export { MusicIcon };
@@ -0,0 +1,34 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const RefundIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M8 4L3 9l5 5" pathLength={1} />
<path d="M3 9h11a7 7 0 0 1 0 14" pathLength={1} />
</svg>
);
};
export { RefundIcon };
@@ -0,0 +1,36 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const RepaymentIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.repayment}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 5h18v14H3V5Z" pathLength={1} />
<path d="M3 9h18" pathLength={1} />
<path d="M10 18v-7h3a2 2 0 0 1 0 4H9m0 1.5h4" pathLength={1} />
</svg>
);
};
export { RepaymentIcon };
@@ -0,0 +1,42 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const ServerIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.server}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 3h18v7H3V3Z" pathLength={1} />
<path d="M3 14h18v7H3v-7Z" pathLength={1} />
<path d="M7 6.5h.01" pathLength={1} />
<path d="M7 17.5h.01" pathLength={1} />
<path d="M15 6.5h3" pathLength={1} />
<path d="M15 17.5h3" pathLength={1} />
</svg>
);
};
export { ServerIcon };
@@ -0,0 +1,34 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const ShieldIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M12 2l8 4v6c0 5-8 10-8 10S4 17 4 12V6l8-4Z" pathLength={1} />
<path d="M8 12l3 3 5-6" pathLength={1} />
</svg>
);
};
export { ShieldIcon };
@@ -0,0 +1,34 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const ShoppingIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M5 7h14l2 14H3L5 7Z" pathLength={1} />
<path d="M8 8V6a4 4 0 0 1 8 0v2" pathLength={1} />
</svg>
);
};
export { ShoppingIcon };
@@ -0,0 +1,38 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const SmokingIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 14h18v5H3v-5Z" pathLength={1} />
<path d="M16 14v5" pathLength={1} />
<path d="M8 10V8c0-2 3-2 3-4V2" pathLength={1} />
<path d="M15 10V8c0-2 3-2 3-4" pathLength={1} />
</svg>
);
};
export { SmokingIcon };
@@ -0,0 +1,38 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const SubscriptionsIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M4 10a8 8 0 0 1 13-6l3 3" pathLength={1} />
<path d="M20 3v4h-4" pathLength={1} />
<path d="M20 14a8 8 0 0 1-13 6l-3-3" pathLength={1} />
<path d="M4 21v-4h4" pathLength={1} />
</svg>
);
};
export { SubscriptionsIcon };
@@ -0,0 +1,44 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const TaxiIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M4 10l2-6h12l2 6" pathLength={1} />
<path d="M3 10h18v8H3v-8Z" pathLength={1} />
<path d="M6 18v3" pathLength={1} />
<path d="M18 18v3" pathLength={1} />
<path d="M6 14h2" pathLength={1} />
<path d="M16 14h2" pathLength={1} />
<path d="M10 4V2h4v2" pathLength={1} />
</svg>
);
};
export { TaxiIcon };
@@ -0,0 +1,36 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const TopupIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.topup}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M20 8V5H5a2 2 0 0 1 0-4h13v4" pathLength={1} />
<path d="M3 3v16a2 2 0 0 0 2 2h15V8H5" pathLength={1} />
<path d="M16 12v5m-2.5-2.5h5" pathLength={1} />
</svg>
);
};
export { TopupIcon };
@@ -0,0 +1,34 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const TransferIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.transfer}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 7h18l-4-4" pathLength={1} />
<path d="M21 17H3l4 4" pathLength={1} />
</svg>
);
};
export { TransferIcon };
@@ -0,0 +1,32 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const TravelIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={classes.icon}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M10 3h4v7l7 5v3l-7-3v5l2 1v1l-4-1-4 1v-1l2-1v-5l-7 3v-3l7-5V3Z" pathLength={1} />
</svg>
);
};
export { TravelIcon };
@@ -0,0 +1,38 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const WifiIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.wifi}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M3 7a14 14 0 0 1 18 0" pathLength={1} />
<path d="M6 11a9 9 0 0 1 12 0" pathLength={1} />
<path d="M9 15a4 4 0 0 1 6 0" pathLength={1} />
<path d="M12 19h.01" pathLength={1} />
</svg>
);
};
export { WifiIcon };
@@ -0,0 +1,38 @@
import type { FC } from "react";
import type { IconProps } from "../../types";
import { bindIconAnimation } from "../../bind-icon-animation";
import classes from "../OperationIcon.module.css";
const WineIcon: FC<IconProps> = (props) => {
const { size = 24, ...svgProps } = props;
return (
<svg
aria-hidden="true"
className={`${classes.icon} ${classes.wine}`}
fill="none"
focusable="false"
height={size}
ref={bindIconAnimation}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.6"
viewBox="0 0 24 24"
width={size}
{...svgProps}
>
<path d="M7 3h10v5a5 5 0 0 1-10 0V3Z" pathLength={1} />
<path d="M7 7h10v1a5 5 0 0 1-10 0V7Z" pathLength={1} />
<path d="M12 13v8" pathLength={1} />
<path d="M8 21h8" pathLength={1} />
</svg>
);
};
export { WineIcon };
+23 -1
View File
@@ -130,7 +130,7 @@ test("ESLint enforces the frontend profile and excludes generated artifacts", as
assert.equal(story.rules["react-refresh/only-export-components"], undefined); assert.equal(story.rules["react-refresh/only-export-components"], undefined);
}); });
test("all recommended plugin rules remain enabled in their intended scopes", async () => { test("recommended plugin rules retain the explicit null-contract exception", async () => {
const eslint = new ESLint(); const eslint = new ESLint();
const source = "src/shared/api/problem-details.ts"; const source = "src/shared/api/problem-details.ts";
const component = "packages/ui/src/EntryCard/EntryCard.tsx"; const component = "packages/ui/src/EntryCard/EntryCard.tsx";
@@ -175,6 +175,12 @@ test("all recommended plugin rules remain enabled in their intended scopes", asy
for (const [rule, setting] of Object.entries(expected)) { for (const [rule, setting] of Object.entries(expected)) {
const severity = Array.isArray(setting) ? setting[0] : setting; const severity = Array.isArray(setting) ? setting[0] : setting;
if (severity === "off" || severity === 0 || !rule.includes("/")) continue; if (severity === "off" || severity === 0 || !rule.includes("/")) continue;
if (rule === "unicorn/no-null") {
assert.equal(actual.get(rule)?.[0], 0);
continue;
}
assert.ok(actual.get(rule)?.[0] > 0, `${filePath}: ${rule}`); assert.ok(actual.get(rule)?.[0] > 0, `${filePath}: ${rule}`);
} }
} }
@@ -316,3 +322,19 @@ test("JSX spacing fixes preserve text interpolation and are stable", async () =>
assert.equal(again.output, undefined); assert.equal(again.output, undefined);
assert.equal(again.errorCount, 0); assert.equal(again.errorCount, 0);
}); });
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";
const code = "import type { FC } from 'react'; type Props = { active: boolean }; const Example: FC<Props> = (props) => props.active ? <span /> : <div />;\nexport { Example };";
const [fixed] = await eslint.lintText(code, { filePath });
assert.equal(fixed.errorCount, 0);
assert.match(fixed.output ?? code, /\? <span \/> : <div \/>/);
const [nullable] = await eslint.lintText(
"export const value: string | null = null;",
{ filePath: "src/shared/api/problem-details.ts" },
);
assert.equal(nullable.errorCount, 0);
});
+4 -17
View File
@@ -29,8 +29,6 @@ const accounts = [
{ id: "b", name: "ИИС" }, { id: "b", name: "ИИС" },
]; ];
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const position: Portfolio["positions"][number] const position: Portfolio["positions"][number]
= { = {
absoluteReturn: null, absoluteReturn: null,
@@ -45,7 +43,6 @@ const position: Portfolio["positions"][number]
type: "STOCK", type: "STOCK",
}; };
/* eslint-enable unicorn/no-null */
const portfolio: Portfolio = { const portfolio: Portfolio = {
cashBalances: [], cashBalances: [],
isStale: false, isStale: false,
@@ -55,8 +52,6 @@ const portfolio: Portfolio = {
totalsByCurrency: [{ currency: "RUB", value: "1200" }], totalsByCurrency: [{ currency: "RUB", value: "1200" }],
}; };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const receipt: BrokerageOperation const receipt: BrokerageOperation
= { = {
accruedInterest: null, accruedInterest: null,
@@ -76,11 +71,8 @@ const receipt: BrokerageOperation
type: "OPERATION_TYPE_COUPON", type: "OPERATION_TYPE_COUPON",
}; };
/* eslint-enable unicorn/no-null */
const items = accounts.map(account => ({ ...receipt, account })); const items = accounts.map(account => ({ ...receipt, account }));
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const analytics: IncomeAnalytics const analytics: IncomeAnalytics
= { = {
accountId: "all", accountId: "all",
@@ -118,7 +110,6 @@ const analytics: IncomeAnalytics
yearComparison: null, yearComparison: null,
}; };
/* eslint-enable unicorn/no-null */
const CurrentLocation: FC = () => { const CurrentLocation: FC = () => {
const location = useLocation(); const location = useLocation();
@@ -162,7 +153,7 @@ beforeEach(() => {
HttpResponse.json( HttpResponse.json(
resource( resource(
accounts.map( accounts.map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
account => account =>
({ ({
...account, ...account,
@@ -170,7 +161,6 @@ beforeEach(() => {
portfolioSummary: null, portfolioSummary: null,
type: "BROKERAGE", type: "BROKERAGE",
}), }),
/* eslint-enable unicorn/no-null */
), ),
), ),
), ),
@@ -182,7 +172,7 @@ beforeEach(() => {
"*/api/v1/brokerage-accounts/:accountId/operations", "*/api/v1/brokerage-accounts/:accountId/operations",
({ params }) => ({ params }) =>
HttpResponse.json( HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: params.accountId, accountId: params.accountId,
@@ -191,7 +181,6 @@ beforeEach(() => {
nextCursor: null, nextCursor: null,
observedAt: receipt.occurredAt, observedAt: receipt.occurredAt,
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
), ),
@@ -204,7 +193,7 @@ beforeEach(() => {
accountId: "all", accountId: "all",
calculatedAt: receipt.occurredAt, calculatedAt: receipt.occurredAt,
events: accounts.map( events: accounts.map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
account => account =>
({ ({
account, account,
@@ -219,7 +208,6 @@ beforeEach(() => {
ticker: "BOND", ticker: "BOND",
type: "coupon", type: "coupon",
}), }),
/* eslint-enable unicorn/no-null */
), ),
incomeTotalsByCurrency: [], incomeTotalsByCurrency: [],
}), }),
@@ -278,7 +266,7 @@ describe("All accounts routes", () => {
requests.push(url); requests.push(url);
return HttpResponse.json( return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: params.accountId, accountId: params.accountId,
@@ -294,7 +282,6 @@ describe("All accounts routes", () => {
nextCursor: url.searchParams.has("cursor") ? null : "next", nextCursor: url.searchParams.has("cursor") ? null : "next",
observedAt: receipt.occurredAt, observedAt: receipt.occurredAt,
}, },
/* eslint-enable unicorn/no-null */
), ),
); );
}, },
+1 -2
View File
@@ -104,7 +104,7 @@ describe("AppRoutes", () => {
), ),
http.get("*/api/v1/bank-cards", () => HttpResponse.json([])), http.get("*/api/v1/bank-cards", () => HttpResponse.json([])),
http.get("*/api/v1/bank-cards/summary", () => http.get("*/api/v1/bank-cards/summary", () =>
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
HttpResponse.json( HttpResponse.json(
{ {
cardId: null, cardId: null,
@@ -114,7 +114,6 @@ describe("AppRoutes", () => {
net: { currency: "RUB", value: "0" }, net: { currency: "RUB", value: "0" },
refunds: { currency: "RUB", value: "0" }, refunds: { currency: "RUB", value: "0" },
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
); );
@@ -106,12 +106,8 @@ const ProtectedDocumentTitle: FC = () => {
if (stocks) return <DocumentTitle title={t("navigation.stocks")} />; if (stocks) return <DocumentTitle title={t("navigation.stocks")} />;
return settings return settings
? ( ? <DocumentTitle title={t("navigation.settings")} />
<DocumentTitle title={t("navigation.settings")} /> : <DocumentTitle title={t("navigation.accounts")} />;
)
: (
<DocumentTitle title={t("navigation.accounts")} />
);
}; };
export { ProtectedDocumentTitle }; export { ProtectedDocumentTitle };
@@ -8,10 +8,10 @@ import { RouteErrorBoundary } from "./RouteErrorBoundary";
const session = { isAuthenticated: false }; const session = { isAuthenticated: false };
vi.mock("@/features/auth", () => ({ vi.mock("@/features/auth", () => ({
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
useAuth: () => useAuth: () =>
({ user: session.isAuthenticated ? {} : null }), ({ user: session.isAuthenticated ? {} : null }),
/* eslint-enable unicorn/no-null */
})); }));
const ThrowingRoute: FC = () => { const ThrowingRoute: FC = () => {
@@ -25,9 +25,7 @@ const HealthyRoute: FC = () => {
? ( ? (
"healthy route" "healthy route"
) )
: ( : <ThrowingRoute />}
<ThrowingRoute />
)}
</div> </div>
); );
}; };
@@ -34,9 +34,7 @@ class Boundary extends Component<BoundaryProps, State> {
render() { render() {
return this.state.hasError return this.state.hasError
? ( ? <RouteErrorFallback authenticated={this.props.authenticated} />
<RouteErrorFallback authenticated={this.props.authenticated} />
)
: ( : (
this.props.children this.props.children
); );
+1 -3
View File
@@ -79,9 +79,7 @@ const LazyPage: FC<LazyPageProps> = (props) => {
? ( ? (
children children
) )
: ( : <RouteTransition>{children}</RouteTransition>}
<RouteTransition>{children}</RouteTransition>
)}
</Suspense> </Suspense>
); );
}; };
+1 -3
View File
@@ -13,9 +13,7 @@ const Protected: FC<PropsWithChildren> = (props) => {
if (!ready) return <RouteSkeleton variant="shell" />; if (!ready) return <RouteSkeleton variant="shell" />;
return user return user
? ( ? <>{children}</>
<>{children}</>
)
: ( : (
<Navigate <Navigate
replace replace
+1 -3
View File
@@ -88,9 +88,7 @@ const AppBreadcrumbs: FC = () => {
return <BreadcrumbTrail items={[{ label: t("breadcrumbs.accounts") }]} />; return <BreadcrumbTrail items={[{ label: t("breadcrumbs.accounts") }]} />;
return settings return settings
? ( ? <BreadcrumbTrail items={[{ label: t("breadcrumbs.settings") }]} />
<BreadcrumbTrail items={[{ label: t("breadcrumbs.settings") }]} />
)
: undefined; : undefined;
}; };
@@ -19,8 +19,6 @@ import {
useBankSummary, useBankSummary,
} from "./queries"; } from "./queries";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const card: BankCard const card: BankCard
= { = {
archivedAt: null, archivedAt: null,
@@ -35,7 +33,6 @@ const card: BankCard
version: 1, version: 1,
}; };
/* eslint-enable unicorn/no-null */
const transfer: CreateBankOperation = { const transfer: CreateBankOperation = {
amount: { currency: "RUB", value: "90071992547409.93" }, amount: { currency: "RUB", value: "90071992547409.93" },
cardId: "a", cardId: "a",
+8
View File
@@ -0,0 +1,8 @@
enum BankName {
AlfaBank = "Альфа-Банк",
Sberbank = "Сбербанк",
TBank = "Т-Банк",
Vtb = "ВТБ",
}
export { BankName };
@@ -32,7 +32,6 @@ function bankToday() {
function moneyCents(value: string): bigint | null { function moneyCents(value: string): bigint | null {
const normalized = value.trim().replace(",", "."); const normalized = value.trim().replace(",", ".");
if (!/^-?\d+$|^-?\d+\.\d{1,2}$/.test(normalized)) if (!/^-?\d+$|^-?\d+\.\d{1,2}$/.test(normalized))
// eslint-disable-next-line unicorn/no-null -- The money parser’s callers distinguish invalid input with null.
return null; return null;
const [whole, fraction = ""] = normalized const [whole, fraction = ""] = normalized
@@ -2,129 +2,68 @@ import type { OperationIconProps } from "@wealthledger/icons";
import type { BankOperation } from "../api/queries"; import type { BankOperation } from "../api/queries";
type OperationIcon = { const categoryIcons: {
animation?: string; appearance: OperationIconProps;
color: string; name: string;
variant: OperationIconProps["variant"]; }[] = [
}; { appearance: { color: "grape", variant: "wine" }, name: "Алкоголь" },
{ appearance: { color: "green", variant: "deposit" }, name: "Вклады" },
{ appearance: { color: "orange", variant: "home" }, name: "Жильё" },
{ appearance: { color: "red", variant: "medical" }, name: "Здоровье" },
{
appearance: { color: "green", variant: "growth" },
name: "Инвестиции и накопления",
},
{ appearance: { color: "orange", variant: "coffee" }, name: "Кафе" },
{ appearance: { color: "blue", variant: "education" }, name: "Образование" },
{ appearance: { color: "blue", variant: "transfer" }, name: "Переводы" },
{
appearance: { color: "orange", variant: "repayment" },
name: "Погашение кредита",
},
{
appearance: { color: "grape", variant: "subscriptions" },
name: "Подписки",
},
{ appearance: { color: "grape", variant: "shopping" }, name: "Покупки" },
{ appearance: { color: "green", variant: "basket" }, name: "Продукты" },
{ appearance: { color: "blue", variant: "travel" }, name: "Путешествия" },
{ appearance: { color: "grape", variant: "music" }, name: "Развлечения" },
{ appearance: { color: "blue", variant: "wifi" }, name: "Связь и интернет" },
{
appearance: { color: "green", variant: "cashWithdrawal" },
name: "Снятие наличных",
},
{ appearance: { color: "grape", variant: "dice" }, name: "Ставки" },
{ appearance: { color: "green", variant: "shield" }, name: "Страхование" },
{ appearance: { color: "orange", variant: "smoking" }, name: "Табак" },
{ appearance: { color: "yellow", variant: "taxi" }, name: "Такси" },
{ appearance: { color: "blue", variant: "car" }, name: "Транспорт" },
{
appearance: { color: "grape", variant: "server" },
name: "Хостинг и ИИ-сервисы",
},
{
appearance: { color: "blue", variant: "download" },
name: "Цифровые покупки",
},
];
const categoryIcons: Record<string, OperationIcon> = { const otherIcon: OperationIconProps = {
"алкоголь": {
animation: "wine",
color: "grape",
variant: "wine",
},
"вклады": {
animation: "deposit",
color: "green",
variant: "deposit",
},
"жилье": { color: "orange", variant: "home" },
"здоровье": { color: "red", variant: "medical" },
"инвестиции и накопления": {
animation: "growth",
color: "green",
variant: "growth",
},
"кафе": {
animation: "coffee",
color: "orange",
variant: "coffee",
},
"образование": {
color: "blue",
variant: "education",
},
"переводы": {
animation: "transfer",
color: "blue",
variant: "transfer",
},
"погашение кредита": {
animation: "repayment",
color: "orange",
variant: "repayment",
},
"подписки": {
color: "grape",
variant: "subscriptions",
},
"покупки": {
color: "grape",
variant: "shopping",
},
"продукты": {
animation: "basket",
color: "green",
variant: "basket",
},
"путешествия": {
color: "blue",
variant: "travel",
},
"развлечения": {
color: "grape",
variant: "music",
},
"связь и интернет": {
animation: "wifi",
color: "blue",
variant: "wifi",
},
"снятие наличных": {
color: "green",
variant: "cashWithdrawal",
},
"ставки": {
color: "grape",
variant: "dice",
},
"страхование": {
color: "green",
variant: "shield",
},
"табак": {
color: "orange",
variant: "smoking",
},
"такси": {
color: "yellow",
variant: "taxi",
},
"транспорт": {
animation: "car",
color: "blue",
variant: "car",
},
"хостинг и ии-сервисы": {
animation: "server",
color: "grape",
variant: "server",
},
"цифровые покупки": {
color: "blue",
variant: "download",
},
};
const otherIcon: OperationIcon = {
animation: "grid",
color: "gray", color: "gray",
variant: "grid", variant: "grid",
}; };
const operationIcons: Record< const operationIcons: Record<
Exclude<BankOperation["kind"], "expense">, Exclude<BankOperation["kind"], "expense">,
OperationIcon OperationIconProps
> = { > = {
refund: { color: "teal", variant: "refund" }, refund: { color: "teal", variant: "refund" },
topup: { topup: {
animation: "topup",
color: "green", color: "green",
variant: "topup", variant: "topup",
}, },
transfer: { transfer: {
animation: "transfer",
color: "blue", color: "blue",
variant: "transfer", variant: "transfer",
}, },
@@ -135,17 +74,20 @@ function bankOperationAppearance(
categoryName?: null | string, categoryName?: null | string,
) { ) {
if (kind !== "expense") if (kind !== "expense")
// eslint-disable-next-line security/detect-object-injection -- Operation kinds are finite; category names are guarded by Object.hasOwn. // eslint-disable-next-line security/detect-object-injection -- Operation kinds are a finite API union.
return operationIcons[kind]; return operationIcons[kind];
const name = categoryName const name = normalizeCategoryName(categoryName ?? "");
?.trim()
.toLocaleLowerCase("ru-RU")
.replaceAll("ё", "е");
return name && Object.hasOwn(categoryIcons, name) const category = categoryIcons.find(
? categoryIcons[name] // eslint-disable-line security/detect-object-injection -- Operation kinds are finite; category names are guarded by Object.hasOwn. item => normalizeCategoryName(item.name) === name,
: otherIcon; );
return category?.appearance ?? otherIcon;
}
function normalizeCategoryName(name: string) {
return name.trim().toLocaleLowerCase("ru-RU").replaceAll("ё", "е");
} }
export { bankOperationAppearance }; export { bankOperationAppearance };
@@ -2,17 +2,16 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
import { SimpleGrid } from "@mantine/core"; import { SimpleGrid } from "@mantine/core";
import { BankName } from "../../model/bank";
import { BankCardFace } from "./BankCardFace"; import { BankCardFace } from "./BankCardFace";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const meta const meta
= { = {
args: { args: {
card: { card: {
archivedAt: null, archivedAt: null,
balance: { currency: "RUB", value: "128450.75" }, balance: { currency: "RUB", value: "128450.75" },
bankName: "Сбербанк", bankName: BankName.Sberbank,
id: "fictional", id: "fictional",
lastFour: "4829", lastFour: "4829",
name: "Для повседневных покупок", name: "Для повседневных покупок",
@@ -25,7 +24,7 @@ const meta
component: BankCardFace, component: BankCardFace,
tags: ["autodocs"], tags: ["autodocs"],
title: "Product/BankCards/CardAppearance", title: "Product/BankCards/CardAppearance",
} satisfies /* eslint-enable unicorn/no-null */ Meta<typeof BankCardFace>; } satisfies Meta<typeof BankCardFace>;
export default meta; export default meta;
@@ -35,33 +34,28 @@ export const Banks: Story = {
render: arguments_ => ( render: arguments_ => (
<SimpleGrid cols={{ base: 1, sm: 2 }} maw={920}> <SimpleGrid cols={{ base: 1, sm: 2 }} maw={920}>
{[ {[
"Сбербанк", BankName.Sberbank,
"Т-Банк", BankName.TBank,
"Альфа-Банк", BankName.AlfaBank,
"ВТБ", BankName.Vtb,
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
].map((bankName, index) => ( ].map((bankName, index) => (
<BankCardFace <BankCardFace
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */ card={{
card={ ...arguments_.card,
{ bankName,
...arguments_.card, paymentSystem:
bankName,
paymentSystem:
// eslint-disable-next-line security/detect-object-injection -- This is a positional index into the local array, not a user-supplied object key. // eslint-disable-next-line security/detect-object-injection -- This is a positional index into the local array, not a user-supplied object key.
(["mir", "mastercard", "visa", "mir", null] as const)[index] (["mir", "mastercard", "visa", "mir", null] as const)[index]
?? null, ?? null,
} }}
/* eslint-enable unicorn/no-null */
}
key={bankName ?? "unknown"} key={bankName ?? "unknown"}
/> />
))} ))}
</SimpleGrid> </SimpleGrid>
), ),
}; };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const MissingDetails: Story export const MissingDetails: Story
= { = {
args: { args: {
@@ -73,7 +67,7 @@ export const MissingDetails: Story
}, },
}, },
}; };
/* eslint-enable unicorn/no-null */
export const LongArchived: Story = { export const LongArchived: Story = {
args: { args: {
card: { card: {
@@ -22,9 +22,7 @@ const BankCardFace: FC<BankCardFaceProps> = (props) => {
<div className={classes.top}> <div className={classes.top}>
<span className={classes.bank}>{card.bankName || t("bank.card")}</span> <span className={classes.bank}>{card.bankName || t("bank.card")}</span>
{card.archivedAt && ( {card.archivedAt && <span className={classes.archive}>{t("bank.archived")}</span>}
<span className={classes.archive}>{t("bank.archived")}</span>
)}
</div> </div>
<div aria-hidden="true" className={classes.hardware}> <div aria-hidden="true" className={classes.hardware}>
@@ -46,9 +44,7 @@ const BankCardFace: FC<BankCardFaceProps> = (props) => {
{card.paymentSystem && ( {card.paymentSystem && (
<span className={classes.network} data-network={card.paymentSystem}> <span className={classes.network} data-network={card.paymentSystem}>
{card.paymentSystem === "mastercard" && ( {card.paymentSystem === "mastercard" && <span aria-hidden="true" className={classes.circles} />}
<span aria-hidden="true" className={classes.circles} />
)}
{t(`bank.paymentSystems.${card.paymentSystem}`)} {t(`bank.paymentSystems.${card.paymentSystem}`)}
</span> </span>
@@ -1,11 +1,10 @@
import type { ComponentProps } from "react"; import { useFormContext } from "react-hook-form";
import { DateInput } from "@mantine/dates";
import { useController, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { bankDateInputValue } from "../../model/form-validation"; import type { BankCardValues } from "../../model/form.schema";
import { type BankCardValues, bankDateSchema } from "../../model/form.schema";
import { useOpeningDateField } from "./useOpeningDateField";
import { usePaymentSystemField } from "./usePaymentSystemField";
function useBankCardFieldsVm() { function useBankCardFieldsVm() {
const { const {
@@ -15,62 +14,10 @@ function useBankCardFieldsVm() {
} = useFormContext<BankCardValues>(); } = useFormContext<BankCardValues>();
const { t } = useTranslation(); const { t } = useTranslation();
const openingDate = useOpeningDateField(control);
const paymentSystem = usePaymentSystemField(control);
const { field: paymentSystemField } = useController({ return { ...openingDate, ...paymentSystem, errors, register, t };
control,
name: "paymentSystem",
});
const { field: openingDateField } = useController({
control,
name: "openingDate",
});
const paymentSystems = ["mir", "visa", "mastercard"].map(value => ({
label: t(`bank.paymentSystems.${value}`),
value,
}));
const onPaymentSystemChange: (value: null | string) => void = value =>
paymentSystemField.onChange(value ?? "");
const dateParser: ComponentProps<typeof DateInput>["dateParser"] = value =>
bankDateSchema.safeParse(bankDateInputValue(value)).success
? bankDateInputValue(value)
: null; // eslint-disable-line unicorn/no-null -- Mantine represents an empty controlled openingDateField as null.
const onOpeningDateBlur: ComponentProps<typeof DateInput>["onBlur"] = (
event,
) => {
openingDateField.onChange(bankDateInputValue(event.currentTarget.value));
openingDateField.onBlur();
};
const onOpeningDateChange: (value: null | string) => void = value =>
openingDateField.onChange(value ?? "");
const onOpeningDateInput: ComponentProps<typeof DateInput>["onInput"] = event =>
openingDateField.onChange(bankDateInputValue(event.currentTarget.value));
const openingDateValue = bankDateSchema.safeParse(openingDateField.value)
.success
? openingDateField.value
: undefined;
return {
dateParser,
errors,
onOpeningDateBlur,
onOpeningDateChange,
onOpeningDateInput,
onPaymentSystemChange,
openingDateField,
openingDateValue,
paymentSystemField,
paymentSystems,
register,
t,
};
} }
export { useBankCardFieldsVm }; export { useBankCardFieldsVm };
@@ -12,8 +12,6 @@ import type { BankCard } from "../../api/queries";
import { BankCardForm } from "./BankCardForm"; import { BankCardForm } from "./BankCardForm";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const card: BankCard const card: BankCard
= { = {
archivedAt: null, archivedAt: null,
@@ -28,7 +26,6 @@ const card: BankCard
version: 1, version: 1,
}; };
/* eslint-enable unicorn/no-null */
function setup(value: BankCard) { function setup(value: BankCard) {
configureAuthClient({ configureAuthClient({
accessToken: () => "test", accessToken: () => "test",
@@ -75,7 +72,7 @@ describe("BankCardForm", () => {
); );
fireEvent.click(screen.getByRole("button", { name: "Сохранить" })); fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
await waitFor(() => expect(close).toHaveBeenCalled()); await waitFor(() => expect(close).toHaveBeenCalled());
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
expect(body).toEqual( expect(body).toEqual(
{ {
bankName: null, bankName: null,
@@ -84,7 +81,6 @@ describe("BankCardForm", () => {
paymentSystem: null, paymentSystem: null,
version: 1, version: 1,
}, },
/* eslint-enable unicorn/no-null */
); );
}); });
it("loads and saves the selected payment system", async () => { it("loads and saves the selected payment system", async () => {
@@ -57,7 +57,6 @@ const BankCardFields: FC<BankCardFieldsProps> = (props) => {
placeholder={t("bank.paymentSystemUnknown")} placeholder={t("bank.paymentSystemUnknown")}
value={ value={
paymentSystemField.value paymentSystemField.value
// eslint-disable-next-line unicorn/no-null -- Mantine represents an empty controlled paymentSystemField as null.
|| null || null
} }
/> />
@@ -49,16 +49,13 @@ function useBankCardFormVm(props: BankCardFormProps) {
const metadata = { const metadata = {
bankName: bankName:
values.bankName values.bankName
// eslint-disable-next-line unicorn/no-null -- PATCH uses null to clear these optional card fields.
|| null, || null,
lastFour: lastFour:
values.lastFour values.lastFour
// eslint-disable-next-line unicorn/no-null -- PATCH uses null to clear these optional card fields.
|| null, || null,
name: values.name, name: values.name,
paymentSystem: paymentSystem:
values.paymentSystem values.paymentSystem
// eslint-disable-next-line unicorn/no-null -- PATCH uses null to clear these optional card fields.
|| null, || null,
}; };
@@ -0,0 +1,49 @@
import type { ComponentProps } from "react";
import type { Control } from "react-hook-form";
import { DateInput } from "@mantine/dates";
import { useController } from "react-hook-form";
import { bankDateInputValue } from "../../model/form-validation";
import { type BankCardValues, bankDateSchema } from "../../model/form.schema";
function useOpeningDateField(control: Control<BankCardValues>) {
const { field: openingDateField } = useController({
control,
name: "openingDate",
});
const dateParser: ComponentProps<typeof DateInput>["dateParser"] = value =>
bankDateSchema.safeParse(bankDateInputValue(value)).success
? bankDateInputValue(value)
: null;
const onOpeningDateBlur: ComponentProps<typeof DateInput>["onBlur"] = (
event,
) => {
openingDateField.onChange(bankDateInputValue(event.currentTarget.value));
openingDateField.onBlur();
};
const onOpeningDateChange = (value: null | string) =>
openingDateField.onChange(value ?? "");
const onOpeningDateInput: ComponentProps<typeof DateInput>["onInput"] = event =>
openingDateField.onChange(bankDateInputValue(event.currentTarget.value));
const openingDateValue = bankDateSchema.safeParse(openingDateField.value)
.success
? openingDateField.value
: undefined;
return {
dateParser,
onOpeningDateBlur,
onOpeningDateChange,
onOpeningDateInput,
openingDateField,
openingDateValue,
};
}
export { useOpeningDateField };
@@ -0,0 +1,27 @@
import type { Control } from "react-hook-form";
import { useController } from "react-hook-form";
import { useTranslation } from "react-i18next";
import type { BankCardValues } from "../../model/form.schema";
function usePaymentSystemField(control: Control<BankCardValues>) {
const { t } = useTranslation();
const { field: paymentSystemField } = useController({
control,
name: "paymentSystem",
});
const paymentSystems = ["mir", "visa", "mastercard"].map(value => ({
label: t(`bank.paymentSystems.${value}`),
value,
}));
const onPaymentSystemChange = (value: null | string) =>
paymentSystemField.onChange(value ?? "");
return { onPaymentSystemChange, paymentSystemField, paymentSystems };
}
export { usePaymentSystemField };
@@ -10,7 +10,9 @@ const BankOperationIcon = (props: {
categoryName?: null | string; categoryName?: null | string;
kind: "expense" | "refund" | "topup" | "transfer"; kind: "expense" | "refund" | "topup" | "transfer";
}) => ( }) => (
<OperationIcon {...bankOperationAppearance(props.kind, props.categoryName)} /> <OperationIcon
{...bankOperationAppearance(props.kind, props.categoryName)}
/>
); );
const meta = { const meta = {
@@ -8,7 +8,9 @@ const BankOperationIcon = (props: {
categoryName?: null | string; categoryName?: null | string;
kind: "expense" | "refund" | "topup" | "transfer"; kind: "expense" | "refund" | "topup" | "transfer";
}) => ( }) => (
<OperationIcon {...bankOperationAppearance(props.kind, props.categoryName)} /> <OperationIcon
{...bankOperationAppearance(props.kind, props.categoryName)}
/>
); );
it("matches normalized category names, falls back after renaming and distinguishes operation kinds", () => { it("matches normalized category names, falls back after renaming and distinguishes operation kinds", () => {
@@ -28,7 +30,6 @@ it("matches normalized category names, falls back after renaming and distinguish
for (const categoryName of [ for (const categoryName of [
"Моя категория", "Моя категория",
"constructor", "constructor",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
]) { ]) {
rerender(<BankOperationIcon categoryName={categoryName} kind="expense" />); rerender(<BankOperationIcon categoryName={categoryName} kind="expense" />);
@@ -4,8 +4,6 @@ import { BankSummaryView } from "./BankSummary.view";
const money = (value: string) => ({ currency: "RUB", value }); const money = (value: string) => ({ currency: "RUB", value });
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const meta const meta
= { = {
args: { args: {
@@ -62,14 +60,14 @@ const meta
}, },
tags: ["autodocs"], tags: ["autodocs"],
title: "Product/BankCards/Summary", title: "Product/BankCards/Summary",
} satisfies /* eslint-enable unicorn/no-null */ Meta<typeof BankSummaryView>; } satisfies Meta<typeof BankSummaryView>;
export default meta; export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof meta>;
export const Loaded: Story = { name: "Расходы по категориям" }; export const Loaded: Story = { name: "Расходы по категориям" };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const RefundMonth: Story export const RefundMonth: Story
= { = {
args: { args: {
@@ -93,8 +91,7 @@ export const RefundMonth: Story
}, },
name: "Возврат покупки прошлого месяца", name: "Возврат покупки прошлого месяца",
}; };
/* eslint-enable unicorn/no-null */
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const Empty: Story export const Empty: Story
= { = {
args: { args: {
@@ -109,7 +106,7 @@ export const Empty: Story
}, },
name: "Известные нули", name: "Известные нули",
}; };
/* eslint-enable unicorn/no-null */
export const Loading: Story = { export const Loading: Story = {
args: { data: undefined, loading: true }, args: { data: undefined, loading: true },
name: "Начальная загрузка", name: "Начальная загрузка",
@@ -122,7 +119,7 @@ export const CachedError: Story = {
args: { error: true }, args: { error: true },
name: "Сохранённые суммы при ошибке обновления", name: "Сохранённые суммы при ошибке обновления",
}; };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const LongAmounts: Story export const LongAmounts: Story
= { = {
args: { args: {
@@ -145,4 +142,3 @@ export const LongAmounts: Story
}, },
name: "Точный итог длиннее предела отдельного ввода", name: "Точный итог длиннее предела отдельного ввода",
}; };
/* eslint-enable unicorn/no-null */
@@ -109,9 +109,7 @@ const BankSummaryView: FC<BankSummaryViewProps> = (props) => {
</Group> </Group>
)} )}
{props.data && props.fetching && ( {props.data && props.fetching && <Text role="status">{t("bank.refreshing")}</Text>}
<Text role="status">{t("bank.refreshing")}</Text>
)}
</Stack> </Stack>
); );
}; };
@@ -88,9 +88,7 @@ const CategoryManager: FC<CategoryManagerProps> = (props) => {
<Text>{category.name}</Text> <Text>{category.name}</Text>
{category.isDefault && ( {category.isDefault && <Badge>{t("bank.defaultCategory")}</Badge>}
<Badge>{t("bank.defaultCategory")}</Badge>
)}
</Group> </Group>
<Button onClick={category.onRename} variant="subtle"> <Button onClick={category.onRename} variant="subtle">
@@ -100,9 +98,7 @@ const CategoryManager: FC<CategoryManagerProps> = (props) => {
); );
})} })}
{categories.isLoading && ( {categories.isLoading && <Text role="status">{t("bank.loading")}</Text>}
<Text role="status">{t("bank.loading")}</Text>
)}
{categories.isError && ( {categories.isError && (
<BankFeedback <BankFeedback
@@ -34,7 +34,7 @@ const LedgerEntryRow: FC<LedgerEntryRowProps> = (props) => {
t("ledger.occurredAt", { date: formatSnapshotTime(operation.occurredAt) }), t("ledger.occurredAt", { date: formatSnapshotTime(operation.occurredAt) }),
instrument, instrument,
props.withDetails props.withDetails
? null // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. ? null
: operation.account?.name, : operation.account?.name,
] ]
.filter(Boolean) .filter(Boolean)
@@ -45,7 +45,6 @@ function brokerageAccountCards(
return { return {
account, account,
positionCounts: positionCounts:
// eslint-disable-next-line unicorn/no-null -- The existing formatter contract uses null for unavailable data.
null, null,
source, source,
summary: { state: "unavailable" }, summary: { state: "unavailable" },
@@ -64,8 +64,6 @@ export const Available: Story = {
}, },
}; };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const Unavailable: Story export const Unavailable: Story
= { = {
args: { args: {
@@ -83,7 +81,6 @@ export const Unavailable: Story
}, },
}, },
}; };
/* eslint-enable unicorn/no-null */
export const Empty: Story = { export const Empty: Story = {
args: { args: {
@@ -193,8 +190,6 @@ export const LargeValues: Story = {
name: "Длинные суммы", name: "Длинные суммы",
}; };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const IndependentFreshness: Story export const IndependentFreshness: Story
= { = {
args: { args: {
@@ -236,4 +231,3 @@ export const IndependentFreshness: Story
}, },
name: "Независимая свежесть и ошибка сводок", name: "Независимая свежесть и ошибка сводок",
}; };
/* eslint-enable unicorn/no-null */
@@ -29,7 +29,7 @@ describe("BrokerageAccountList", () => {
], ],
}, },
type: "BROKERAGE", type: "BROKERAGE",
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
}, },
{ {
id: "account-2", id: "account-2",
@@ -38,10 +38,10 @@ describe("BrokerageAccountList", () => {
portfolioSummary: null, portfolioSummary: null,
type: "BROKERAGE", type: "BROKERAGE",
}, },
/* eslint-enable unicorn/no-null */ { {
id: "account-3", id: "account-3",
name: "Пустой", name: "Пустой",
openedOn: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture. openedOn: null,
portfolioSummary: { portfolioSummary: {
bondPositionCount: 0, bondPositionCount: 0,
stockPositionCount: 2, stockPositionCount: 2,
@@ -84,7 +84,7 @@ describe("BrokerageAccountList", () => {
it("shows portfolio summary freshness independently for each account", () => { it("shows portfolio summary freshness independently for each account", () => {
const accounts = brokerageAccountCards( const accounts = brokerageAccountCards(
["fresh", "stale"].map( ["fresh", "stale"].map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
id => id =>
({ ({
id, id,
@@ -97,7 +97,6 @@ it("shows portfolio summary freshness independently for each account", () => {
}, },
type: "BROKERAGE" as const, type: "BROKERAGE" as const,
}), }),
/* eslint-enable unicorn/no-null */
), ),
[ [
resource(null, { freshness: "fresh", resourceKey: "portfolio/fresh" }) resource(null, { freshness: "fresh", resourceKey: "portfolio/fresh" })
@@ -105,13 +105,9 @@ const BrokerageAccountList: FC<BrokerageAccountListProps> = (props) => {
</Text> </Text>
)} )}
{source?.sync.state === "failed" && ( {source?.sync.state === "failed" && <Text size="xs">{t("accounts.refreshError")}</Text>}
<Text size="xs">{t("accounts.refreshError")}</Text>
)}
{source?.freshness === "stale" && ( {source?.freshness === "stale" && <Text size="xs">{t("investment.stale")}</Text>}
<Text size="xs">{t("investment.stale")}</Text>
)}
</Stack> </Stack>
{!!positionCounts && ( {!!positionCounts && (
@@ -37,7 +37,7 @@ describe("useBrokerageEvents", () => {
server.use( server.use(
http.get("*/api/v1/brokerage-accounts/account-1/events", () => http.get("*/api/v1/brokerage-accounts/account-1/events", () =>
HttpResponse.json( HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: "account-1", accountId: "account-1",
@@ -56,7 +56,6 @@ describe("useBrokerageEvents", () => {
}, },
], ],
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
), ),
@@ -67,7 +66,7 @@ describe("useBrokerageEvents", () => {
}); });
await waitFor(() => expect(result.current.isSuccess).toBe(true)); await waitFor(() => expect(result.current.isSuccess).toBe(true));
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
expect(result.current.data?.events).toEqual([ expect(result.current.data?.events).toEqual([
{ {
date: "2026-09-01", date: "2026-09-01",
@@ -80,7 +79,7 @@ describe("useBrokerageEvents", () => {
ticker: "SBER", ticker: "SBER",
type: "dividend", type: "dividend",
}, },
/* eslint-enable unicorn/no-null */
]); ]);
}); });
}); });
@@ -19,7 +19,7 @@ const BrokerageEventCard: FC<BrokerageEventCardProps> = (props) => {
date, date,
event.dateKind === "registry" event.dateKind === "registry"
? t("events.dateKind.registry") ? t("events.dateKind.registry")
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. : null,
event.isin, event.isin,
event.account?.name, event.account?.name,
] ]
@@ -6,8 +6,6 @@ import type { BrokerageEvent } from "../../api/useBrokerageEvents";
import { BrokerageEventsPreview } from "./BrokerageEventsPreview"; import { BrokerageEventsPreview } from "./BrokerageEventsPreview";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const syntheticEvent const syntheticEvent
= { = {
date: "2026-10-15", date: "2026-10-15",
@@ -20,7 +18,7 @@ const syntheticEvent
projectedAmount: { currency: "RUB", value: "850.50" }, projectedAmount: { currency: "RUB", value: "850.50" },
ticker: "DEMO", ticker: "DEMO",
type: "coupon", type: "coupon",
} satisfies /* eslint-enable unicorn/no-null */ BrokerageEvent; } satisfies BrokerageEvent;
const eventsPath = "/accounts/storybook-account/events"; const eventsPath = "/accounts/storybook-account/events";
@@ -121,8 +119,6 @@ export const Empty: Story = {
}, },
}; };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const MissingAmount: Story export const MissingAmount: Story
= { = {
args: { args: {
@@ -142,9 +138,6 @@ export const MissingAmount: Story
}, },
}, },
}; };
/* eslint-enable unicorn/no-null */
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
export const LongEvent: Story export const LongEvent: Story
= { = {
@@ -165,4 +158,3 @@ export const LongEvent: Story
}, },
name: "Длинное название на узком экране", name: "Длинное название на узком экране",
}; };
/* eslint-enable unicorn/no-null */
@@ -46,31 +46,21 @@ const BrokerageEventsPreview: FC<BrokerageEventsPreviewProps> = (props) => {
{!!events.stale && <Text size="xs">{t("investment.stale")}</Text>} {!!events.stale && <Text size="xs">{t("investment.stale")}</Text>}
{!!events.refreshError && ( {!!events.refreshError && <Text role="status">{t("accounts.refreshError")}</Text>}
<Text role="status">{t("accounts.refreshError")}</Text>
)}
</> </>
)} )}
{events.state === "loading" && ( {events.state === "loading" && (
<LoadingSkeleton label={t("events.recentLoading")}> <LoadingSkeleton label={t("events.recentLoading")}>
{["first", "second", "third"].map(placeholder => ( {["first", "second", "third"].map(placeholder => <Skeleton height={48} key={placeholder} radius="sm" />)}
<Skeleton height={48} key={placeholder} radius="sm" />
))}
</LoadingSkeleton> </LoadingSkeleton>
)} )}
{events.state === "error" && ( {events.state === "error" && <Alert color="red">{t("events.recentError")}</Alert>}
<Alert color="red">{t("events.recentError")}</Alert>
)}
{events.state === "empty" && ( {events.state === "empty" && <Text c="dimmed">{t("events.recentEmpty")}</Text>}
<Text c="dimmed">{t("events.recentEmpty")}</Text>
)}
{events.state === "partial" && ( {events.state === "partial" && <Text role="status">{t("investment.partial")}</Text>}
<Text role="status">{t("investment.partial")}</Text>
)}
{(events.state === "loaded" || events.state === "partial") {(events.state === "loaded" || events.state === "partial")
&& events.items.map(event => ( && events.items.map(event => (
@@ -56,7 +56,7 @@ describe("persistent resource query lifecycle", () => {
await waitFor(() => expect(result.current.isSuccess).toBe(true)); await waitFor(() => expect(result.current.isSuccess).toBe(true));
await act(() => result.current.observe("new")); await act(() => result.current.observe("new"));
await waitFor(() => expect(result.current.meta?.stateVersion).toBe("new")); await waitFor(() => expect(result.current.meta?.stateVersion).toBe("new"));
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
expect(modes).toEqual([null, "observe", "observe"]); expect(modes).toEqual([null, "observe", "observe"]);
expect(queryClient.getQueryCache().getAll()).toHaveLength(1); expect(queryClient.getQueryCache().getAll()).toHaveLength(1);
expect( expect(
@@ -89,14 +89,13 @@ describe("persistent resource query lifecycle", () => {
expect(result.current.status).toBe("pending"); expect(result.current.status).toBe("pending");
expect(result.current.isPending).toBe(true); expect(result.current.isPending).toBe(true);
expect(result.current.isLoading).toBe(false); expect(result.current.isLoading).toBe(false);
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
expect(result.current.transport).toEqual( expect(result.current.transport).toEqual(
{ {
error: null, error: null,
fetchStatus: "idle", fetchStatus: "idle",
status: "success", status: "success",
}, },
/* eslint-enable unicorn/no-null */
); );
expect(result.current.meta?.availability).toBe("missing"); expect(result.current.meta?.availability).toBe("missing");
}); });
-3
View File
@@ -9,15 +9,12 @@ function formatShare(
denominator: Money | null, denominator: Money | null,
): null | string { ): null | string {
if (!numerator || !denominator) if (!numerator || !denominator)
// eslint-disable-next-line unicorn/no-null -- The existing formatter contract uses null for unavailable data.
return null; return null;
const total = Number(denominator.value); const total = Number(denominator.value);
if (!Number.isFinite(total) || total <= 0) if (!Number.isFinite(total) || total <= 0)
// eslint-disable-next-line unicorn/no-null -- The existing formatter contract uses null for unavailable data.
return null; return null;
const percent = (Number(numerator.value) / total) * PERCENT_SCALE; const percent = (Number(numerator.value) / total) * PERCENT_SCALE;
if (!Number.isFinite(percent)) if (!Number.isFinite(percent))
// eslint-disable-next-line unicorn/no-null -- The existing formatter contract uses null for unavailable data.
return null; return null;
const rounded = new Intl.NumberFormat("ru-RU", { const rounded = new Intl.NumberFormat("ru-RU", {
+3 -3
View File
@@ -30,7 +30,7 @@ function createAuthWrapper(
describe("AuthProvider", () => { describe("AuthProvider", () => {
it("authorizes requests after restoring the initial session", async () => { it("authorizes requests after restoring the initial session", async () => {
let authorization: null | string let authorization: null | string
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
= null; = null;
server.use( server.use(
@@ -234,12 +234,12 @@ describe("AuthProvider", () => {
}); });
expect( expect(
result.current.user?.id result.current.user?.id
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
?? null, ?? null,
).toBe( ).toBe(
loginAfterLogout loginAfterLogout
? "b" ? "b"
: null, // eslint-disable-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract. : null,
); );
expect(order).toEqual([ expect(order).toEqual([
"refresh-start", "refresh-start",
@@ -30,9 +30,7 @@ const InstrumentPosition: FC<InstrumentPositionProps> = (props) => {
<InstrumentSection state={vm.positionState} title={t("stock.position")}> <InstrumentSection state={vm.positionState} title={t("stock.position")}>
{!!vm.position && ( {!!vm.position && (
<Stack gap="md"> <Stack gap="md">
{!!vm.position.isStale && ( {!!vm.position.isStale && <Alert color="orange">{t("portfolio.staleWarning")}</Alert>}
<Alert color="orange">{t("portfolio.staleWarning")}</Alert>
)}
<div className={`${classes.metrics} ${classes.holding}`}> <div className={`${classes.metrics} ${classes.holding}`}>
<div> <div>
@@ -103,9 +101,7 @@ const InstrumentPosition: FC<InstrumentPositionProps> = (props) => {
{account.account.name} {account.account.name}
</Anchor> </Anchor>
) )
: ( : <Text>{t("stock.portfolio")}</Text>}
<Text>{t("stock.portfolio")}</Text>
)}
<div> <div>
<Text c="dimmed" size="xs"> <Text c="dimmed" size="xs">
@@ -82,9 +82,7 @@ const InstrumentSection: FC<InstrumentSectionProps> = (props) => {
{!!props.state.stale && <Text size="xs">{t("investment.stale")}</Text>} {!!props.state.stale && <Text size="xs">{t("investment.stale")}</Text>}
{!!props.state.partial && ( {!!props.state.partial && <Text role="status">{t("investment.partial")}</Text>}
<Text role="status">{t("investment.partial")}</Text>
)}
{!!props.state.lastSuccessfulAt && ( {!!props.state.lastSuccessfulAt && (
<Text c="dimmed" size="xs"> <Text c="dimmed" size="xs">
@@ -135,12 +135,11 @@ describe("page-owned screen synchronization", () => {
expect(modes).toEqual( expect(modes).toEqual(
cached cached
? [ ? [
null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture. null,
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
"observe", "observe",
] ]
: [null, "observe"], // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture. : [null, "observe"],
); );
expect(posts).toBe(0); expect(posts).toBe(0);
expect(result.current.disabled.data).toBeUndefined(); expect(result.current.disabled.data).toBeUndefined();
@@ -316,7 +315,7 @@ describe("page-owned screen synchronization", () => {
await waitFor(() => expect(result.current.accounts.isSuccess).toBe(true)); await waitFor(() => expect(result.current.accounts.isSuccess).toBe(true));
expect(result.current.accounts.data).toEqual([]); expect(result.current.accounts.data).toEqual([]);
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
expect(modes).toEqual([null, "observe"]); expect(modes).toEqual([null, "observe"]);
expect( expect(
queryClient queryClient
@@ -357,8 +356,6 @@ describe("page-owned screen synchronization", () => {
http.get("*/api/v1/tbank-sync/:id", () => { http.get("*/api/v1/tbank-sync/:id", () => {
polls++; polls++;
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
return HttpResponse.json( return HttpResponse.json(
{ {
errorCode: null, errorCode: null,
@@ -369,7 +366,6 @@ describe("page-owned screen synchronization", () => {
scope: { screen: "accounts" }, scope: { screen: "accounts" },
state: "succeeded", state: "succeeded",
}, },
/* eslint-enable unicorn/no-null */
); );
}), }),
); );
@@ -397,14 +393,14 @@ describe("page-owned screen synchronization", () => {
expect(result.current.accounts.meta?.stateVersion).toBe("2"), expect(result.current.accounts.meta?.stateVersion).toBe("2"),
); );
expect(bodies).toEqual([{ screen: "accounts" }]); expect(bodies).toEqual([{ screen: "accounts" }]);
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
expect(modes).toEqual([null, "observe"]); expect(modes).toEqual([null, "observe"]);
vi.useFakeTimers(); vi.useFakeTimers();
try { try {
await act(() => vi.advanceTimersByTimeAsync(120_000)); await act(() => vi.advanceTimersByTimeAsync(120_000));
expect(polls).toBe(1); expect(polls).toBe(1);
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
expect(modes).toEqual([null, "observe"]); expect(modes).toEqual([null, "observe"]);
unmount(); unmount();
await vi.advanceTimersByTimeAsync(120_000); await vi.advanceTimersByTimeAsync(120_000);
@@ -429,10 +425,10 @@ describe("page-owned screen synchronization", () => {
resource( resource(
isCompleted isCompleted
? [] ? []
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable API fixture. : null,
{ {
stateVersion: isCompleted ? "covered" : "initial", stateVersion: isCompleted ? "covered" : "initial",
sync: { errorCode: null, id: "history", state: "running" }, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture. sync: { errorCode: null, id: "history", state: "running" },
}, },
); );
@@ -445,8 +441,6 @@ describe("page-owned screen synchronization", () => {
http.get("*/api/v1/tbank-sync/:id", () => { http.get("*/api/v1/tbank-sync/:id", () => {
polls++; polls++;
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
return HttpResponse.json( return HttpResponse.json(
{ {
errorCode: null, errorCode: null,
@@ -461,7 +455,6 @@ describe("page-owned screen synchronization", () => {
scope: null, scope: null,
state: "running", state: "running",
}, },
/* eslint-enable unicorn/no-null */
); );
}), }),
); );
@@ -493,7 +486,7 @@ describe("page-owned screen synchronization", () => {
await act(async () => { await act(async () => {
await new Promise(resolve => setTimeout(resolve, 1100)); await new Promise(resolve => setTimeout(resolve, 1100));
}); });
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
expect(modes).toEqual([null, "observe"]); expect(modes).toEqual([null, "observe"]);
if (boundary === "leave") unmount(); if (boundary === "leave") unmount();
else act(() => configureAuthClient()); else act(() => configureAuthClient());
@@ -515,12 +508,11 @@ describe("page-owned screen synchronization", () => {
http.get("*/api/v1/brokerage-accounts", () => http.get("*/api/v1/brokerage-accounts", () =>
HttpResponse.json( HttpResponse.json(
resource( resource(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
[], [],
{ {
sync: { errorCode: null, id: "revoked", state: "running" }, sync: { errorCode: null, id: "revoked", state: "running" },
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
), ),
@@ -550,7 +542,7 @@ describe("page-owned screen synchronization", () => {
const modes: (null | string)[] = []; const modes: (null | string)[] = [];
const dependency = resource( const dependency = resource(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
[], [],
{ {
resourceKey: "operations", resourceKey: "operations",
@@ -561,7 +553,6 @@ describe("page-owned screen synchronization", () => {
state: isObserved ? "idle" : "running", state: isObserved ? "idle" : "running",
}, },
}, },
/* eslint-enable unicorn/no-null */
).meta; ).meta;
server.use( server.use(
@@ -574,7 +565,7 @@ describe("page-owned screen synchronization", () => {
resource( resource(
isObserved isObserved
? [] ? []
: null, // eslint-disable-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract. : null,
{ {
dependencies: [ dependencies: [
{ {
@@ -588,7 +579,7 @@ describe("page-owned screen synchronization", () => {
); );
}), }),
http.get("*/api/v1/tbank-sync/:id", () => http.get("*/api/v1/tbank-sync/:id", () =>
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
HttpResponse.json( HttpResponse.json(
{ {
errorCode: null, errorCode: null,
@@ -599,7 +590,6 @@ describe("page-owned screen synchronization", () => {
scope: null, scope: null,
state: "succeeded", state: "succeeded",
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
); );
@@ -615,7 +605,7 @@ describe("page-owned screen synchronization", () => {
); );
await waitFor(() => expect(result.current.data).toEqual([])); await waitFor(() => expect(result.current.data).toEqual([]));
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
expect(modes).toEqual([null, "observe"]); expect(modes).toEqual([null, "observe"]);
}); });
@@ -206,7 +206,7 @@ function useScreenSync(scope: InvestmentScreen, resources: ScreenResource[]) {
finally { finally {
if (requestInFlightRef.current === scopeKey) if (requestInFlightRef.current === scopeKey)
requestInFlightRef.current requestInFlightRef.current
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
= null; = null;
} }
}; };
@@ -48,8 +48,6 @@ describe("sync connection and session boundaries", () => {
started += 1; started += 1;
await blocked; await blocked;
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
return HttpResponse.json( return HttpResponse.json(
{ {
...accepted, ...accepted,
@@ -59,7 +57,6 @@ describe("sync connection and session boundaries", () => {
resources: [], resources: [],
state: "succeeded", state: "succeeded",
}, },
/* eslint-enable unicorn/no-null */
); );
}), }),
); );
@@ -103,13 +100,12 @@ describe("sync connection and session boundaries", () => {
if (boundary === "connection") if (boundary === "connection")
commitTbankConnectionBoundary( commitTbankConnectionBoundary(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
queryClient, queryClient,
{ {
connected: false, connected: false,
connectedAt: null, connectedAt: null,
}, },
/* eslint-enable unicorn/no-null */
); );
else { else {
generation += 1; generation += 1;
@@ -77,7 +77,7 @@ function renderSection(client: QueryClient) {
function seededClient() { function seededClient() {
const client = createTestQueryClient(); const client = createTestQueryClient();
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
client.setQueryData(brokerageAccountsQueryKey, [ client.setQueryData(brokerageAccountsQueryKey, [
{ {
id: "account-1", id: "account-1",
@@ -85,7 +85,7 @@ function seededClient() {
openedOn: null, openedOn: null,
type: "BROKERAGE", type: "BROKERAGE",
}, },
/* eslint-enable unicorn/no-null */
]); ]);
client.setQueryData(portfolioQueryKey, { source: "previous-connection" }); client.setQueryData(portfolioQueryKey, { source: "previous-connection" });
client.setQueryData(stockQueryKey, { source: "previous-connection" }); client.setQueryData(stockQueryKey, { source: "previous-connection" });
@@ -157,10 +157,9 @@ describe("TbankConnectionSection", () => {
return reads === 1 return reads === 1
? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 }) ? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 })
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
: HttpResponse.json( : HttpResponse.json(
{ connected: false, connectedAt: null }, { connected: false, connectedAt: null },
/* eslint-enable unicorn/no-null */
); );
}), }),
); );
@@ -12,12 +12,8 @@ const TbankConnectionSection: FC = () => {
if (viewModel.state === "loading") return <TbankConnectionLoading />; if (viewModel.state === "loading") return <TbankConnectionLoading />;
return viewModel.state === "error" return viewModel.state === "error"
? ( ? <TbankConnectionError retry={viewModel.retry} />
<TbankConnectionError retry={viewModel.retry} /> : <TbankConnectionSectionLoaded {...viewModel} />;
)
: (
<TbankConnectionSectionLoaded {...viewModel} />
);
}; };
export { TbankConnectionSection }; export { TbankConnectionSection };
@@ -129,9 +129,7 @@ const TbankConnectionSectionLoaded: FC<TbankConnectionSectionLoadedProps> = (
<Stack> <Stack>
<Text>{t("tbank.deleteText")}</Text> <Text>{t("tbank.deleteText")}</Text>
{remove.isError && ( {remove.isError && <Alert color="red">{t("tbank.requestError")}</Alert>}
<Alert color="red">{t("tbank.requestError")}</Alert>
)}
<Button <Button
color="red" color="red"
@@ -59,7 +59,6 @@ function useTbankConnectionSectionLoadedVm(
commitTbankConnectionBoundary(queryClient, { commitTbankConnectionBoundary(queryClient, {
connected: false, connected: false,
connectedAt: connectedAt:
// eslint-disable-next-line unicorn/no-null -- The disconnected API snapshot requires a null connection timestamp.
null, null,
}); });
setIsDeleteOpened(false); setIsDeleteOpened(false);
@@ -100,7 +100,6 @@ describe("Upcoming event date filters", () => {
return HttpResponse.json( return HttpResponse.json(
resource( resource(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
null, null,
{ {
sync: { sync: {
@@ -109,7 +108,6 @@ describe("Upcoming event date filters", () => {
state: "failed", state: "failed",
}, },
}, },
/* eslint-enable unicorn/no-null */
), ),
); );
}), }),
@@ -37,7 +37,7 @@ function renderPage() {
describe("AccountEventsPage", () => { describe("AccountEventsPage", () => {
beforeEach(() => { beforeEach(() => {
refresh.mockClear(); refresh.mockClear();
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
query.mockReturnValue( query.mockReturnValue(
{ {
data: { data: {
@@ -52,7 +52,7 @@ describe("AccountEventsPage", () => {
sync: { errorCode: null, id: null, state: "idle" }, sync: { errorCode: null, id: null, state: "idle" },
}, },
refetch: vi.fn(), refetch: vi.fn(),
} as /* eslint-enable unicorn/no-null */ never, } as never,
); );
}); });
@@ -73,7 +73,6 @@ describe("AccountEventsPage", () => {
}); });
it("keeps the loading fallback when no result or account is available", () => { it("keeps the loading fallback when no result or account is available", () => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
query.mockReturnValue( query.mockReturnValue(
{ {
data: undefined, data: undefined,
@@ -84,7 +83,7 @@ describe("AccountEventsPage", () => {
sync: { errorCode: null, id: null, state: "idle" }, sync: { errorCode: null, id: null, state: "idle" },
}, },
refetch: vi.fn(), refetch: vi.fn(),
} as /* eslint-enable unicorn/no-null */ never, } as never,
); );
renderPage(); renderPage();
expect(query).toHaveBeenCalledWith("", {}); expect(query).toHaveBeenCalledWith("", {});
@@ -95,7 +94,6 @@ describe("AccountEventsPage", () => {
}); });
it("renders events and marks an offer without an amount", () => { it("renders events and marks an offer without an amount", () => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
query.mockReturnValue( query.mockReturnValue(
{ {
data: { data: {
@@ -135,7 +133,7 @@ describe("AccountEventsPage", () => {
sync: { errorCode: null, id: null, state: "idle" }, sync: { errorCode: null, id: null, state: "idle" },
}, },
refetch: vi.fn(), refetch: vi.fn(),
} as /* eslint-enable unicorn/no-null */ never, } as never,
); );
renderPage(); renderPage();
expect(screen.getByText("1 250 ₽")).toBeInTheDocument(); expect(screen.getByText("1 250 ₽")).toBeInTheDocument();
@@ -183,7 +181,6 @@ describe("AccountEventsPage", () => {
}); });
it("shows a structural busy state before events are available", () => { it("shows a structural busy state before events are available", () => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
query.mockReturnValue( query.mockReturnValue(
{ {
data: undefined, data: undefined,
@@ -194,7 +191,7 @@ describe("AccountEventsPage", () => {
sync: { errorCode: null, id: null, state: "idle" }, sync: { errorCode: null, id: null, state: "idle" },
}, },
refetch: vi.fn(), refetch: vi.fn(),
} as /* eslint-enable unicorn/no-null */ never, } as never,
); );
renderPage(); renderPage();
@@ -12,12 +12,8 @@ const AccountEventsPage: FC<AccountEventsPageProps> = (props) => {
if (viewModel.state === "loading") return <AccountEventsPageLoading />; if (viewModel.state === "loading") return <AccountEventsPageLoading />;
return viewModel.state === "error" return viewModel.state === "error"
? ( ? <AccountEventsPageError {...viewModel} />
<AccountEventsPageError {...viewModel} /> : <AccountEventsPageLoaded {...viewModel} />;
)
: (
<AccountEventsPageLoaded {...viewModel} />
);
}; };
export { AccountEventsPage }; export { AccountEventsPage };
@@ -41,11 +41,11 @@ function useAccountEventsPageViewModel(
const appliedFrom = isValidUrlPeriod const appliedFrom = isValidUrlPeriod
? urlFrom ? urlFrom
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. : null;
const appliedTo = isValidUrlPeriod const appliedTo = isValidUrlPeriod
? urlTo ? urlTo
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. : null;
const [draftFrom, setDraftFrom] = useState<null | string>(appliedFrom); const [draftFrom, setDraftFrom] = useState<null | string>(appliedFrom);
const [draftTo, setDraftTo] = useState<null | string>(appliedTo); const [draftTo, setDraftTo] = useState<null | string>(appliedTo);
@@ -6,9 +6,7 @@ import { useTranslation } from "react-i18next";
const AccountEventsPageEmpty: FC = () => { const AccountEventsPageEmpty: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return <StateView text={t("events.emptyText")} title={t("events.emptyTitle")} />;
<StateView text={t("events.emptyText")} title={t("events.emptyTitle")} />
);
}; };
export { AccountEventsPageEmpty }; export { AccountEventsPageEmpty };
@@ -74,13 +74,9 @@ const AccountEventsPageLoaded: FC<AccountEventsPageLoadedProps> = (props) => {
{t("investment.refresh")} {t("investment.refresh")}
</Button> </Button>
{!!props.partial && ( {!!props.partial && <Alert role="status">{t("investment.partial")}</Alert>}
<Alert role="status">{t("investment.partial")}</Alert>
)}
{!!props.refreshing && ( {!!props.refreshing && <Progress animated aria-label={t("accounts.refreshing")} value={100} />}
<Progress animated aria-label={t("accounts.refreshing")} value={100} />
)}
{!!props.refreshError && ( {!!props.refreshError && (
<Alert aria-live="polite" color="yellow" role="status"> <Alert aria-live="polite" color="yellow" role="status">
@@ -27,9 +27,7 @@ const AccountEventsPageLoading: FC = () => {
<Skeleton height={36} width={108} /> <Skeleton height={36} width={108} />
</Group> </Group>
{skeletonRowIds.map(id => ( {skeletonRowIds.map(id => <Skeleton height={82} key={id} radius="md" />)}
<Skeleton height={82} key={id} radius="md" />
))}
</LoadingSkeleton> </LoadingSkeleton>
); );
}; };
+1 -2
View File
@@ -37,14 +37,13 @@ function moscowDate(value: Date): string {
function parseDraftDate(value: string): null | string { function parseDraftDate(value: string): null | string {
if (!/^\d{2}\.\d{2}\.\d{4}$/.test(value)) if (!/^\d{2}\.\d{2}\.\d{4}$/.test(value))
// eslint-disable-next-line unicorn/no-null -- Mantine date fields use null for an incomplete or invalid date.
return null; return null;
const [day, month, year] = value.split(".", DATE_PART_COUNT); const [day, month, year] = value.split(".", DATE_PART_COUNT);
const date = `${year}-${month}-${day}`; const date = `${year}-${month}-${day}`;
return isDate(date) return isDate(date)
? date ? date
: null; // eslint-disable-line unicorn/no-null -- Mantine date fields use null for an incomplete or invalid date. : null;
} }
export { export {
@@ -38,8 +38,6 @@ import {
} from "./AccountIncomePage.vm"; } from "./AccountIncomePage.vm";
import { IncomePayments } from "./components/IncomePayments.view"; import { IncomePayments } from "./components/IncomePayments.view";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const analytics const analytics
= { = {
accountId: "account-1", accountId: "account-1",
@@ -81,7 +79,7 @@ const analytics
}, },
totalsByCurrency: [summary("RUB", "10", "2")], totalsByCurrency: [summary("RUB", "10", "2")],
yearComparison: null, yearComparison: null,
} satisfies /* eslint-enable unicorn/no-null */ IncomeAnalytics; } satisfies IncomeAnalytics;
function comparison(currency: string) { function comparison(currency: string) {
return { return {
@@ -93,28 +91,24 @@ function comparison(currency: string) {
} }
function entry(): IncomeAnalytics["entries"][number] { function entry(): IncomeAnalytics["entries"][number] {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */ return {
return ( accruedInterest: null,
{ category: "income",
accruedInterest: null, commission: null,
category: "income", description: "Купон",
commission: null, figi: "FIGI",
description: "Купон", id: "coupon-1",
figi: "FIGI", instrumentKey: "income-group-opaque-v2",
id: "coupon-1", instrumentType: "bond",
instrumentKey: "income-group-opaque-v2", instrumentUid: null,
instrumentType: "bond", logoName: null,
instrumentUid: null, occurredAt: "2026-08-01T12:00:00Z",
logoName: null, payment: money("RUB", "10"),
occurredAt: "2026-08-01T12:00:00Z", price: null,
payment: money("RUB", "10"), quantity: null,
price: null, ticker: "BOND",
quantity: null, type: "OPERATION_TYPE_COUPON",
ticker: "BOND", };
type: "OPERATION_TYPE_COUPON",
}
/* eslint-enable unicorn/no-null */
);
} }
function money(currency: string, value: string) { function money(currency: string, value: string) {
@@ -211,7 +205,6 @@ describe("AccountIncomePage", () => {
return HttpResponse.json( return HttpResponse.json(
resource( resource(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
null, null,
{ {
sync: { sync: {
@@ -220,7 +213,6 @@ describe("AccountIncomePage", () => {
state: "failed", state: "failed",
}, },
}, },
/* eslint-enable unicorn/no-null */
), ),
); );
}, },
@@ -434,7 +426,6 @@ describe("AccountIncomePage", () => {
}); });
it("renders the signed drill-down using opaque membership rather than instrument identity", async () => { it("renders the signed drill-down using opaque membership rather than instrument identity", async () => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const response const response
= { = {
...analytics, ...analytics,
@@ -464,7 +455,6 @@ describe("AccountIncomePage", () => {
], ],
}; };
/* eslint-enable unicorn/no-null */
server.use( server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () => http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json(resource(response)), HttpResponse.json(resource(response)),
@@ -607,37 +597,31 @@ describe("income comparison display", () => {
[ [
"10", "10",
"0", "0",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
], ],
[ [
"0", "0",
"0", "0",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
], ],
[ [
"10", "10",
"-0.00", "-0.00",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
], ],
[ [
"10", "10",
"-100", "-100",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
], ],
[ [
"9".repeat(400), "9".repeat(400),
"1", "1",
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
], ],
[ [
"1", "1",
"9".repeat(400), "9".repeat(400),
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
null, null,
], ],
])("formats change %s against base %s", (difference, base, expected) => { ])("formats change %s against base %s", (difference, base, expected) => {
@@ -656,7 +640,7 @@ describe("income comparison display", () => {
const result = display?.comparison?.values[0]; const result = display?.comparison?.values[0];
expect( expect(
result?.percentages.coupon?.replaceAll("\u{A0}", " ") result?.percentages.coupon?.replaceAll("\u{A0}", " ")
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
?? null, ?? null,
).toBe(expected); ).toBe(expected);
expect(result?.difference.coupon.value).toBe(difference); expect(result?.difference.coupon.value).toBe(difference);
@@ -863,7 +847,7 @@ describe("income period and snapshot boundary", () => {
), ),
), ),
http.get("*/api/v1/tbank-sync/:id", () => http.get("*/api/v1/tbank-sync/:id", () =>
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
HttpResponse.json( HttpResponse.json(
{ {
errorCode: null, errorCode: null,
@@ -874,7 +858,6 @@ describe("income period and snapshot boundary", () => {
scope: null, scope: null,
state: "succeeded", state: "succeeded",
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () => http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
@@ -998,8 +981,6 @@ describe("income period and snapshot boundary", () => {
it("maps comparison notices by role regardless of dependency order or resource key", () => { it("maps comparison notices by role regardless of dependency order or resource key", () => {
const selected = { ...resource(analytics).meta, role: "selected" as const }; const selected = { ...resource(analytics).meta, role: "selected" as const };
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const previous const previous
= { = {
...resource(null).meta, ...resource(null).meta,
@@ -1012,7 +993,6 @@ it("maps comparison notices by role regardless of dependency order or resource k
}, },
}; };
/* eslint-enable unicorn/no-null */
const year = { const year = {
...resource(null).meta, ...resource(null).meta,
resourceKey: "opaque/previous-looking-key", resourceKey: "opaque/previous-looking-key",
@@ -1041,10 +1021,10 @@ it.each([false, true])(
server.use( server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () => http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json( HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ ...analytics, previous: null, yearComparison: null }, { ...analytics, previous: null, yearComparison: null },
/* eslint-enable unicorn/no-null */ { {
dependencies: legacy dependencies: legacy
? [ ? [
resource(null).meta, resource(null).meta,
@@ -1073,22 +1053,21 @@ it.each([false, true])(
it("shows selected income before comparison coverage without inventing a zero baseline", async () => { it("shows selected income before comparison coverage without inventing a zero baseline", async () => {
const dependencies = (["year", "selected", "previous"] as const).map( const dependencies = (["year", "selected", "previous"] as const).map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
role => role =>
({ ({
...resource(role === "selected" ? analytics : null).meta, ...resource(role === "selected" ? analytics : null).meta,
role, role,
}), }),
/* eslint-enable unicorn/no-null */
); );
server.use( server.use(
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () => http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
HttpResponse.json( HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ ...analytics, previous: null, yearComparison: null }, { ...analytics, previous: null, yearComparison: null },
/* eslint-enable unicorn/no-null */ { dependencies }, { dependencies },
), ),
), ),
), ),
@@ -1116,7 +1095,7 @@ it("keeps complete previous comparison while year source failed without inventin
{ {
...resource(analytics).meta, ...resource(analytics).meta,
role: "previous" as const, role: "previous" as const,
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
}, },
{ {
...resource(null).meta, ...resource(null).meta,
@@ -1127,7 +1106,7 @@ it("keeps complete previous comparison while year source failed without inventin
state: "failed" as const, state: "failed" as const,
}, },
}, },
/* eslint-enable unicorn/no-null */ { {
...resource(analytics).meta, ...resource(analytics).meta,
role: "selected" as const, role: "selected" as const,
}, },
@@ -12,12 +12,8 @@ const AccountIncomePage: FC<AccountIncomePageProps> = (props) => {
if (viewModel.state === "loading") return <AccountIncomePageLoading />; if (viewModel.state === "loading") return <AccountIncomePageLoading />;
return viewModel.state === "error" return viewModel.state === "error"
? ( ? <AccountIncomePageError {...viewModel} />
<AccountIncomePageError {...viewModel} /> : <AccountIncomePageLoaded {...viewModel} />;
)
: (
<AccountIncomePageLoaded {...viewModel} />
);
}; };
export { AccountIncomePage }; export { AccountIncomePage };
@@ -161,7 +161,6 @@ function comparisonDisplay(
comparison: IncomeAnalytics["yearComparison"], comparison: IncomeAnalytics["yearComparison"],
): IncomeDisplayModel["comparison"] { ): IncomeDisplayModel["comparison"] {
if (!comparison) if (!comparison)
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
return null; return null;
return { return {
@@ -220,11 +219,9 @@ function formatIncomeChange(
// Display-only ratio; server-provided Money values remain exact decimal strings. // Display-only ratio; server-provided Money values remain exact decimal strings.
const base = Number(compared); const base = Number(compared);
if (!Number.isFinite(base) || base <= 0) if (!Number.isFinite(base) || base <= 0)
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
return null; return null;
const ratio = Number(difference) / base; const ratio = Number(difference) / base;
if (!Number.isFinite(ratio)) if (!Number.isFinite(ratio))
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
return null; return null;
return new Intl.NumberFormat("ru-RU", { return new Intl.NumberFormat("ru-RU", {
@@ -272,7 +269,7 @@ function toIncomeDisplayModel(
|| instruments.has(instrument.key) || instruments.has(instrument.key)
|| !currencies.has(instrument.total.currency) || !currencies.has(instrument.total.currency)
) )
// eslint-disable-next-line unicorn/no-null -- Preserve the existing nullable validation/ref contract.
return null; return null;
instruments.set(instrument.key, { instruments.set(instrument.key, {
currency: instrument.total.currency, currency: instrument.total.currency,
@@ -286,11 +283,9 @@ function toIncomeDisplayModel(
for (const entry of analytics.entries) { for (const entry of analytics.entries) {
if (typeof entry.instrumentKey !== "string" || !entry.instrumentKey.trim()) if (typeof entry.instrumentKey !== "string" || !entry.instrumentKey.trim())
// eslint-disable-next-line unicorn/no-null -- Preserve the existing nullable validation/ref contract.
return null; return null;
const instrument = instruments.get(entry.instrumentKey); const instrument = instruments.get(entry.instrumentKey);
if (!instrument || entry.payment?.currency !== instrument.currency) if (!instrument || entry.payment?.currency !== instrument.currency)
// eslint-disable-next-line unicorn/no-null -- Preserve the existing nullable validation/ref contract.
return null; return null;
const occurrenceKey = JSON.stringify([entry.instrumentKey, entry.id]); const occurrenceKey = JSON.stringify([entry.instrumentKey, entry.id]);
const occurrence = occurrences.get(occurrenceKey) ?? 0; const occurrence = occurrences.get(occurrenceKey) ?? 0;
@@ -309,7 +304,6 @@ function toIncomeDisplayModel(
for (const instrument of instruments.values()) { for (const instrument of instruments.values()) {
if (instrument.includedCount !== instrument.receiptCount) if (instrument.includedCount !== instrument.receiptCount)
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
return null; return null;
} }
@@ -35,12 +35,8 @@ const AccountIncomePageLoadedSummary: FC<LoadedProps> = (props) => {
} }
return analytics.totalsByCurrency.length > 0 return analytics.totalsByCurrency.length > 0
? ( ? <IncomeSummary totals={analytics.totalsByCurrency} />
<IncomeSummary totals={analytics.totalsByCurrency} /> : <StateView text={t("income.emptyText")} title={t("income.emptyTitle")} />;
)
: (
<StateView text={t("income.emptyText")} title={t("income.emptyTitle")} />
);
}; };
const AccountIncomePageLoaded: FC<LoadedProps> = (props) => { const AccountIncomePageLoaded: FC<LoadedProps> = (props) => {
@@ -34,9 +34,7 @@ const IncomeComparisons: FC<IncomeComparisonsProps> = (props) => {
<Title order={2}>{props.title}</Title> <Title order={2}>{props.title}</Title>
{props.comparison {props.comparison
? ( ? <Comparison {...props.comparison} title={props.title} />
<Comparison {...props.comparison} title={props.title} />
)
: ( : (
<Paper className={classes.comparisonNotice} p="sm" radius="md"> <Paper className={classes.comparisonNotice} p="sm" radius="md">
<Text aria-live="polite" c="gray.7" role="status" size="sm"> <Text aria-live="polite" c="gray.7" role="status" size="sm">
@@ -25,7 +25,7 @@ const PeriodExample: FC<PeriodExampleProps> = (props) => {
const submitted = isSubmitSuccessful const submitted = isSubmitSuccessful
? methods.getValues() ? methods.getValues()
: null; // eslint-disable-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract. : null;
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
@@ -24,8 +24,6 @@ import { server } from "@/test/server";
import { AccountOperationsPage } from "./AccountOperationsPage"; import { AccountOperationsPage } from "./AccountOperationsPage";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const operation: BrokerageOperation const operation: BrokerageOperation
= { = {
accruedInterest: null, accruedInterest: null,
@@ -44,7 +42,6 @@ const operation: BrokerageOperation
ticker: "SBER", ticker: "SBER",
type: "OPERATION_TYPE_BUY", type: "OPERATION_TYPE_BUY",
}; };
/* eslint-enable unicorn/no-null */
const Navigation: FC = () => { const Navigation: FC = () => {
const location = useLocation(); const location = useLocation();
@@ -322,10 +319,9 @@ describe("AccountOperationsPage", () => {
new URL(request.url).searchParams.has("from") new URL(request.url).searchParams.has("from")
? HttpResponse.json({ code: "VALIDATION_ERROR" }, { status: 400 }) ? HttpResponse.json({ code: "VALIDATION_ERROR" }, { status: 400 })
: HttpResponse.json( : HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ hasNext: false, items: [], nextCursor: null }, { hasNext: false, items: [], nextCursor: null },
/* eslint-enable unicorn/no-null */
), ),
), ),
), ),
@@ -368,7 +364,7 @@ describe("AccountOperationsPage", () => {
searches.push(url.search); searches.push(url.search);
return HttpResponse.json( return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: "account-1", accountId: "account-1",
@@ -377,7 +373,6 @@ describe("AccountOperationsPage", () => {
nextCursor: url.searchParams.get("cursor") ? null : "next-1", nextCursor: url.searchParams.get("cursor") ? null : "next-1",
observedAt: "2026-08-14T00:00:00Z", observedAt: "2026-08-14T00:00:00Z",
}, },
/* eslint-enable unicorn/no-null */
), ),
); );
}, },
@@ -411,7 +406,7 @@ describe("AccountOperationsPage", () => {
searches.push(url.search); searches.push(url.search);
return HttpResponse.json( return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: "account-1", accountId: "account-1",
@@ -420,7 +415,6 @@ describe("AccountOperationsPage", () => {
nextCursor: null, nextCursor: null,
observedAt: "2026-08-14T00:00:00Z", observedAt: "2026-08-14T00:00:00Z",
}, },
/* eslint-enable unicorn/no-null */
), ),
); );
}, },
@@ -451,7 +445,7 @@ describe("AccountOperationsPage", () => {
searches.push(url.search); searches.push(url.search);
return HttpResponse.json( return HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: "account-1", accountId: "account-1",
@@ -460,7 +454,6 @@ describe("AccountOperationsPage", () => {
nextCursor: null, nextCursor: null,
observedAt: "2026-08-14T00:00:00Z", observedAt: "2026-08-14T00:00:00Z",
}, },
/* eslint-enable unicorn/no-null */
), ),
); );
}, },
@@ -508,7 +501,6 @@ it("keeps UTC operation boundaries in GET and source sync requests", async () =>
return HttpResponse.json( return HttpResponse.json(
resource( resource(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
null, null,
{ {
sync: { sync: {
@@ -517,7 +509,6 @@ it("keeps UTC operation boundaries in GET and source sync requests", async () =>
state: "failed", state: "failed",
}, },
}, },
/* eslint-enable unicorn/no-null */
), ),
); );
}, },
@@ -559,7 +550,7 @@ it("does not label incomplete empty operations as a confirmed empty period", asy
server.use( server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () => http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
HttpResponse.json( HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: "account-1", accountId: "account-1",
@@ -567,7 +558,7 @@ it("does not label incomplete empty operations as a confirmed empty period", asy
items: [], items: [],
nextCursor: null, nextCursor: null,
}, },
/* eslint-enable unicorn/no-null */ { availability: "partial" }, { availability: "partial" },
), ),
), ),
), ),
@@ -15,12 +15,8 @@ const AccountOperationsPage: FC<AccountOperationsPageProps> = (props) => {
} }
return viewModel.state === "error" return viewModel.state === "error"
? ( ? <AccountOperationsPageError {...viewModel} />
<AccountOperationsPageError {...viewModel} /> : <AccountOperationsPageLoaded {...viewModel} />;
)
: (
<AccountOperationsPageLoaded {...viewModel} />
);
}; };
export { AccountOperationsPage }; export { AccountOperationsPage };
@@ -54,11 +54,11 @@ const useAccountOperationsPageVm = (
const appliedFrom = isValidUrlPeriod const appliedFrom = isValidUrlPeriod
? urlFrom ? urlFrom
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. : null;
const appliedTo = isValidUrlPeriod const appliedTo = isValidUrlPeriod
? urlTo ? urlTo
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. : null;
const appliedCategories = operationCategories.filter(category => const appliedCategories = operationCategories.filter(category =>
(searchParameters.get("categories") ?? "").split(",").includes(category), (searchParameters.get("categories") ?? "").split(",").includes(category),
@@ -50,9 +50,7 @@ const AccountOperationsPageLoadedOperations: FC<
return; return;
} }
return ( return <StateView text={t("ledger.emptyText")} title={t("ledger.emptyTitle")} />;
<StateView text={t("ledger.emptyText")} title={t("ledger.emptyTitle")} />
);
}; };
const AccountOperationsPageLoaded: FC<AccountOperationsPageLoadedProps> = ( const AccountOperationsPageLoaded: FC<AccountOperationsPageLoadedProps> = (
@@ -91,15 +89,11 @@ const AccountOperationsPageLoaded: FC<AccountOperationsPageLoadedProps> = (
{t("investment.refresh")} {t("investment.refresh")}
</Button> </Button>
{!!props.partial && ( {!!props.partial && <Alert role="status">{t("investment.partial")}</Alert>}
<Alert role="status">{t("investment.partial")}</Alert>
)}
{!!props.historyLoading && <Alert>{t("investment.history")}</Alert>} {!!props.historyLoading && <Alert>{t("investment.history")}</Alert>}
{!!props.refreshing && ( {!!props.refreshing && <Progress animated aria-label={t("accounts.refreshing")} value={100} />}
<Progress animated aria-label={t("accounts.refreshing")} value={100} />
)}
{!!props.refreshError && ( {!!props.refreshError && (
<Alert aria-live="polite" color="yellow" role="status"> <Alert aria-live="polite" color="yellow" role="status">
@@ -28,9 +28,7 @@ const AccountOperationsPageLoading: FC = () => {
</Group> </Group>
<Stack gap="xs"> <Stack gap="xs">
{skeletonRowIds.map(id => ( {skeletonRowIds.map(id => <Skeleton height={64} key={id} radius="md" />)}
<Skeleton height={64} key={id} radius="md" />
))}
</Stack> </Stack>
</LoadingSkeleton> </LoadingSkeleton>
); );
@@ -67,8 +67,6 @@ function renderPage(ui: React.ReactNode) {
); );
} }
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const portfolio: Portfolio const portfolio: Portfolio
= { = {
cashBalances: [{ currency: "USD", value: "0.5" }], cashBalances: [{ currency: "USD", value: "0.5" }],
@@ -103,7 +101,6 @@ const portfolio: Portfolio
structure: { currencies: [], unvaluedCategories: [] }, structure: { currencies: [], unvaluedCategories: [] },
totalsByCurrency: [{ currency: "RUB", value: "1234.5" }], totalsByCurrency: [{ currency: "RUB", value: "1234.5" }],
}; };
/* eslint-enable unicorn/no-null */
const emptyRecentOperations: AccountPageRecentOperations = { const emptyRecentOperations: AccountPageRecentOperations = {
state: "empty", state: "empty",
@@ -146,8 +143,6 @@ describe("AccountPage states", () => {
const uid = "c190ff1f-1447-4227-b543-316332699ca5"; const uid = "c190ff1f-1447-4227-b543-316332699ca5";
const requests = vi.fn(); const requests = vi.fn();
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const position const position
= { = {
...portfolio.positions[0], ...portfolio.positions[0],
@@ -158,7 +153,6 @@ describe("AccountPage states", () => {
type, type,
}; };
/* eslint-enable unicorn/no-null */
server.use( server.use(
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
HttpResponse.json( HttpResponse.json(
@@ -483,7 +477,6 @@ describe("AccountPage states", () => {
it("renders stock cards with logo fallback and signed unlabeled return", () => { it("renders stock cards with logo fallback and signed unlabeled return", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps( {...loadedProps(
{ {
...portfolio, ...portfolio,
@@ -514,7 +507,6 @@ describe("AccountPage states", () => {
}, },
], ],
}, },
/* eslint-enable unicorn/no-null */
)} )}
/>, />,
); );
@@ -528,7 +520,6 @@ describe("AccountPage states", () => {
it("groups stock supporting metrics into quiet single lines", () => { it("groups stock supporting metrics into quiet single lines", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps( {...loadedProps(
{ {
...portfolio, ...portfolio,
@@ -547,7 +538,6 @@ describe("AccountPage states", () => {
}, },
], ],
}, },
/* eslint-enable unicorn/no-null */
)} )}
/>, />,
); );
@@ -563,7 +553,6 @@ describe("AccountPage states", () => {
it("uses the ticker only for stock cards and otherwise keeps the ISIN", () => { it("uses the ticker only for stock cards and otherwise keeps the ISIN", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps( {...loadedProps(
{ {
...portfolio, ...portfolio,
@@ -606,7 +595,6 @@ describe("AccountPage states", () => {
}, },
], ],
}, },
/* eslint-enable unicorn/no-null */
)} )}
/>, />,
); );
@@ -623,7 +611,6 @@ describe("AccountPage states", () => {
it("renders bond cards with logo, fallback, and signed unlabeled return like stocks", () => { it("renders bond cards with logo, fallback, and signed unlabeled return like stocks", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps( {...loadedProps(
{ {
...portfolio, ...portfolio,
@@ -654,7 +641,6 @@ describe("AccountPage states", () => {
}, },
], ],
}, },
/* eslint-enable unicorn/no-null */
)} )}
/>, />,
); );
@@ -674,7 +660,6 @@ describe("AccountPage states", () => {
it("renders bond cards in expanded layout with bond-specific classes", () => { it("renders bond cards in expanded layout with bond-specific classes", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps( {...loadedProps(
{ {
...portfolio, ...portfolio,
@@ -693,7 +678,6 @@ describe("AccountPage states", () => {
}, },
], ],
}, },
/* eslint-enable unicorn/no-null */
)} )}
/>, />,
); );
@@ -707,7 +691,6 @@ describe("AccountPage states", () => {
it("keeps other-position cards compact with labeled return and no logo", () => { it("keeps other-position cards compact with labeled return and no logo", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{...loadedProps( {...loadedProps(
{ {
...portfolio, ...portfolio,
@@ -726,7 +709,6 @@ describe("AccountPage states", () => {
}, },
], ],
}, },
/* eslint-enable unicorn/no-null */
)} )}
/>, />,
); );
@@ -738,17 +720,14 @@ describe("AccountPage states", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
{...loadedProps()} {...loadedProps()}
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
account={ account={{
{ id: "account-1",
id: "account-1", name: "Основной",
name: "Основной", openedOn: null,
openedOn: null, portfolioSummary: null,
portfolioSummary: null, type: "BROKERAGE",
type: "BROKERAGE", }}
}
/* eslint-enable unicorn/no-null */
}
/>, />,
); );
expect( expect(
@@ -777,33 +756,30 @@ describe("AccountPage states", () => {
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
{...loadedProps()} {...loadedProps()}
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
recentOperations={ recentOperations={{
{ items: [
items: [ {
{ accruedInterest: null,
accruedInterest: null, category: "trade",
category: "trade", commission: null,
commission: null, description: "Покупка акции",
description: "Покупка акции", figi: "FIGI-1",
figi: "FIGI-1", id: "operation-1",
id: "operation-1", instrumentType: "share",
instrumentType: "share", instrumentUid: "UID-1",
instrumentUid: "UID-1", logoName: null,
logoName: null, occurredAt: "2026-08-14T10:00:00Z",
occurredAt: "2026-08-14T10:00:00Z", payment: { currency: "RUB", value: "-12" },
payment: { currency: "RUB", value: "-12" }, price: null,
price: null, quantity: null,
quantity: null, ticker: "SBER",
ticker: "SBER", type: "OPERATION_TYPE_BUY",
type: "OPERATION_TYPE_BUY", },
}, ],
], state: "loaded",
state: "loaded", to: "/accounts/account-1/operations",
to: "/accounts/account-1/operations", }}
}
/* eslint-enable unicorn/no-null */
}
/>, />,
); );
@@ -812,7 +788,6 @@ describe("AccountPage states", () => {
}); });
it("keeps future events ordered with calendar dates and projected rather than posted amounts", () => { it("keeps future events ordered with calendar dates and projected rather than posted amounts", () => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const event: BrokerageEvent const event: BrokerageEvent
= { = {
date: "2026-09-10", date: "2026-09-10",
@@ -827,41 +802,37 @@ describe("AccountPage states", () => {
type: "coupon", type: "coupon",
}; };
/* eslint-enable unicorn/no-null */
renderPage( renderPage(
<AccountPageLoaded <AccountPageLoaded
{...loadedProps()} {...loadedProps()}
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
recentEvents={ recentEvents={{
{ items: [
items: [ event,
event, {
{ ...event,
...event, date: "2026-09-11",
date: "2026-09-11", dateKind: "registry",
dateKind: "registry", displayName: "Акция",
displayName: "Акция", projectedAmount: { currency: "USD", value: "0" },
projectedAmount: { currency: "USD", value: "0" }, type: "dividend",
type: "dividend", },
}, {
{ ...event,
...event, date: "2026-09-12",
date: "2026-09-12", projectedAmount: { currency: "RUB", value: "1000" },
projectedAmount: { currency: "RUB", value: "1000" }, type: "maturity",
type: "maturity", },
}, {
{ ...event,
...event, date: "2026-09-13",
date: "2026-09-13", projectedAmount: null,
projectedAmount: null, type: "offer",
type: "offer", },
}, ],
], state: "loaded",
state: "loaded", to: "/accounts/account-1/events",
to: "/accounts/account-1/events", }}
}
/* eslint-enable unicorn/no-null */
}
/>, />,
); );
@@ -953,7 +924,6 @@ describe("AccountPage states", () => {
it.each([503])( it.each([503])(
"preserves totals and UTC snapshot through refresh error %i, retry and success", "preserves totals and UTC snapshot through refresh error %i, retry and success",
async (status) => { async (status) => {
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const initialPortfolio: Portfolio const initialPortfolio: Portfolio
= { = {
...portfolio, ...portfolio,
@@ -970,7 +940,6 @@ describe("AccountPage states", () => {
}, },
}; };
/* eslint-enable unicorn/no-null */
let requestCount = 0; let requestCount = 0;
let resolveRetry: ((response: Response) => void) | undefined; let resolveRetry: ((response: Response) => void) | undefined;
server.use( server.use(
@@ -985,7 +954,7 @@ describe("AccountPage states", () => {
), ),
), ),
http.get("*/api/v1/tbank-sync/:id", () => http.get("*/api/v1/tbank-sync/:id", () =>
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
HttpResponse.json( HttpResponse.json(
{ {
errorCode: null, errorCode: null,
@@ -999,7 +968,6 @@ describe("AccountPage states", () => {
scope: { accountId: "account-1", screen: "account" }, scope: { accountId: "account-1", screen: "account" },
state: "succeeded", state: "succeeded",
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => { http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => {
@@ -18,8 +18,6 @@ import {
useAccountPageViewModel, useAccountPageViewModel,
} from "./AccountPage.vm"; } from "./AccountPage.vm";
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const portfolio: Portfolio const portfolio: Portfolio
= { = {
cashBalances: [{ currency: "USD", value: "1" }], cashBalances: [{ currency: "USD", value: "1" }],
@@ -66,7 +64,6 @@ const portfolio: Portfolio
structure: { currencies: [], unvaluedCategories: [] }, structure: { currencies: [], unvaluedCategories: [] },
totalsByCurrency: [{ currency: "RUB", value: "150" }], totalsByCurrency: [{ currency: "RUB", value: "150" }],
}; };
/* eslint-enable unicorn/no-null */
function configureTestAuth() { function configureTestAuth() {
configureAuthClient({ configureAuthClient({
@@ -101,7 +98,7 @@ function mockLoadedAccountPage() {
), ),
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () => http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
HttpResponse.json( HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ {
accountId: "account-1", accountId: "account-1",
@@ -127,7 +124,6 @@ function mockLoadedAccountPage() {
nextCursor: null, nextCursor: null,
observedAt: "2026-08-13T10:00:00Z", observedAt: "2026-08-13T10:00:00Z",
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
), ),
@@ -138,7 +134,7 @@ function mockLoadedAccountPage() {
calculatedAt: "2026-08-13T10:00:00Z", calculatedAt: "2026-08-13T10:00:00Z",
events: Array.from( events: Array.from(
{ length: 6 }, { length: 6 },
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
(_, index) => (_, index) =>
({ ({
date: `2026-09-${String(index + 1).padStart(2, "0")}`, date: `2026-09-${String(index + 1).padStart(2, "0")}`,
@@ -150,7 +146,6 @@ function mockLoadedAccountPage() {
ticker: `BND${index}`, ticker: `BND${index}`,
type: "coupon", type: "coupon",
}), }),
/* eslint-enable unicorn/no-null */
), ),
}), }),
), ),
@@ -181,7 +176,7 @@ describe("useAccountPageViewModel", () => {
type: "CURRENCY", type: "CURRENCY",
}, },
...otherTypes.map( ...otherTypes.map(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
type => type =>
({ ({
...portfolio.positions[2], ...portfolio.positions[2],
@@ -190,7 +185,6 @@ describe("useAccountPageViewModel", () => {
figi: `FIGI-${type}-NEXT`, figi: `FIGI-${type}-NEXT`,
type, type,
}), }),
/* eslint-enable unicorn/no-null */
), ),
], ],
structure: { currencies: [], unvaluedCategories: [] }, structure: { currencies: [], unvaluedCategories: [] },
@@ -248,7 +242,7 @@ describe("useAccountPageViewModel", () => {
shareText: "66,7 %", shareText: "66,7 %",
}, },
], ],
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
}, },
{ {
kind: "bonds", kind: "bonds",
@@ -262,15 +256,15 @@ describe("useAccountPageViewModel", () => {
}, },
], ],
}, },
/* eslint-enable unicorn/no-null */ { {
kind: "other", kind: "other",
positions: [ positions: [
{ {
displayName: "Фонд", displayName: "Фонд",
logoSrc: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture. logoSrc: null,
returnText: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture. returnText: null,
returnTone: undefined, returnTone: undefined,
shareText: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture. shareText: null,
}, },
], ],
}, },
@@ -351,10 +345,10 @@ it("keeps overview sections independent when their source data is incomplete", a
), ),
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () => http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
HttpResponse.json( HttpResponse.json(
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
resource( resource(
{ hasNext: false, items: [], nextCursor: null }, { hasNext: false, items: [], nextCursor: null },
/* eslint-enable unicorn/no-null */ { availability: "partial" }, { availability: "partial" },
), ),
), ),
), ),
@@ -385,8 +379,6 @@ it("keeps overview sections independent when their source data is incomplete", a
it("refreshes every overview section through one account descriptor and observational reads", async () => { it("refreshes every overview section through one account descriptor and observational reads", async () => {
configureTestAuth(); configureTestAuth();
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
const sections: Record<string, unknown> const sections: Record<string, unknown>
= { = {
"events": { events: [], incomeTotalsByCurrency: [] }, "events": { events: [], incomeTotalsByCurrency: [] },
@@ -395,7 +387,6 @@ it("refreshes every overview section through one account descriptor and observat
portfolio, portfolio,
}; };
/* eslint-enable unicorn/no-null */
const observed: string[] = []; const observed: string[] = [];
const scopes: unknown[] = []; const scopes: unknown[] = [];
let version = "1"; let version = "1";
@@ -427,7 +418,7 @@ it("refreshes every overview section through one account descriptor and observat
); );
}), }),
http.get("*/api/v1/tbank-sync/:id", () => http.get("*/api/v1/tbank-sync/:id", () =>
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
HttpResponse.json( HttpResponse.json(
{ {
errorCode: null, errorCode: null,
@@ -441,7 +432,6 @@ it("refreshes every overview section through one account descriptor and observat
scope: null, scope: null,
state: "succeeded", state: "succeeded",
}, },
/* eslint-enable unicorn/no-null */
), ),
), ),
); );

Some files were not shown because too many files have changed in this diff Show More