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
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:
@@ -19,8 +19,6 @@ import {
|
||||
useBankSummary,
|
||||
} from "./queries";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const card: BankCard
|
||||
= {
|
||||
archivedAt: null,
|
||||
@@ -35,7 +33,6 @@ const card: BankCard
|
||||
version: 1,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
const transfer: CreateBankOperation = {
|
||||
amount: { currency: "RUB", value: "90071992547409.93" },
|
||||
cardId: "a",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
enum BankName {
|
||||
AlfaBank = "Альфа-Банк",
|
||||
Sberbank = "Сбербанк",
|
||||
TBank = "Т-Банк",
|
||||
Vtb = "ВТБ",
|
||||
}
|
||||
|
||||
export { BankName };
|
||||
@@ -32,7 +32,6 @@ function bankToday() {
|
||||
function moneyCents(value: string): bigint | null {
|
||||
const normalized = value.trim().replace(",", ".");
|
||||
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;
|
||||
|
||||
const [whole, fraction = ""] = normalized
|
||||
|
||||
@@ -2,129 +2,68 @@ import type { OperationIconProps } from "@wealthledger/icons";
|
||||
|
||||
import type { BankOperation } from "../api/queries";
|
||||
|
||||
type OperationIcon = {
|
||||
animation?: string;
|
||||
color: string;
|
||||
variant: OperationIconProps["variant"];
|
||||
};
|
||||
const categoryIcons: {
|
||||
appearance: OperationIconProps;
|
||||
name: string;
|
||||
}[] = [
|
||||
{ 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> = {
|
||||
"алкоголь": {
|
||||
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",
|
||||
const otherIcon: OperationIconProps = {
|
||||
color: "gray",
|
||||
variant: "grid",
|
||||
};
|
||||
|
||||
const operationIcons: Record<
|
||||
Exclude<BankOperation["kind"], "expense">,
|
||||
OperationIcon
|
||||
OperationIconProps
|
||||
> = {
|
||||
refund: { color: "teal", variant: "refund" },
|
||||
topup: {
|
||||
animation: "topup",
|
||||
color: "green",
|
||||
variant: "topup",
|
||||
},
|
||||
transfer: {
|
||||
animation: "transfer",
|
||||
color: "blue",
|
||||
variant: "transfer",
|
||||
},
|
||||
@@ -135,17 +74,20 @@ function bankOperationAppearance(
|
||||
categoryName?: null | string,
|
||||
) {
|
||||
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];
|
||||
|
||||
const name = categoryName
|
||||
?.trim()
|
||||
.toLocaleLowerCase("ru-RU")
|
||||
.replaceAll("ё", "е");
|
||||
const name = normalizeCategoryName(categoryName ?? "");
|
||||
|
||||
return name && Object.hasOwn(categoryIcons, name)
|
||||
? categoryIcons[name] // eslint-disable-line security/detect-object-injection -- Operation kinds are finite; category names are guarded by Object.hasOwn.
|
||||
: otherIcon;
|
||||
const category = categoryIcons.find(
|
||||
item => normalizeCategoryName(item.name) === name,
|
||||
);
|
||||
|
||||
return category?.appearance ?? otherIcon;
|
||||
}
|
||||
|
||||
function normalizeCategoryName(name: string) {
|
||||
return name.trim().toLocaleLowerCase("ru-RU").replaceAll("ё", "е");
|
||||
}
|
||||
|
||||
export { bankOperationAppearance };
|
||||
|
||||
@@ -2,17 +2,16 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
|
||||
import { SimpleGrid } from "@mantine/core";
|
||||
|
||||
import { BankName } from "../../model/bank";
|
||||
import { BankCardFace } from "./BankCardFace";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const meta
|
||||
= {
|
||||
args: {
|
||||
card: {
|
||||
archivedAt: null,
|
||||
balance: { currency: "RUB", value: "128450.75" },
|
||||
bankName: "Сбербанк",
|
||||
bankName: BankName.Sberbank,
|
||||
id: "fictional",
|
||||
lastFour: "4829",
|
||||
name: "Для повседневных покупок",
|
||||
@@ -25,7 +24,7 @@ const meta
|
||||
component: BankCardFace,
|
||||
tags: ["autodocs"],
|
||||
title: "Product/BankCards/CardAppearance",
|
||||
} satisfies /* eslint-enable unicorn/no-null */ Meta<typeof BankCardFace>;
|
||||
} satisfies Meta<typeof BankCardFace>;
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -35,33 +34,28 @@ export const Banks: Story = {
|
||||
render: arguments_ => (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} maw={920}>
|
||||
{[
|
||||
"Сбербанк",
|
||||
"Т-Банк",
|
||||
"Альфа-Банк",
|
||||
"ВТБ",
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
BankName.Sberbank,
|
||||
BankName.TBank,
|
||||
BankName.AlfaBank,
|
||||
BankName.Vtb,
|
||||
null,
|
||||
].map((bankName, index) => (
|
||||
<BankCardFace
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
card={
|
||||
{
|
||||
...arguments_.card,
|
||||
bankName,
|
||||
paymentSystem:
|
||||
card={{
|
||||
...arguments_.card,
|
||||
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.
|
||||
(["mir", "mastercard", "visa", "mir", null] as const)[index]
|
||||
?? null,
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */
|
||||
}
|
||||
}}
|
||||
key={bankName ?? "unknown"}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
),
|
||||
};
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const MissingDetails: Story
|
||||
= {
|
||||
args: {
|
||||
@@ -73,7 +67,7 @@ export const MissingDetails: Story
|
||||
},
|
||||
},
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
export const LongArchived: Story = {
|
||||
args: {
|
||||
card: {
|
||||
|
||||
@@ -22,9 +22,7 @@ const BankCardFace: FC<BankCardFaceProps> = (props) => {
|
||||
<div className={classes.top}>
|
||||
<span className={classes.bank}>{card.bankName || t("bank.card")}</span>
|
||||
|
||||
{card.archivedAt && (
|
||||
<span className={classes.archive}>{t("bank.archived")}</span>
|
||||
)}
|
||||
{card.archivedAt && <span className={classes.archive}>{t("bank.archived")}</span>}
|
||||
</div>
|
||||
|
||||
<div aria-hidden="true" className={classes.hardware}>
|
||||
@@ -46,9 +44,7 @@ const BankCardFace: FC<BankCardFaceProps> = (props) => {
|
||||
|
||||
{card.paymentSystem && (
|
||||
<span className={classes.network} data-network={card.paymentSystem}>
|
||||
{card.paymentSystem === "mastercard" && (
|
||||
<span aria-hidden="true" className={classes.circles} />
|
||||
)}
|
||||
{card.paymentSystem === "mastercard" && <span aria-hidden="true" className={classes.circles} />}
|
||||
|
||||
{t(`bank.paymentSystems.${card.paymentSystem}`)}
|
||||
</span>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { DateInput } from "@mantine/dates";
|
||||
import { useController, useFormContext } from "react-hook-form";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { bankDateInputValue } from "../../model/form-validation";
|
||||
import { type BankCardValues, bankDateSchema } from "../../model/form.schema";
|
||||
import type { BankCardValues } from "../../model/form.schema";
|
||||
|
||||
import { useOpeningDateField } from "./useOpeningDateField";
|
||||
import { usePaymentSystemField } from "./usePaymentSystemField";
|
||||
|
||||
function useBankCardFieldsVm() {
|
||||
const {
|
||||
@@ -15,62 +14,10 @@ function useBankCardFieldsVm() {
|
||||
} = useFormContext<BankCardValues>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const openingDate = useOpeningDateField(control);
|
||||
const paymentSystem = usePaymentSystemField(control);
|
||||
|
||||
const { field: paymentSystemField } = useController({
|
||||
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,
|
||||
};
|
||||
return { ...openingDate, ...paymentSystem, errors, register, t };
|
||||
}
|
||||
|
||||
export { useBankCardFieldsVm };
|
||||
|
||||
@@ -12,8 +12,6 @@ import type { BankCard } from "../../api/queries";
|
||||
|
||||
import { BankCardForm } from "./BankCardForm";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const card: BankCard
|
||||
= {
|
||||
archivedAt: null,
|
||||
@@ -28,7 +26,6 @@ const card: BankCard
|
||||
version: 1,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
function setup(value: BankCard) {
|
||||
configureAuthClient({
|
||||
accessToken: () => "test",
|
||||
@@ -75,7 +72,7 @@ describe("BankCardForm", () => {
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
|
||||
await waitFor(() => expect(close).toHaveBeenCalled());
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
expect(body).toEqual(
|
||||
{
|
||||
bankName: null,
|
||||
@@ -84,7 +81,6 @@ describe("BankCardForm", () => {
|
||||
paymentSystem: null,
|
||||
version: 1,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
);
|
||||
});
|
||||
it("loads and saves the selected payment system", async () => {
|
||||
|
||||
@@ -57,7 +57,6 @@ const BankCardFields: FC<BankCardFieldsProps> = (props) => {
|
||||
placeholder={t("bank.paymentSystemUnknown")}
|
||||
value={
|
||||
paymentSystemField.value
|
||||
// eslint-disable-next-line unicorn/no-null -- Mantine represents an empty controlled paymentSystemField as null.
|
||||
|| null
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -49,16 +49,13 @@ function useBankCardFormVm(props: BankCardFormProps) {
|
||||
const metadata = {
|
||||
bankName:
|
||||
values.bankName
|
||||
// eslint-disable-next-line unicorn/no-null -- PATCH uses null to clear these optional card fields.
|
||||
|| null,
|
||||
lastFour:
|
||||
values.lastFour
|
||||
// eslint-disable-next-line unicorn/no-null -- PATCH uses null to clear these optional card fields.
|
||||
|| null,
|
||||
name: values.name,
|
||||
paymentSystem:
|
||||
values.paymentSystem
|
||||
// eslint-disable-next-line unicorn/no-null -- PATCH uses null to clear these optional card fields.
|
||||
|| 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;
|
||||
kind: "expense" | "refund" | "topup" | "transfer";
|
||||
}) => (
|
||||
<OperationIcon {...bankOperationAppearance(props.kind, props.categoryName)} />
|
||||
<OperationIcon
|
||||
{...bankOperationAppearance(props.kind, props.categoryName)}
|
||||
/>
|
||||
);
|
||||
|
||||
const meta = {
|
||||
|
||||
@@ -8,7 +8,9 @@ const BankOperationIcon = (props: {
|
||||
categoryName?: null | string;
|
||||
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", () => {
|
||||
@@ -28,7 +30,6 @@ it("matches normalized category names, falls back after renaming and distinguish
|
||||
for (const categoryName of [
|
||||
"Моя категория",
|
||||
"constructor",
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
null,
|
||||
]) {
|
||||
rerender(<BankOperationIcon categoryName={categoryName} kind="expense" />);
|
||||
|
||||
@@ -4,8 +4,6 @@ import { BankSummaryView } from "./BankSummary.view";
|
||||
|
||||
const money = (value: string) => ({ currency: "RUB", value });
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const meta
|
||||
= {
|
||||
args: {
|
||||
@@ -62,14 +60,14 @@ const meta
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
title: "Product/BankCards/Summary",
|
||||
} satisfies /* eslint-enable unicorn/no-null */ Meta<typeof BankSummaryView>;
|
||||
} satisfies Meta<typeof BankSummaryView>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Loaded: Story = { name: "Расходы по категориям" };
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const RefundMonth: Story
|
||||
= {
|
||||
args: {
|
||||
@@ -93,8 +91,7 @@ export const RefundMonth: Story
|
||||
},
|
||||
name: "Возврат покупки прошлого месяца",
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const Empty: Story
|
||||
= {
|
||||
args: {
|
||||
@@ -109,7 +106,7 @@ export const Empty: Story
|
||||
},
|
||||
name: "Известные нули",
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
export const Loading: Story = {
|
||||
args: { data: undefined, loading: true },
|
||||
name: "Начальная загрузка",
|
||||
@@ -122,7 +119,7 @@ export const CachedError: Story = {
|
||||
args: { error: true },
|
||||
name: "Сохранённые суммы при ошибке обновления",
|
||||
};
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const LongAmounts: Story
|
||||
= {
|
||||
args: {
|
||||
@@ -145,4 +142,3 @@ export const LongAmounts: Story
|
||||
},
|
||||
name: "Точный итог длиннее предела отдельного ввода",
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
@@ -109,9 +109,7 @@ const BankSummaryView: FC<BankSummaryViewProps> = (props) => {
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{props.data && props.fetching && (
|
||||
<Text role="status">{t("bank.refreshing")}</Text>
|
||||
)}
|
||||
{props.data && props.fetching && <Text role="status">{t("bank.refreshing")}</Text>}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -88,9 +88,7 @@ const CategoryManager: FC<CategoryManagerProps> = (props) => {
|
||||
|
||||
<Text>{category.name}</Text>
|
||||
|
||||
{category.isDefault && (
|
||||
<Badge>{t("bank.defaultCategory")}</Badge>
|
||||
)}
|
||||
{category.isDefault && <Badge>{t("bank.defaultCategory")}</Badge>}
|
||||
</Group>
|
||||
|
||||
<Button onClick={category.onRename} variant="subtle">
|
||||
@@ -100,9 +98,7 @@ const CategoryManager: FC<CategoryManagerProps> = (props) => {
|
||||
);
|
||||
})}
|
||||
|
||||
{categories.isLoading && (
|
||||
<Text role="status">{t("bank.loading")}</Text>
|
||||
)}
|
||||
{categories.isLoading && <Text role="status">{t("bank.loading")}</Text>}
|
||||
|
||||
{categories.isError && (
|
||||
<BankFeedback
|
||||
|
||||
Reference in New Issue
Block a user