refactor: establish local frontend workspace boundaries
Frontend security / security (push) Successful in 9s
Frontend verify / verify (push) Successful in 5m27s
Frontend security / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 5m24s
Conventional commits / commits (pull_request) Failing after 26s

OpenSpec change: evolve-frontend-stack
This commit is contained in:
2026-09-22 22:11:02 +03:00
parent 5b2c6bb7cb
commit f876411a37
101 changed files with 1487 additions and 666 deletions
+141 -29
View File
@@ -1,15 +1,20 @@
import { Select, Stack, Text, Textarea, TextInput } from "@mantine/core";
import {
Button,
Group,
Modal,
Select,
Stack,
Text,
Textarea,
TextInput,
} from "@mantine/core";
import { DateInput } from "@mantine/dates";
import type { FC } from "react";
import type { FC, FormEventHandler, ReactNode } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
type BankOperation,
bankDateInputValue,
bankDateSchema,
bankToday,
} from "@/entities/bank-card";
import type { BankOperation } from "@/entities/bank-card";
import { formatMoney } from "@/shared/lib";
import classes from "./OperationForm.module.css";
import type { OperationValues } from "./OperationForm.types";
type Choice = { value: string; label: string };
@@ -21,6 +26,10 @@ type OperationFieldsProps = {
targets: Choice[];
categories: Choice[];
defaultCategory: string;
kinds: Choice[];
dateInputValue: (value: string) => string;
validDate: (value: string) => boolean;
maxDate: () => string;
};
const OperationFields: FC<OperationFieldsProps> = (props) => {
const { t } = useTranslation();
@@ -39,9 +48,7 @@ const OperationFields: FC<OperationFieldsProps> = (props) => {
<Select
label={t("bank.kind")}
required
data={(["expense", "topup", "transfer", "refund"] as const).map(
(value) => ({ value, label: t(`bank.kinds.${value}`) }),
)}
data={props.kinds}
{...field}
disabled={isSubmitting || props.editing}
onChange={(value) => {
@@ -99,24 +106,22 @@ const OperationFields: FC<OperationFieldsProps> = (props) => {
label={t("bank.date")}
required
valueFormat="DD.MM.YYYY"
value={
bankDateSchema.safeParse(field.value).success ? field.value : null
}
value={props.validDate(field.value) ? field.value : null}
dateParser={(value) =>
bankDateSchema.safeParse(bankDateInputValue(value)).success
? bankDateInputValue(value)
props.validDate(props.dateInputValue(value))
? props.dateInputValue(value)
: null
}
fixOnBlur={false}
onInput={(event) =>
field.onChange(bankDateInputValue(event.currentTarget.value))
field.onChange(props.dateInputValue(event.currentTarget.value))
}
onChange={(value) => field.onChange(value ?? "")}
onBlur={(event) => {
field.onChange(bankDateInputValue(event.currentTarget.value));
field.onChange(props.dateInputValue(event.currentTarget.value));
field.onBlur();
}}
maxDate={bankToday()}
maxDate={props.maxDate()}
popoverProps={{
withinPortal: false,
middlewares: { shift: { crossAxis: true } },
@@ -153,7 +158,6 @@ const OperationFields: FC<OperationFieldsProps> = (props) => {
type RefundFieldsProps = {
editing: boolean;
expenses: Choice[];
selected?: Choice;
onSelect: (id: string) => void;
limit: BankOperation["availableRefund"] | undefined;
};
@@ -172,14 +176,7 @@ const RefundFields: FC<RefundFieldsProps> = (props) => {
<Select
label={t("bank.expense")}
required
data={
props.selected &&
!props.expenses.some(
(item) => item.value === props.selected?.value,
)
? [props.selected, ...props.expenses]
: props.expenses
}
data={props.expenses}
{...field}
onChange={(value) => {
field.onChange(value ?? "");
@@ -200,4 +197,119 @@ const RefundFields: FC<RefundFieldsProps> = (props) => {
);
};
export { OperationFields, RefundFields };
type OperationFormViewProps = {
opened: boolean;
pending: boolean;
categoriesOpen: boolean;
editing: boolean;
onClose: () => void;
onExitTransitionEnd: () => void;
onCancel: () => void;
onSubmit: FormEventHandler<HTMLFormElement>;
fields: OperationFieldsProps;
refund: RefundFieldsProps;
loadingExpenses: boolean;
emptyExpenses: boolean;
sourcePage: number;
previousDisabled: boolean;
nextDisabled: boolean;
onPrevious: () => void;
onNext: () => void;
onAddCategory: () => void;
onReload?: () => void;
latest?: { version: number; details: string; onAdopt: () => void };
saveDisabled: boolean;
refundFeedback?: ReactNode;
categoriesFeedback?: ReactNode;
feedback?: ReactNode;
};
const OperationFormView: FC<OperationFormViewProps> = (props) => {
const { t } = useTranslation();
return (
<Modal
opened={props.opened}
onExitTransitionEnd={props.onExitTransitionEnd}
onClose={props.onClose}
title={t(props.editing ? "bank.editOperation" : "bank.addOperation")}
closeOnEscape={!props.pending && !props.categoriesOpen}
closeOnClickOutside={!props.pending && !props.categoriesOpen}
closeButtonProps={{
"aria-label": t("bank.close"),
disabled: props.pending,
}}
>
<form onSubmit={props.onSubmit}>
<fieldset disabled={props.pending} className={classes.fieldset}>
<Stack>
<OperationFields {...props.fields} />
{props.fields.kind === "refund" && (
<>
<RefundFields {...props.refund} />
{props.loadingExpenses && (
<Text role="status">{t("bank.loadingHistory")}</Text>
)}
{props.emptyExpenses && <Text>{t("bank.noPurchases")}</Text>}
{!props.editing && (
<Group>
<Button
variant="default"
disabled={props.previousDisabled}
onClick={props.onPrevious}
>
{t("bank.previous")}
</Button>
<Text>{t("bank.page", { page: props.sourcePage })}</Text>
<Button
variant="default"
disabled={props.nextDisabled}
onClick={props.onNext}
>
{t("bank.next")}
</Button>
</Group>
)}
{props.refundFeedback}
</>
)}
{props.fields.kind === "expense" && (
<Button variant="subtle" onClick={props.onAddCategory}>
{t("bank.addCategory")}
</Button>
)}
{props.categoriesFeedback}
{props.feedback}
{props.onReload && (
<Button variant="default" onClick={props.onReload}>
{t("bank.reloadRecord")}
</Button>
)}
{props.latest && (
<>
<Text>
{t("bank.versionHint", { details: props.latest.details })}
</Text>
<Button variant="default" onClick={props.latest.onAdopt}>
{t("bank.adoptVersion", { version: props.latest.version })}
</Button>
</>
)}
<Group>
<Button
type="submit"
loading={props.pending}
disabled={props.saveDisabled}
>
{t("bank.save")}
</Button>
<Button variant="default" onClick={props.onCancel}>
{t("bank.cancel")}
</Button>
</Group>
</Stack>
</fieldset>
</form>
</Modal>
);
};
export { OperationFormView };