feat: replace paired date inputs with range pickers
This commit is contained in:
@@ -55,7 +55,7 @@ describe("Upcoming event date filters", () => {
|
||||
);
|
||||
render(
|
||||
<QueryClientProvider client={createTestQueryClient()}>
|
||||
<MantineProvider>
|
||||
<MantineProvider env="test">
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
`/accounts/${accountId}/events?from=2099-02-01&to=2099-02-28`,
|
||||
@@ -72,31 +72,37 @@ describe("Upcoming event date filters", () => {
|
||||
</MantineProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
const from = await screen.findByLabelText("С");
|
||||
const to = screen.getByLabelText("По");
|
||||
expect(from).toHaveValue("01.02.2099");
|
||||
expect(to).toHaveValue("28.02.2099");
|
||||
const from = await screen.findByLabelText("Период");
|
||||
expect(from).toHaveTextContent("01.02.2099 – 28.02.2099");
|
||||
expect(screen.getByRole("button", { name: "Применить" })).toBeDisabled();
|
||||
expect(searches).toEqual(["?from=2099-02-01&to=2099-02-28"]);
|
||||
fireEvent.change(from, { target: { value: "10.02.2099" } });
|
||||
fireEvent.blur(from);
|
||||
fireEvent.click(from);
|
||||
fireEvent.click(
|
||||
await screen.findByRole("button", { name: /10 February 2099/i }),
|
||||
);
|
||||
fireEvent.click(
|
||||
await screen.findByRole("button", { name: /28 February 2099/i }),
|
||||
);
|
||||
expect(searches).toHaveLength(1);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Применить" }));
|
||||
await waitFor(() =>
|
||||
expect(searches.at(-1)).toBe("?from=2099-02-10&to=2099-02-28"),
|
||||
);
|
||||
expect(await screen.findByLabelText("С")).toHaveValue("10.02.2099");
|
||||
expect(await screen.findByLabelText("Период")).toHaveTextContent(
|
||||
"10.02.2099 – 28.02.2099",
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "History back" }));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByLabelText("С")).toHaveValue("01.02.2099"),
|
||||
expect(screen.getByLabelText("Период")).toHaveTextContent(
|
||||
"01.02.2099 – 28.02.2099",
|
||||
),
|
||||
);
|
||||
expect(screen.getByLabelText("URL")).toHaveTextContent(
|
||||
"?from=2099-02-01&to=2099-02-28",
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Сбросить" }));
|
||||
await waitFor(() => expect(searches.at(-1)).toBe(""));
|
||||
expect(await screen.findByLabelText("С")).toHaveValue("");
|
||||
expect(screen.getByLabelText("По")).toHaveValue("");
|
||||
expect(await screen.findByLabelText("Период")).toHaveTextContent("");
|
||||
expect(screen.getByRole("button", { name: "Применить" })).toBeDisabled();
|
||||
expect(
|
||||
screen.getByText("За выбранный период событий не найдено"),
|
||||
|
||||
@@ -7,22 +7,13 @@ import {
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { DateInput } from "@mantine/dates";
|
||||
import dayjs from "dayjs";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import { DatePickerInput } from "@mantine/dates";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BrokerageEventCard } from "@/entities/brokerage-event";
|
||||
import type { AccountEventsPageViewModel } from "../AccountEventsPage.vm";
|
||||
import { AccountEventsPageEmpty } from "./AccountEventsPageEmpty.view";
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
const parseDate = (value: string) => {
|
||||
const date = dayjs(value, "DD.MM.YYYY", true);
|
||||
return date.isValid() ? date.format("YYYY-MM-DD") : null;
|
||||
};
|
||||
|
||||
type AccountEventsPageLoadedProps = Extract<
|
||||
AccountEventsPageViewModel,
|
||||
{ state: "loaded" }
|
||||
@@ -46,25 +37,22 @@ const AccountEventsPageLoaded: FC<AccountEventsPageLoadedProps> = (props) => {
|
||||
) : null}
|
||||
<Text c="dimmed">{t("events.hint")}</Text>
|
||||
<Group align="end">
|
||||
<DateInput
|
||||
label={t("ledger.from")}
|
||||
value={props.draftFrom}
|
||||
onChange={props.onDraftFromChange}
|
||||
maxDate={props.draftTo ?? undefined}
|
||||
<DatePickerInput
|
||||
type="range"
|
||||
allowSingleDateInRange
|
||||
clearable
|
||||
clearButtonProps={{ "aria-label": t("ledger.clearPeriod") }}
|
||||
label={t("ledger.period")}
|
||||
value={[props.draftFrom, props.draftTo]}
|
||||
onChange={([from, to]) => {
|
||||
props.onDraftFromChange(from);
|
||||
props.onDraftToChange(to);
|
||||
}}
|
||||
valueFormat="DD.MM.YYYY"
|
||||
dateParser={parseDate}
|
||||
allowDeselect
|
||||
popoverProps={{ middlewares: { shift: { crossAxis: true } } }}
|
||||
/>
|
||||
<DateInput
|
||||
label={t("ledger.to")}
|
||||
value={props.draftTo}
|
||||
onChange={props.onDraftToChange}
|
||||
minDate={props.draftFrom ?? undefined}
|
||||
valueFormat="DD.MM.YYYY"
|
||||
dateParser={parseDate}
|
||||
allowDeselect
|
||||
popoverProps={{ middlewares: { shift: { crossAxis: true } } }}
|
||||
popoverProps={{
|
||||
returnFocus: true,
|
||||
middlewares: { shift: { crossAxis: true } },
|
||||
}}
|
||||
/>
|
||||
</Group>
|
||||
<Group>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
.fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: var(--mantine-spacing-sm);
|
||||
}
|
||||
|
||||
|
||||
@@ -523,12 +523,10 @@ const LocationProbe: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
function inputDate(label: string, value: string) {
|
||||
const field = screen.getByLabelText(label);
|
||||
fireEvent.focus(field);
|
||||
fireEvent.input(field, { target: { value } });
|
||||
fireEvent.blur(field);
|
||||
return field;
|
||||
function selectPeriod() {
|
||||
fireEvent.click(screen.getByLabelText("Период"));
|
||||
fireEvent.click(screen.getByRole("button", { name: /^2 августа 2026$/i }));
|
||||
fireEvent.click(screen.getByRole("button", { name: /^27 августа 2026$/i }));
|
||||
}
|
||||
|
||||
function respondWithRequestedPeriod(requests: string[]) {
|
||||
@@ -557,9 +555,10 @@ describe("income period and snapshot boundary", () => {
|
||||
const requests: string[] = [];
|
||||
respondWithRequestedPeriod(requests);
|
||||
renderPage();
|
||||
expect(await screen.findByLabelText("С")).toHaveValue("01.08.2026");
|
||||
expect(screen.getByLabelText("По")).toHaveValue("27.08.2026");
|
||||
inputDate("С", "02.08.2026");
|
||||
expect(await screen.findByLabelText("Период")).toHaveTextContent(
|
||||
"01.08.2026 – 27.08.2026",
|
||||
);
|
||||
selectPeriod();
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(
|
||||
screen.queryByText("2026-08-01 — 2026-08-27"),
|
||||
@@ -573,66 +572,37 @@ describe("income period and snapshot boundary", () => {
|
||||
.closest("form") as HTMLFormElement,
|
||||
);
|
||||
await waitFor(() => expect(requests).toHaveLength(2));
|
||||
expect(await screen.findByLabelText("С")).toHaveValue("02.08.2026");
|
||||
expect(await screen.findByLabelText("Период")).toHaveTextContent(
|
||||
"02.08.2026 – 27.08.2026",
|
||||
);
|
||||
expect(screen.getByTestId("location")).toHaveTextContent(
|
||||
"from=2026-08-02&to=2026-08-27",
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Back" }));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByLabelText("С")).toHaveValue("01.08.2026"),
|
||||
expect(screen.getByLabelText("Период")).toHaveTextContent(
|
||||
"01.08.2026 – 27.08.2026",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["С", "", "Введите существующую дату"],
|
||||
["С", "31.02.2026", "Введите существующую дату"],
|
||||
["По", "29.02.2026", "Введите существующую дату"],
|
||||
["По", "2026-08-02", "Введите существующую дату"],
|
||||
["С", "01.01.2099", "Дата не может быть позже"],
|
||||
["По", "01.01.2099", "Дата не может быть позже"],
|
||||
["По", "31.07.2026", "Укажите корректный период"],
|
||||
])(
|
||||
"rejects visible %s=%s without replacing data or URL",
|
||||
async (label, value, error) => {
|
||||
const requests: string[] = [];
|
||||
respondWithRequestedPeriod(requests);
|
||||
renderPage();
|
||||
await screen.findByLabelText("С");
|
||||
const field = inputDate(label, value);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Применить" }));
|
||||
expect(await screen.findByText(new RegExp(error))).toBeInTheDocument();
|
||||
expect(field).toHaveValue(value);
|
||||
expect(field).toHaveAttribute("aria-invalid", "true");
|
||||
await waitFor(() => expect(field).toHaveFocus());
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(screen.getByTestId("location")).toHaveTextContent(
|
||||
"from=2026-08-01&to=2026-08-27",
|
||||
);
|
||||
expect(
|
||||
screen.queryByText("2026-08-01 — 2026-08-27"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("region", { name: "Выплаты" })).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
it("replaces invalid raw text with a calendar choice and closes the calendar with Escape", async () => {
|
||||
respondWithRequestedPeriod([]);
|
||||
it("rejects an incomplete range without replacing the URL and allows a single day", async () => {
|
||||
const requests: string[] = [];
|
||||
respondWithRequestedPeriod(requests);
|
||||
renderPage();
|
||||
const from = await screen.findByLabelText("С");
|
||||
act(() => from.focus());
|
||||
fireEvent.input(from, { target: { value: "invalid" } });
|
||||
// The selected month stays available even when the visible draft is invalid.
|
||||
const day = await screen.findByRole("button", {
|
||||
name: /^2 августа 2026$/i,
|
||||
});
|
||||
fireEvent.click(day);
|
||||
expect(from).toHaveValue("02.08.2026");
|
||||
act(() => from.focus());
|
||||
fireEvent.keyDown(from, { key: "Escape" });
|
||||
const period = await screen.findByLabelText("Период");
|
||||
fireEvent.click(period);
|
||||
fireEvent.click(screen.getByRole("button", { name: /^2 августа 2026$/i }));
|
||||
fireEvent.keyDown(period, { key: "Escape" });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Применить" }));
|
||||
expect(await screen.findByText(/Введите существующую дату/)).toBeVisible();
|
||||
expect(requests).toHaveLength(1);
|
||||
if (period.getAttribute("aria-expanded") !== "true")
|
||||
fireEvent.click(period);
|
||||
fireEvent.click(screen.getByRole("button", { name: /^2 августа 2026$/i }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Применить" }));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /^2 августа 2026$/i }),
|
||||
).not.toBeInTheDocument(),
|
||||
expect(requests.at(-1)).toBe("?from=2026-08-02&to=2026-08-02"),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -642,19 +612,23 @@ describe("income period and snapshot boundary", () => {
|
||||
const requests: string[] = [];
|
||||
respondWithRequestedPeriod(requests);
|
||||
renderPage("");
|
||||
expect(await screen.findByLabelText("С")).toHaveValue("01.09.2026");
|
||||
expect(screen.getByLabelText("По")).toHaveValue("01.09.2026");
|
||||
inputDate("С", "wrong");
|
||||
expect(await screen.findByLabelText("Период")).toHaveTextContent(
|
||||
"01.09.2026 – 01.09.2026",
|
||||
);
|
||||
fireEvent.click(screen.getByLabelText("Период"));
|
||||
fireEvent.click(screen.getByRole("button", { name: /^1 сентября 2026$/i }));
|
||||
fireEvent.keyDown(screen.getByLabelText("Период"), { key: "Escape" });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Применить" }));
|
||||
await screen.findByText(/Введите существующую дату/);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Сбросить" }));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByLabelText("С")).toHaveValue("01.09.2026"),
|
||||
expect(screen.getByLabelText("Период")).toHaveTextContent(
|
||||
"01.09.2026 – 01.09.2026",
|
||||
),
|
||||
);
|
||||
expect(
|
||||
screen.queryByText(/Введите существующую дату/),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByLabelText("По")).toHaveValue("01.09.2026");
|
||||
expect(screen.getByTestId("location")).toHaveTextContent(
|
||||
"from=2026-09-01&to=2026-09-01",
|
||||
);
|
||||
@@ -664,8 +638,8 @@ describe("income period and snapshot boundary", () => {
|
||||
it("keeps valid data and draft during refetch, warns on failure and retries", async () => {
|
||||
respondWithRequestedPeriod([]);
|
||||
const { queryClient } = renderPage();
|
||||
await screen.findByLabelText("С");
|
||||
inputDate("С", "02.08.2026");
|
||||
await screen.findByLabelText("Период");
|
||||
selectPeriod();
|
||||
let release!: () => void;
|
||||
const pending = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
@@ -686,7 +660,9 @@ describe("income period and snapshot boundary", () => {
|
||||
await screen.findByRole("status", { name: "Обновляем пассивный доход" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("region", { name: "Выплаты" })).toBeVisible();
|
||||
expect(screen.getByLabelText("С")).toHaveValue("02.08.2026");
|
||||
expect(screen.getByLabelText("Период")).toHaveTextContent(
|
||||
"02.08.2026 – 27.08.2026",
|
||||
);
|
||||
release();
|
||||
expect(
|
||||
await screen.findByText(/Показан последний загруженный результат/),
|
||||
@@ -699,13 +675,15 @@ describe("income period and snapshot boundary", () => {
|
||||
screen.queryByText(/Показан последний загруженный результат/),
|
||||
).not.toBeInTheDocument(),
|
||||
);
|
||||
expect(screen.getByLabelText("С")).toHaveValue("02.08.2026");
|
||||
expect(screen.getByLabelText("Период")).toHaveTextContent(
|
||||
"02.08.2026 – 27.08.2026",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows busy skeleton for a new uncached period without relabeling the previous result", async () => {
|
||||
respondWithRequestedPeriod([]);
|
||||
renderPage();
|
||||
await screen.findByLabelText("С");
|
||||
await screen.findByLabelText("Период");
|
||||
let release!: () => void;
|
||||
const pending = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
@@ -722,7 +700,7 @@ describe("income period and snapshot boundary", () => {
|
||||
},
|
||||
),
|
||||
);
|
||||
inputDate("С", "02.08.2026");
|
||||
selectPeriod();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Применить" }));
|
||||
expect(
|
||||
await screen.findByRole("status", { name: "Загрузка" }),
|
||||
@@ -734,7 +712,9 @@ describe("income period and snapshot boundary", () => {
|
||||
screen.queryByText("2026-08-02 — 2026-08-27"),
|
||||
).not.toBeInTheDocument();
|
||||
release();
|
||||
expect(await screen.findByLabelText("С")).toHaveValue("02.08.2026");
|
||||
expect(await screen.findByLabelText("Период")).toHaveTextContent(
|
||||
"02.08.2026 – 27.08.2026",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows one flat payment card per entry in server order without currency or instrument groups", () => {
|
||||
|
||||
@@ -99,7 +99,7 @@ const useAccountIncomePageViewModel = (
|
||||
const onReset = () => {
|
||||
const next = defaultPeriod();
|
||||
form.reset(draftPeriod(next));
|
||||
// DateInput retains raw text while its calendar is open; reset that UI state too.
|
||||
// Reset the open calendar along with the form.
|
||||
setFormRevision((revision) => revision + 1);
|
||||
setSearchParams(next, { replace: true });
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Paper, Stack, Text } from "@mantine/core";
|
||||
import { DateInput } from "@mantine/dates";
|
||||
import { DatePickerInput } from "@mantine/dates";
|
||||
import type { FC, FormEventHandler } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { useController, useFormContext, useWatch } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classes from "../AccountIncomePage.module.css";
|
||||
import {
|
||||
@@ -22,6 +22,8 @@ const IncomePeriodForm: FC<IncomePeriodFormProps> = (props) => {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
} = useFormContext<IncomeForm>();
|
||||
const fromField = useController({ control, name: "from" });
|
||||
const toField = useController({ control, name: "to" });
|
||||
const [from, to] = useWatch({ control, name: ["from", "to"] });
|
||||
const today = moscowDate(new Date());
|
||||
const start = parseDraftDate(from);
|
||||
@@ -37,39 +39,34 @@ const IncomePeriodForm: FC<IncomePeriodFormProps> = (props) => {
|
||||
<Stack gap="sm">
|
||||
<Text fw={700}>{t("income.period")}</Text>
|
||||
<div className={classes.fields}>
|
||||
{(["from", "to"] as const).map((name) => (
|
||||
<Controller
|
||||
key={name}
|
||||
name={name}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<DateInput
|
||||
ref={field.ref}
|
||||
label={t(`income.${name}`)}
|
||||
placeholder={t("income.datePlaceholder")}
|
||||
valueFormat="DD.MM.YYYY"
|
||||
value={parseDraftDate(field.value)}
|
||||
dateParser={parseDraftDate}
|
||||
fixOnBlur={false}
|
||||
popoverProps={{ middlewares: { shift: { crossAxis: true } } }}
|
||||
onInput={(event) => field.onChange(event.currentTarget.value)}
|
||||
onChange={(value) =>
|
||||
field.onChange(value ? formatDraftDate(value) : "")
|
||||
}
|
||||
onBlur={(event) => {
|
||||
field.onChange(event.currentTarget.value);
|
||||
field.onBlur();
|
||||
}}
|
||||
minDate={
|
||||
name === "to" && start && start <= today ? start : undefined
|
||||
}
|
||||
maxDate={name === "from" && end && end <= today ? end : today}
|
||||
error={fieldState.error?.message}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<DatePickerInput
|
||||
type="range"
|
||||
allowSingleDateInRange
|
||||
clearable
|
||||
clearButtonProps={{ "aria-label": t("ledger.clearPeriod") }}
|
||||
ref={fromField.field.ref}
|
||||
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) : "");
|
||||
}}
|
||||
onBlur={() => {
|
||||
fromField.field.onBlur();
|
||||
toField.field.onBlur();
|
||||
}}
|
||||
maxDate={today}
|
||||
error={
|
||||
fromField.fieldState.error?.message ??
|
||||
toField.fieldState.error?.message
|
||||
}
|
||||
disabled={isSubmitting}
|
||||
popoverProps={{
|
||||
returnFocus: true,
|
||||
middlewares: { shift: { crossAxis: true } },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.actions}>
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
Stack,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { DateInput } from "@mantine/dates";
|
||||
import { DatePickerInput } from "@mantine/dates";
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
@@ -25,21 +25,6 @@ type AccountOperationsPageLoadedProps = Extract<
|
||||
|
||||
const operationCategorySet = new Set<string>(operationCategories);
|
||||
|
||||
function toDateValue(value: string | null): Date | null {
|
||||
if (!value) return null;
|
||||
const [year, month, day] = value.split("-").map(Number);
|
||||
return new Date(year, month - 1, day);
|
||||
}
|
||||
|
||||
function fromDateValue(value: Date | string | null): string | null {
|
||||
if (!value) return null;
|
||||
if (typeof value === "string") return value;
|
||||
const year = value.getFullYear();
|
||||
const month = String(value.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(value.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function fromMultiSelectValue(value: string[]) {
|
||||
return value.filter((item): item is OperationCategory =>
|
||||
operationCategorySet.has(item),
|
||||
@@ -87,19 +72,22 @@ const AccountOperationsPageLoaded: FC<AccountOperationsPageLoadedProps> = (
|
||||
</Alert>
|
||||
) : null}
|
||||
<Group align="end">
|
||||
<DateInput
|
||||
label={t("ledger.from")}
|
||||
value={toDateValue(draftFrom)}
|
||||
onChange={(value) => onDraftFromChange(fromDateValue(value))}
|
||||
maxDate={toDateValue(draftTo) ?? undefined}
|
||||
valueFormat="DD.MM.YYYY"
|
||||
/>
|
||||
<DateInput
|
||||
label={t("ledger.to")}
|
||||
value={toDateValue(draftTo)}
|
||||
onChange={(value) => onDraftToChange(fromDateValue(value))}
|
||||
minDate={toDateValue(draftFrom) ?? undefined}
|
||||
<DatePickerInput
|
||||
type="range"
|
||||
allowSingleDateInRange
|
||||
clearable
|
||||
clearButtonProps={{ "aria-label": t("ledger.clearPeriod") }}
|
||||
label={t("ledger.period")}
|
||||
value={[draftFrom, draftTo]}
|
||||
onChange={([from, to]) => {
|
||||
onDraftFromChange(from);
|
||||
onDraftToChange(to);
|
||||
}}
|
||||
valueFormat="DD.MM.YYYY"
|
||||
popoverProps={{
|
||||
returnFocus: true,
|
||||
middlewares: { shift: { crossAxis: true } },
|
||||
}}
|
||||
/>
|
||||
</Group>
|
||||
<MultiSelect
|
||||
@@ -111,6 +99,7 @@ const AccountOperationsPageLoaded: FC<AccountOperationsPageLoadedProps> = (
|
||||
onDraftCategoriesChange(fromMultiSelectValue(value))
|
||||
}
|
||||
clearable
|
||||
clearButtonProps={{ "aria-label": t("ledger.clearPeriod") }}
|
||||
/>
|
||||
<Group>
|
||||
<Button onClick={onApply} disabled={!canApply}>
|
||||
|
||||
@@ -529,6 +529,7 @@ const ru = {
|
||||
share: "Доля: {{value}}",
|
||||
},
|
||||
ledger: {
|
||||
clearPeriod: "Очистить период",
|
||||
occurredAt: "{{date}} UTC",
|
||||
quantity: "{{value}} шт.",
|
||||
price: "Цена: {{value}}",
|
||||
@@ -550,6 +551,7 @@ const ru = {
|
||||
recentLoading: "Загрузка последних событий",
|
||||
recentError: "Не удалось загрузить последние события.",
|
||||
recentEmpty: "Последних событий пока нет.",
|
||||
period: "Период",
|
||||
from: "С",
|
||||
to: "По",
|
||||
categories: "Категории",
|
||||
|
||||
Reference in New Issue
Block a user