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:
@@ -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