chore: finish frontend ESLint migration #18

Enable every plugin recommended preset and replace Biome with ESLint formatting.

Preserve API contracts and JSX whitespace; verify source, tests, builds and tooling.
This commit is contained in:
2026-09-28 16:36:40 +03:00
committed by ksv741
parent 57630102e4
commit 72d7835227
253 changed files with 10059 additions and 8864 deletions
@@ -1,20 +1,21 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { FormProvider, useForm } from "react-hook-form";
import type { BankCardValues } from "../../model/form-validation";
import { BankCardFields } from "./BankCardForm.view";
const meta = {
title: "Product/BankCards/CardFields",
component: BankCardFields,
tags: ["autodocs"],
args: { archived: false },
component: BankCardFields,
decorators: [
(Story) => {
const methods = useForm<BankCardValues>({
defaultValues: {
name: "Учебная повседневная карта",
bankName: "Вымышленный банк",
lastFour: "1234",
name: "Учебная повседневная карта",
openingBalance: "-92233720368547758.08",
openingDate: "2026-08-01",
},
@@ -34,11 +35,13 @@ const meta = {
},
},
},
tags: ["autodocs"],
title: "Product/BankCards/CardFields",
} satisfies Meta<typeof BankCardFields>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Active: Story = { name: "Независимая карта в рублях" };
export const Archived: Story = {
name: "Финансовые поля архивной карты",
args: { archived: true },
name: "Финансовые поля архивной карты",
};
@@ -1,31 +1,36 @@
import { MantineProvider } from "@mantine/core";
import { QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { HttpResponse, http } from "msw";
import { http, HttpResponse } from "msw";
import { describe, expect, it, vi } from "vitest";
import { configureAuthClient } from "@/shared/api";
import { createTestQueryClient } from "@/test/query";
import { server } from "@/test/server";
import type { BankCard } from "../../api/queries";
import { BankCardForm } from "./BankCardForm";
const card: BankCard = {
id: "a",
name: "Основная",
bankName: "Банк",
lastFour: "1234",
paymentSystem: null,
openingBalance: { currency: "RUB", value: "100" },
balance: { currency: "RUB", value: "50" },
openingDate: "2026-01-01",
archivedAt: null,
version: 1,
};
const card: BankCard = (/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{
archivedAt: null,
balance: { currency: "RUB", value: "50" },
bankName: "Банк",
id: "a",
lastFour: "1234",
name: "Основная",
openingBalance: { currency: "RUB", value: "100" },
openingDate: "2026-01-01",
paymentSystem: null,
version: 1,
}
/* eslint-enable unicorn/no-null */);
function setup(value: BankCard) {
configureAuthClient({
accessToken: () => "test",
refresh: async () => false,
clear: () => {},
refresh: () => Promise.resolve(false),
});
server.use(http.get("*/api/v1/bank-cards/a", () => HttpResponse.json(value)));
const close = vi.fn();
@@ -64,13 +69,15 @@ describe("BankCardForm", () => {
);
fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
await waitFor(() => expect(close).toHaveBeenCalled());
expect(body).toEqual({
version: 1,
name: "Основная",
bankName: null,
lastFour: null,
paymentSystem: null,
});
expect(body).toEqual((/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
{
bankName: null,
lastFour: null,
name: "Основная",
paymentSystem: null,
version: 1,
}
/* eslint-enable unicorn/no-null */));
});
it("loads and saves the selected payment system", async () => {
const close = setup({ ...card, paymentSystem: "mir" });
@@ -146,6 +153,6 @@ describe("BankCardForm", () => {
fireEvent.click(adopt);
fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
await waitFor(() => expect(close).toHaveBeenCalled());
expect(bodies.map((b) => b.version)).toEqual([1, 2]);
expect(bodies.map(b => b.version)).toEqual([1, 2]);
});
});
@@ -3,12 +3,14 @@ import { Button, Group, Modal, Stack, Text } from "@mantine/core";
import { type FC, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { formatDate } from "@/shared/lib";
import { useBankMutations } from "../../api/mutations";
import { type BankCard, useBankCard } from "../../api/queries";
import {
type BankCardValues,
bankCardSchema,
type BankCardValues,
bankMoneyValue,
bankToday,
} from "../../model/form-validation";
@@ -29,29 +31,29 @@ const BankCardForm: FC<BankCardFormProps> = (props) => {
const fresh = useBankCard(props.card?.id);
const mutation = useBankMutations();
const methods = useForm<BankCardValues>({
resolver: zodResolver(bankCardSchema),
defaultValues: {
name: props.card?.name ?? "",
bankName: props.card?.bankName ?? "",
lastFour: props.card?.lastFour ?? "",
paymentSystem: props.card?.paymentSystem ?? "",
name: props.card?.name ?? "",
openingBalance: props.card?.openingBalance.value ?? "0",
openingDate: props.card?.openingDate ?? bankToday(),
paymentSystem: props.card?.paymentSystem ?? "",
},
resolver: zodResolver(bankCardSchema),
});
const pending =
!opened ||
methods.formState.isSubmitting ||
(fresh.isFetching && Boolean(error));
const isPending
= !opened
|| methods.formState.isSubmitting
|| (fresh.isFetching && Boolean(error));
return (
<Modal
opened={opened}
closeButtonProps={{ "aria-label": t("bank.close"), "disabled": isPending }}
closeOnClickOutside={!isPending}
closeOnEscape={!isPending}
onClose={() => !isPending && setOpened(false)}
onExitTransitionEnd={props.onClose}
onClose={() => !pending && setOpened(false)}
opened={opened}
title={t(props.card ? "bank.editCard" : "bank.addCard")}
closeButtonProps={{ "aria-label": t("bank.close"), disabled: pending }}
closeOnClickOutside={!pending}
closeOnEscape={!pending}
>
<FormProvider {...methods}>
<form
@@ -59,10 +61,10 @@ const BankCardForm: FC<BankCardFormProps> = (props) => {
setError(undefined);
try {
const metadata = {
bankName: values.bankName || /* eslint-disable-line unicorn/no-null -- PATCH uses null to clear these optional card fields. */ null,
lastFour: values.lastFour || /* eslint-disable-line unicorn/no-null -- PATCH uses null to clear these optional card fields. */ null,
name: values.name,
bankName: values.bankName || null,
lastFour: values.lastFour || null,
paymentSystem: values.paymentSystem || null,
paymentSystem: values.paymentSystem || /* eslint-disable-line unicorn/no-null -- PATCH uses null to clear these optional card fields. */ null,
};
const financial = {
openingBalance: {
@@ -73,12 +75,12 @@ const BankCardForm: FC<BankCardFormProps> = (props) => {
};
if (props.card)
await mutation.updateCard.mutateAsync({
id: props.card.id,
body: {
...metadata,
...(!props.card.archivedAt ? financial : {}),
...(!props.card.archivedAt && financial),
version,
},
id: props.card.id,
});
else
await mutation.createCard.mutateAsync({
@@ -87,32 +89,33 @@ const BankCardForm: FC<BankCardFormProps> = (props) => {
...financial,
});
setOpened(false);
} catch (failure) {
setError(failure);
}
catch (error_) {
setError(error_);
}
})}
>
<fieldset className={classes.fieldset} disabled={pending}>
<fieldset className={classes.fieldset} disabled={isPending}>
<Stack>
<BankCardFields archived={Boolean(props.card?.archivedAt)} />
{error ? (
{!!(error) && ((
<>
<BankFeedback error={error} />
{props.card && (
<Button
variant="default"
onClick={async () => {
const response = await fresh.refetch();
if (response.data && !response.error)
setLatest(response.data);
else setError(response.error);
}}
variant="default"
>
{t("bank.reloadRecord")}
</Button>
)}
</>
) : null}
))}
{latest && (
<>
<Text>
@@ -121,22 +124,22 @@ const BankCardForm: FC<BankCardFormProps> = (props) => {
})}
</Text>
<Button
variant="default"
onClick={() => {
setVersion(latest.version);
setLatest(undefined);
setError(undefined);
}}
variant="default"
>
{t("bank.adoptVersion", { version: latest.version })}
</Button>
</>
)}
<Group>
<Button type="submit" loading={pending}>
<Button loading={isPending} type="submit">
{t("bank.save")}
</Button>
<Button variant="default" onClick={() => setOpened(false)}>
<Button onClick={() => setOpened(false)} variant="default">
{t("bank.cancel")}
</Button>
</Group>
@@ -1,8 +1,10 @@
import type { FC } from "react";
import { Select, Stack, Text, TextInput } from "@mantine/core";
import { DateInput } from "@mantine/dates";
import type { FC } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
type BankCardValues,
bankDateInputValue,
@@ -14,8 +16,8 @@ type BankCardFieldsProps = { archived: boolean };
const BankCardFields: FC<BankCardFieldsProps> = (props) => {
const {
control,
register,
formState: { errors },
register,
} = useFormContext<BankCardValues>();
const { t } = useTranslation();
return (
@@ -32,8 +34,8 @@ const BankCardFields: FC<BankCardFieldsProps> = (props) => {
error={errors.bankName?.message}
/>
<TextInput
label={t("bank.lastFour")}
inputMode="numeric"
label={t("bank.lastFour")}
{...register("lastFour")}
error={errors.lastFour?.message}
/>
@@ -43,25 +45,25 @@ const BankCardFields: FC<BankCardFieldsProps> = (props) => {
render={({ field }) => (
<Select
{...field}
value={field.value || null}
onChange={(value) => field.onChange(value ?? "")}
label={t("bank.paymentSystem")}
placeholder={t("bank.paymentSystemUnknown")}
clearable
data={["mir", "visa", "mastercard"].map((value) => ({
value,
label: t(`bank.paymentSystems.${value}`),
}))}
comboboxProps={{ withinPortal: false }}
data={["mir", "visa", "mastercard"].map(value => ({
label: t(`bank.paymentSystems.${value}`),
value,
}))}
error={errors.paymentSystem?.message}
label={t("bank.paymentSystem")}
onChange={value => field.onChange(value ?? "")}
placeholder={t("bank.paymentSystemUnknown")}
value={field.value || /* eslint-disable-line unicorn/no-null -- Mantine represents an empty controlled field as null. */ null}
/>
)}
/>
<TextInput
label={t("bank.openingBalance")}
required
inputMode="decimal"
label={t("bank.openingBalance")}
readOnly={props.archived}
required
{...register("openingBalance")}
error={errors.openingBalance?.message}
/>
@@ -71,37 +73,35 @@ const BankCardFields: FC<BankCardFieldsProps> = (props) => {
render={({ field }) => (
<DateInput
{...field}
label={t("bank.openingDate")}
required
valueFormat="DD.MM.YYYY"
value={
bankDateSchema.safeParse(field.value).success ? field.value : null
}
dateParser={(value) =>
dateParser={value =>
bankDateSchema.safeParse(bankDateInputValue(value)).success
? bankDateInputValue(value)
: null
}
: /* eslint-disable-line unicorn/no-null -- Mantine represents an empty controlled field as null. */ null}
error={errors.openingDate?.message}
fixOnBlur={false}
onInput={(event) =>
field.onChange(bankDateInputValue(event.currentTarget.value))
}
onChange={(value) => field.onChange(value ?? "")}
label={t("bank.openingDate")}
maxDate={bankToday()}
onBlur={(event) => {
field.onChange(bankDateInputValue(event.currentTarget.value));
field.onBlur();
}}
maxDate={bankToday()}
onChange={value => field.onChange(value ?? "")}
onInput={event =>
field.onChange(bankDateInputValue(event.currentTarget.value))}
popoverProps={{
withinPortal: false,
middlewares: { shift: { crossAxis: true } },
withinPortal: false,
}}
readOnly={props.archived}
error={errors.openingDate?.message}
required
value={
(bankDateSchema.safeParse(field.value).success) ? (field.value) : undefined
}
valueFormat="DD.MM.YYYY"
/>
)}
/>
<Text size="sm" c="dimmed">
<Text c="dimmed" size="sm">
{t("bank.openingHint")}
</Text>
{props.archived && <Text>{t("bank.archiveHint")}</Text>}