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
@@ -7,36 +7,36 @@ import {
screen,
waitFor,
} from "@testing-library/react";
import { HttpResponse, http } from "msw";
import { http, HttpResponse } from "msw";
import { describe, expect, it } from "vitest";
import { configureAuthClient } from "@/shared/api";
import { createTestQueryClient } from "@/test/query";
import { server } from "@/test/server";
import { CategoryManager } from "./CategoryManager";
describe("CategoryManager", () => {
it("ignores a late reload for A while editing B and submits B's draft and version", async () => {
configureAuthClient({
accessToken: () => "test",
refresh: async () => false,
clear: () => {},
refresh: () => Promise.resolve(false),
});
const categories = [
{ id: "a", name: "Первая", isDefault: false, version: 1 },
{ id: "b", name: "Вторая", isDefault: false, version: 4 },
{ id: "a", isDefault: false, name: "Первая", version: 1 },
{ id: "b", isDefault: false, name: "Вторая", version: 4 },
];
let reads = 0;
let release = () => {};
const reloaded = new Promise<void>((resolve) => {
release = resolve;
});
const patches: { id: unknown; body: unknown }[] = [];
const { promise: reloaded, resolve: release } = Promise.withResolvers<void>();
const patches: { body: unknown; id: unknown }[] = [];
server.use(
http.get("*/api/v1/bank-cards/categories", async () => {
reads++;
if (reads === 3) await reloaded;
return HttpResponse.json(
categories.map((category) =>
categories.map(category =>
category.id === "a" && reads > 1
? {
...category,
@@ -49,8 +49,8 @@ describe("CategoryManager", () => {
}),
http.patch(
"*/api/v1/bank-cards/categories/:id",
async ({ request, params }) => {
patches.push({ id: params.id, body: await request.json() });
async ({ params, request }) => {
patches.push({ body: await request.json(), id: params.id });
return patches.length === 1
? HttpResponse.json(
{ code: "BANK_VERSION_CONFLICT" },
@@ -67,9 +67,8 @@ describe("CategoryManager", () => {
</MantineProvider>
</QueryClientProvider>,
);
fireEvent.click(
(await screen.findAllByRole("button", { name: "Переименовать" }))[0],
);
const renameButtons = await screen.findAllByRole("button", { name: "Переименовать" });
fireEvent.click(renameButtons[0]);
fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
fireEvent.click(
await screen.findByRole("button", { name: "Перечитать запись" }),
@@ -99,8 +98,8 @@ describe("CategoryManager", () => {
fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
await waitFor(() =>
expect(patches).toEqual([
{ id: "a", body: { name: "Первая", version: 1 } },
{ id: "b", body: { name: "Новая вторая", version: 4 } },
{ body: { name: "Первая", version: 1 }, id: "a" },
{ body: { name: "Новая вторая", version: 4 }, id: "b" },
]),
);
});
@@ -110,15 +109,15 @@ describe("CategoryManager", () => {
async (action) => {
configureAuthClient({
accessToken: () => "test",
refresh: async () => false,
clear: () => {},
refresh: () => Promise.resolve(false),
});
let version = 1;
let writes = 0;
server.use(
http.get("*/api/v1/bank-cards/categories", () =>
HttpResponse.json([
{ id: "a", name: "Первая", isDefault: false, version },
{ id: "a", isDefault: false, name: "Первая", version },
]),
),
http.patch("*/api/v1/bank-cards/categories/a", () => {
@@ -161,13 +160,13 @@ describe("CategoryManager", () => {
it("rejects duplicate case-insensitive and too long names then renames default identity", async () => {
configureAuthClient({
accessToken: () => "test",
refresh: async () => false,
clear: () => {},
refresh: () => Promise.resolve(false),
});
let category = {
id: "default",
name: "Другое",
isDefault: true,
name: "Другое",
version: 1,
};
let creates = 0;
@@ -17,7 +17,9 @@ import {
} from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { validationMessage } from "@/shared/i18n";
import { useBankMutations } from "../../api/mutations";
import { type ExpenseCategory, useBankCategories } from "../../api/queries";
import { BankFeedback } from "../BankFeedback";
@@ -60,44 +62,40 @@ const CategoryManager: FC<CategoryManagerProps> = (props) => {
const categories = useBankCategories();
const mutation = useBankMutations();
const [editing, setEditing] = useState<ExpenseCategory>();
const editSession = useRef(0);
const editSessionRef = useRef(0);
const [error, setError] = useState<unknown>();
const [latest, setLatest] = useState<ExpenseCategory>();
const methods = useForm<CategoryValues>({
resolver: zodResolver(
z.object({
name: z
.string()
.trim()
.refine(
(value) =>
value.length > 0 && value.length <= MAX_CATEGORY_NAME_LENGTH,
{ error: validationMessage("bank.validation.category") },
)
.refine(
(value) =>
!categories.data?.some(
(c) =>
c.id !== editing?.id &&
c.name.toLocaleLowerCase("ru-RU") ===
value.toLocaleLowerCase("ru-RU"),
),
{ error: validationMessage("bank.validation.duplicate") },
const schema = z.object({
name: z
.string()
.trim()
.refine(
value =>
value.length > 0 && value.length <= MAX_CATEGORY_NAME_LENGTH,
{ error: validationMessage("bank.validation.category") },
)
.refine(
value =>
!categories.data?.some(
c =>
c.id !== editing?.id
&& c.name.toLocaleLowerCase("ru-RU")
=== value.toLocaleLowerCase("ru-RU"),
),
}),
),
defaultValues: { name: "" },
{ error: validationMessage("bank.validation.duplicate") },
),
});
const pending = !opened || methods.formState.isSubmitting;
const methods = useForm<CategoryValues>({ defaultValues: { name: "" }, resolver: zodResolver(schema) });
const isPending = !opened || methods.formState.isSubmitting;
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("bank.categories")}
closeOnClickOutside={!pending}
closeOnEscape={!pending}
closeButtonProps={{ "aria-label": t("bank.close"), disabled: pending }}
>
<FormProvider {...methods}>
<form
@@ -107,8 +105,8 @@ const CategoryManager: FC<CategoryManagerProps> = (props) => {
try {
if (editing)
await mutation.updateCategory.mutateAsync({
id: editing.id,
body: { name: values.name, version: editing.version },
id: editing.id,
});
else {
const result = await mutation.createCategory.mutateAsync({
@@ -116,24 +114,25 @@ const CategoryManager: FC<CategoryManagerProps> = (props) => {
});
if (result) props.onCreated?.(result.id);
}
editSession.current++;
editSessionRef.current++;
setEditing(undefined);
setLatest(undefined);
methods.reset({ name: "" });
} catch (failure) {
setError(failure);
}
catch (error_) {
setError(error_);
}
})(event);
}}
>
<fieldset disabled={pending} className={classes.fieldset}>
<fieldset className={classes.fieldset} disabled={isPending}>
<Stack>
{categories.data?.map((category) => (
{categories.data?.map(category => (
<Group justify="space-between" key={category.id}>
<Group gap="xs">
<BankOperationIcon
kind="expense"
categoryName={category.name}
kind="expense"
/>
<Text>{category.name}</Text>
{category.isDefault && (
@@ -141,14 +140,14 @@ const CategoryManager: FC<CategoryManagerProps> = (props) => {
)}
</Group>
<Button
variant="subtle"
onClick={() => {
editSession.current++;
editSessionRef.current++;
setEditing(category);
methods.reset({ name: category.name });
setError(undefined);
setLatest(undefined);
}}
variant="subtle"
>
{t("bank.rename")}
</Button>
@@ -159,35 +158,35 @@ const CategoryManager: FC<CategoryManagerProps> = (props) => {
)}
{categories.isError && (
<BankFeedback
loadingError
error={categories.error}
cached={Boolean(categories.data)}
error={categories.error}
loadingError
retry={() => void categories.refetch()}
/>
)}
<CategoryFields />
{error ? (
{!!(error) && ((
<>
<BankFeedback error={error} />
{editing && (
<Button
variant="default"
onClick={async () => {
const session = editSession.current;
const session = editSessionRef.current;
const response = await categories.refetch();
if (session !== editSession.current) return;
if (session !== editSessionRef.current) return;
if (response.error) setError(response.error);
else
setLatest(
response.data?.find((c) => c.id === editing.id),
response.data?.find(c => c.id === editing.id),
);
}}
variant="default"
>
{t("bank.reloadRecord")}
</Button>
)}
</>
) : null}
))}
{latest && latest.id === editing?.id && (
<>
<Text>{t("bank.versionHint", { details: latest.name })}</Text>
@@ -203,24 +202,24 @@ const CategoryManager: FC<CategoryManagerProps> = (props) => {
</>
)}
<Group>
<Button type="submit" loading={pending}>
<Button loading={isPending} type="submit">
{t(editing ? "bank.save" : "bank.addCategory")}
</Button>
{editing && (
<Button
variant="default"
onClick={() => {
editSession.current++;
editSessionRef.current++;
setEditing(undefined);
setLatest(undefined);
methods.reset({ name: "" });
setError(undefined);
}}
variant="default"
>
{t("bank.cancel")}
</Button>
)}
<Button variant="default" onClick={() => setOpened(false)}>
<Button onClick={() => setOpened(false)} variant="default">
{t("bank.close")}
</Button>
</Group>