chore: add typed ESLint checks #18

Refs WealthLedger/main#18. Keep Biome; add no Prettier dependencies.
This commit is contained in:
2026-09-28 16:36:40 +03:00
committed by ksv741
parent 95fdc8759e
commit 57630102e4
38 changed files with 2914 additions and 155 deletions
@@ -48,10 +48,15 @@ function useAccountEventsPageViewModel(
normalized.delete("to");
setSearchParams(normalized, { replace: true });
}, [validUrlPeriod, searchParams, setSearchParams]);
useEffect(() => {
const [previousPeriod, setPreviousPeriod] = useState([
appliedFrom,
appliedTo,
]);
if (previousPeriod[0] !== appliedFrom || previousPeriod[1] !== appliedTo) {
setPreviousPeriod([appliedFrom, appliedTo]);
setDraftFrom(appliedFrom);
setDraftTo(appliedTo);
}, [appliedFrom, appliedTo]);
}
const events = useBrokerageEvents(accountId, {
...(appliedFrom ? { from: appliedFrom } : {}),
...(appliedTo ? { to: appliedTo } : {}),
@@ -462,11 +462,9 @@ describe("AccountIncomePage", () => {
http.get("*/api/v1/brokerage-accounts/account-1/events", eventsRequest),
);
renderPage();
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "Пассивный доход" }),
).toBeInTheDocument(),
);
expect(
await screen.findByRole("heading", { name: "Пассивный доход" }),
).toBeInTheDocument();
expect(
screen.getByText("Налоговые операции не включены в этот результат."),
).toBeInTheDocument();
@@ -542,11 +540,9 @@ describe("AccountIncomePage", () => {
),
);
renderPage();
await waitFor(() =>
expect(
screen.getByRole("button", { name: "Повторить" }),
).toBeInTheDocument(),
);
expect(
await screen.findByRole("button", { name: "Повторить" }),
).toBeInTheDocument();
});
});
@@ -43,18 +43,22 @@ type IncomeDisplayModel = {
type AccountIncomePageViewModel =
| { state: "loading" }
| { state: "error"; retry: () => void; period: IncomePeriod }
| {
state: "error";
retry: () => undefined | Promise<unknown>;
period: IncomePeriod;
}
| {
state: "loaded";
analytics: IncomeAnalytics;
display: IncomeDisplayModel;
formRevision: number;
formRevision: string;
sourceError?: boolean;
lastSuccessfulAt?: string | null;
partial?: boolean;
refreshing: boolean;
refreshError: boolean;
retry: () => void;
retry: () => undefined | Promise<unknown>;
form: ReturnType<typeof useForm<IncomeForm>>;
onReset: () => void;
onSubmit: (values: IncomeForm) => void;
@@ -102,7 +106,7 @@ const useAccountIncomePageViewModel = (
defaultValues: draftPeriod(period),
resolver: zodResolver(incomeFormSchema),
});
const [formRevision, setFormRevision] = useState(0);
const [resetRevision, setResetRevision] = useState(0);
const analytics = useIncomeAnalytics(accountId, period);
const sync = useScreenSync(
{ screen: "income", accountId, from: period.from, to: period.to },
@@ -114,7 +118,6 @@ const useAccountIncomePageViewModel = (
}, [period, requested, setSearchParams]);
useEffect(() => {
form.reset(draftPeriod(period));
setFormRevision((revision) => revision + 1);
}, [form, period]);
const onSubmit = (values: IncomeForm) => {
@@ -126,7 +129,7 @@ const useAccountIncomePageViewModel = (
const next = defaultPeriod();
form.reset(draftPeriod(next));
// Reset the open calendar along with the form.
setFormRevision((revision) => revision + 1);
setResetRevision((revision) => revision + 1);
setSearchParams(next, { replace: true });
};
if (analytics.isLoading || !analytics.data) {
@@ -138,16 +141,14 @@ const useAccountIncomePageViewModel = (
if (!display)
return {
state: "error",
retry: () => {
void analytics.observe();
},
retry: () => analytics.observe(),
period,
};
return {
state: "loaded",
analytics: analytics.data,
display,
formRevision,
formRevision: `${period.from}:${period.to}:${resetRevision}`,
sourceError: sync.sourceError,
lastSuccessfulAt: analytics.meta?.lastSuccessfulAt,
partial: analytics.meta?.availability !== "complete",
@@ -22,8 +22,14 @@ const IncomePeriodForm: FC<IncomePeriodFormProps> = (props) => {
control,
formState: { isSubmitting },
} = useFormContext<IncomeForm>();
const fromField = useController({ control, name: "from" });
const toField = useController({ control, name: "to" });
const {
field: { ref: fromRef, onChange: onFromChange, onBlur: onFromBlur },
fieldState: { error: fromError },
} = useController({ control, name: "from" });
const {
field: { onChange: onToChange, onBlur: onToBlur },
fieldState: { error: toError },
} = useController({ control, name: "to" });
const [from, to] = useWatch({ control, name: ["from", "to"] });
const today = moscowDate(new Date());
const start = parseDraftDate(from);
@@ -44,23 +50,20 @@ const IncomePeriodForm: FC<IncomePeriodFormProps> = (props) => {
allowSingleDateInRange
clearable
clearButtonProps={{ "aria-label": t("ledger.clearPeriod") }}
ref={fromField.field.ref}
ref={fromRef}
label={t("income.period")}
valueFormat="DD.MM.YYYY"
value={[start, end]}
onChange={([from, to]) => {
fromField.field.onChange(from ? formatDraftDate(from) : "");
toField.field.onChange(to ? formatDraftDate(to) : "");
onFromChange(from ? formatDraftDate(from) : "");
onToChange(to ? formatDraftDate(to) : "");
}}
onBlur={() => {
fromField.field.onBlur();
toField.field.onBlur();
onFromBlur();
onToBlur();
}}
maxDate={today}
error={
fromField.fieldState.error?.message ??
toField.fieldState.error?.message
}
error={fromError?.message ?? toError?.message}
disabled={isSubmitting}
popoverProps={{
returnFocus: true,
@@ -72,11 +72,9 @@ const useAccountOperationsPageVm = (
}>({ scopeKey, cursors: [] });
const cursors = pagination.scopeKey === scopeKey ? pagination.cursors : [];
const cursor = cursors.at(-1);
useEffect(() => {
setPagination((current) =>
current.scopeKey === scopeKey ? current : { scopeKey, cursors: [] },
);
}, [scopeKey]);
if (pagination.scopeKey !== scopeKey) {
setPagination({ scopeKey, cursors: [] });
}
useEffect(() => {
if (validUrlPeriod) return;
const normalized = new URLSearchParams(searchParams);
@@ -84,7 +82,9 @@ const useAccountOperationsPageVm = (
normalized.delete("to");
setSearchParams(normalized, { replace: true });
}, [validUrlPeriod, searchParams, setSearchParams]);
useEffect(() => {
const [previousSearch, setPreviousSearch] = useState(searchParams);
if (previousSearch !== searchParams) {
setPreviousSearch(searchParams);
setDraftFrom(appliedFrom);
setDraftTo(appliedTo);
setDraftCategories(
@@ -92,7 +92,7 @@ const useAccountOperationsPageVm = (
(searchParams.get("categories") ?? "").split(",").includes(category),
),
);
}, [appliedFrom, appliedTo, searchParams]);
}
const firstPageQuery = {
from: dateStart(appliedFrom),
to: dateEnd(appliedTo),
@@ -58,7 +58,7 @@ type AccountPageRecentEvents = BrokerageEventsPreviewState;
type AccountPageIncomeSummary =
| { state: "loading" | "partial" | "empty"; to: string }
| { state: "error"; to: string; retry: () => void }
| { state: "error"; to: string; retry: () => undefined | Promise<unknown> }
| {
state: "loaded";
to: string;
@@ -75,7 +75,7 @@ type AccountPageContent = {
accountId?: string;
refreshing: boolean;
refreshError: boolean;
retry: () => void;
retry: () => undefined | Promise<unknown>;
recentOperations: AccountPageRecentOperations;
recentEvents: AccountPageRecentEvents;
incomeSummary: AccountPageIncomeSummary;
@@ -93,7 +93,7 @@ type AccountPageEmptyViewModel = AccountPageContent & {
type AccountPageViewModel =
| { state: "loading" }
| { state: "error"; code?: string; retry: () => void }
| { state: "error"; code?: string; retry: () => undefined | Promise<unknown> }
| AccountPageEmptyViewModel
| AccountPageLoadedViewModel;
@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useForm, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import {
@@ -157,12 +157,12 @@ const useOperationFormVm = (props: OperationFormProps) => {
description: props.operation?.description ?? "",
},
});
const kind = methods.watch("kind");
const kind = useWatch({ control: methods.control, name: "kind" });
const expenses = useBankExpenses(
kind === "refund" ? props.card.id : undefined,
{ page: sourcePage },
);
const source = methods.watch("cardId");
const source = useWatch({ control: methods.control, name: "cardId" });
const pending = !opened || methods.formState.isSubmitting;
const options = props.cards
.filter((c) => !c.archivedAt)
+1 -1
View File
@@ -17,7 +17,7 @@ const SettingsPage: FC = () => {
<Button
onClick={async () => {
await logout();
navigate("/login", { replace: true });
await navigate("/login", { replace: true });
}}
>
{t("auth.logout")}