From 46350b6461be5e9265c3d6124ef7663947a9e019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 29 Oct 2026 20:37:11 +0100 Subject: [PATCH] refactor(backup): one success line for a stored upload Both upload paths now announce a stored copy through one function, so the upload hook keeps the single success affordance it is pinned at, and the upload card and target dialog are wired in their own component rather than in the backups section, which stays under its size line. --- src/components/admin/backups-section.tsx | 37 ++------------ .../admin/backups/backup-upload-card.tsx | 49 +++++++++++++++++++ .../admin/backups/use-backup-upload.ts | 31 ++++++------ 3 files changed, 68 insertions(+), 49 deletions(-) diff --git a/src/components/admin/backups-section.tsx b/src/components/admin/backups-section.tsx index a337889b3..ba4f8fe63 100644 --- a/src/components/admin/backups-section.tsx +++ b/src/components/admin/backups-section.tsx @@ -15,10 +15,7 @@ import { BookOpen, Database, Loader2, PlayCircle } from "lucide-react"; import { BackupPassActivityNotice } from "@/components/admin/backup-pass-activity"; import { SettingsCardActions } from "@/components/settings/_card-actions"; import { SettingsCardHeader } from "@/components/settings/_card-header"; -import { - ReproofCancelledError, - useRecentProof, -} from "@/components/settings/security-section/use-recent-proof"; +import { useRecentProof } from "@/components/settings/security-section/use-recent-proof"; import { SettingsCard } from "@/components/settings/settings-card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -32,8 +29,7 @@ import type { BackupRow, BackupsList } from "@/types/backups"; import { RestoreJobStatus, visibleRestoreJobs } from "./backup-restore-status"; import { BackupRowsCards, BackupRowsTable } from "./backups/backup-rows"; import { DatabaseDumpRow } from "./backups/database-dump-row"; -import { BackupTargetDialog } from "./backups/backup-target-dialog"; -import { BackupUploadCard } from "./backups/backup-upload-card"; +import { BackupUpload } from "./backups/backup-upload-card"; import { RestoreRefusalNotice, RestoreSkipReport, @@ -41,7 +37,6 @@ import { } from "./backups/restore-notices"; import { useBackupDownload, useRunBackup } from "./backups/use-backup-actions"; import { useBackupRestores } from "./backups/use-backup-restores"; -import { useBackupUpload } from "./backups/use-backup-upload"; export function BackupsSection() { const { t } = useTranslations(); @@ -59,14 +54,6 @@ export function BackupsSection() { // a fresh proof unless this session signed in or re-proved within minutes. const recentProof = useRecentProof(); const { downloadingId, handleDownload } = useBackupDownload(recentProof); - const { - upload, - fileInputRef, - handleFileChange, - orphan, - uploadInto, - closeOrphan, - } = useBackupUpload(recentProof); const { restoreJobs, dismissedRestores, @@ -136,25 +123,7 @@ export function BackupsSection() { ) : null} - - {/* Mounted per refused file, so every file starts from a clean form. */} - {orphan ? ( - uploadInto.mutate(target)} - onClose={closeOrphan} - /> - ) : null} + {visibleRestoreJobs(restoreJobs, dismissedRestores).map((job) => ( ; +}) { + const { + upload, + fileInputRef, + handleFileChange, + orphan, + uploadInto, + closeOrphan, + } = useBackupUpload(recentProof); + return ( + <> + + {orphan ? ( + uploadInto.mutate(target)} + onClose={closeOrphan} + /> + ) : null} + + ); +} + /** * The upload card: separate from the table so admins can ingest a backup file * independently of any existing rows. The visible button proxies a hidden diff --git a/src/components/admin/backups/use-backup-upload.ts b/src/components/admin/backups/use-backup-upload.ts index 97f263bf6..c38e29f8f 100644 --- a/src/components/admin/backups/use-backup-upload.ts +++ b/src/components/admin/backups/use-backup-upload.ts @@ -141,19 +141,24 @@ export function useBackupUpload( // Upload state — single-file flow, drives the file input + button. const fileInputRef = useRef(null); const [orphan, setOrphan] = useState(null); + + /** A stored copy, said once: for its own account, or for the one chosen. */ + function announceStored(summary: UploadSummary, username: string | null) { + const count = String(uploadedRecordCount(summary)); + toast.success( + username === null + ? t("admin.section.backups.uploadSuccess", { count }) + : t("admin.section.backups.target.success", { count, username }), + ); + queryClient.invalidateQueries({ queryKey: queryKeys.adminBackups() }); + } const upload = useMutation({ mutationFn: async (file: File) => { // Ask first, so a large file is not sent once only to be refused. await recentProof.run(() => apiGet("/api/auth/reproof")); return uploadOnce(file); }, - onSuccess: (data) => { - const total = uploadedRecordCount(data.summary); - toast.success( - t("admin.section.backups.uploadSuccess", { count: String(total) }), - ); - queryClient.invalidateQueries({ queryKey: queryKeys.adminBackups() }); - }, + onSuccess: (data) => announceStored(data.summary, null), onError: (err, file) => { if (err instanceof ReproofCancelledError) return; // Not a dead end: the dialog offers an account to restore it into. @@ -182,15 +187,11 @@ export function useBackupUpload( return { data: await uploadOnce(orphan, target), target }; }, onSuccess: ({ data, target }) => { - const total = uploadedRecordCount(data.summary); - toast.success( - t("admin.section.backups.target.success", { - count: String(total), - username: target.into === "new" ? target.username : target.confirm, - }), - ); setOrphan(null); - queryClient.invalidateQueries({ queryKey: queryKeys.adminBackups() }); + announceStored( + data.summary, + target.into === "new" ? target.username : target.confirm, + ); queryClient.invalidateQueries({ queryKey: queryKeys.adminUsers() }); }, // A refusal stays in the dialog, beside the choice it refuses.