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.