From 611e542ea71102e1744a8678cd82404fb81c6629 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 4 Oct 2026 05:01:29 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D1=8C=20=D0=BA=D0=B0=D1=82=D0=B0=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=20=D0=B1=D1=8D=D0=BA=D0=B0=D0=BF=D0=BE=D0=B2=20=D0=B8=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=BD=D1=83=D1=82=D1=8C=20=D1=84=D0=BE=D0=BA?= =?UTF-8?q?=D1=83=D1=81=20=D0=B4=D0=B5=D0=B9=D1=81=D1=82=D0=B2=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/backups/page.tsx | 26 ++++++++-- components/backups/backup-confirm-dialogs.tsx | 8 +++- components/data-grids/backups-data-grid.tsx | 45 ++++++++++-------- docs/ux-ui-performance.md | 5 +- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 8 ++++ docs/ux-ui-stage-12.md | 8 ++++ docs/ux-ui/screens/large-backups-320.png | Bin 0 -> 44614 bytes tests/e2e/backups-dialog-layout.spec.ts | 6 +-- tests/e2e/backups-layout.spec.ts | 8 ++-- tests/e2e/large-backups.spec.ts | 42 ++++++++++++++++ 11 files changed, 123 insertions(+), 35 deletions(-) create mode 100644 docs/ux-ui/screens/large-backups-320.png create mode 100644 tests/e2e/large-backups.spec.ts diff --git a/app/(main)/backups/page.tsx b/app/(main)/backups/page.tsx index ab324af5..d49454c0 100644 --- a/app/(main)/backups/page.tsx +++ b/app/(main)/backups/page.tsx @@ -74,6 +74,24 @@ function BackupsContent() { const canWrite = isLive && can("mm:backups:write") const canReadServers = can("mm:servers:read") const [resources, setResources] = useState>(() => ({ backups: unknownResource(), servers: unknownResource(), schedule: unknownResource(), storage: unknownResource() })) + const dialogTrigger = useRef<{ element: HTMLElement; rowId: string | null; label: string | null } | null>(null) + const refreshTrigger = useRef(null) + const captureDialogFocus = () => { + const element = document.activeElement + dialogTrigger.current = element instanceof HTMLElement + ? { element, rowId: element.closest("[data-row-id]")?.getAttribute("data-row-id") ?? null, label: element.getAttribute("aria-label") } + : null + } + const returnDialogFocus = () => { + const trigger = dialogTrigger.current + if (trigger?.element.isConnected) return trigger.element + // Cell renderers can remount when operation state changes; resolve the same row/action. + if (trigger?.rowId && trigger.label) { + const replacement = document.querySelector(`[data-row-id="${CSS.escape(trigger.rowId)}"] button[aria-label="${CSS.escape(trigger.label)}"]`) + if (replacement) return replacement + } + return refreshTrigger.current + } const reader = useRef(null) const revision = useRef(0) const alive = useRef(true) @@ -403,7 +421,7 @@ function BackupsContent() { {!canRead &&

Нет права mm:backups:read

} {isLive && !canReadServers &&

Каталог устройств недоступен без mm:servers:read.

} {isLive && canRead &&
- + {loading ? "Обновление данных…" : resources.backups.receivedAt ? "Получено: " + new Date(resources.backups.receivedAt).toLocaleString("ru-RU") : "Резервные копии пока неизвестны"}
} {isLive && (Object.keys(resources) as Resource[]).filter(resource => resources[resource].error).map(resource => @@ -493,8 +511,8 @@ function BackupsContent() { filters={advancedFilters} onFiltersChange={setAdvancedFilters} onDownload={handleDownload} - onRestore={backup => { setOpError(null); setRestoreTarget(backup) }} - onDelete={backup => { setOpError(null); setDeleteTarget(backup) }} + onRestore={backup => { captureDialogFocus(); setOpError(null); setRestoreTarget(backup) }} + onDelete={backup => { captureDialogFocus(); setOpError(null); setDeleteTarget(backup) }} onCreate={() => openCreate(false)} canCreate={canCreate} canWrite={canWrite && !opBusy && !scheduleSaveBusy && !backupJobId} @@ -578,6 +596,7 @@ function BackupsContent() { /> setDeleteTarget(null)} /> void onCancel: () => void + returnFocus?: () => HTMLElement | null }) { return ( { if (!v && !busy) onCancel() }}> - + @@ -61,6 +63,7 @@ export function BackupRestoreDialog({ error, onConfirm, onCancel, + returnFocus, }: { backup: Backup | null busy: boolean @@ -68,10 +71,11 @@ export function BackupRestoreDialog({ error?: string | null onConfirm: () => void onCancel: () => void + returnFocus?: () => HTMLElement | null }) { return ( { if (!v && !busy) onCancel() }}> - + diff --git a/components/data-grids/backups-data-grid.tsx b/components/data-grids/backups-data-grid.tsx index ce58128f..b5d38680 100644 --- a/components/data-grids/backups-data-grid.tsx +++ b/components/data-grids/backups-data-grid.tsx @@ -1,8 +1,8 @@ "use client" -import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" +import { DataGridContainer, DataGridScrollArea, DataGridTable, dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" -import { useMemo, type ReactNode } from "react" +import { useEffect, useMemo, type ReactNode } from "react" import { type ColumnDef, useTable, @@ -10,9 +10,9 @@ import { import type { Backup } from "@/lib/data" import { Button } from "@/components/ui/button" import { Badge } from "@/components/reui/badge" -import { IconTile } from "@/components/reui/icon-tile" import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell" import { + DEFAULT_DATA_GRID_LAYOUT, DATA_GRID_CELL_PAD, DATA_GRID_CELL_PAD_FIRST, DATA_GRID_CELL_PAD_LAST, @@ -20,7 +20,6 @@ import { import { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sort-header" import { EmptyState } from "@/components/empty-state" import { - CloudIcon, DownloadIcon, HardDriveIcon, RefreshCwIcon, @@ -63,20 +62,12 @@ function BackupsDataGrid({ { id: "filename", accessorKey: "filename", - size: 280, + size: 110, header: ({ column }) => , cell: ({ row }) => { const b = row.original - const inS3 = b.storage === "s3" || b.storage === "both" return (
-
{b.filename} {b.uploadError ? ( @@ -190,12 +181,12 @@ function BackupsDataGrid({ cell: ({ row }) => { const b = row.original return ( -
+