mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
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.
This commit is contained in:
1 parent
8ed6eb5793
commit
46350b6461
3 files changed
+68
-49
No files matched your search
@@ -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() {
|
||||
<BackupPassActivityNotice activity={data.scheduleActivity} />
|
||||
) : null}
|
||||
|
||||
<BackupUploadCard
|
||||
pending={upload.isPending || uploadInto.isPending}
|
||||
fileInputRef={fileInputRef}
|
||||
onFileChange={handleFileChange}
|
||||
/>
|
||||
{/* Mounted per refused file, so every file starts from a clean form. */}
|
||||
{orphan ? (
|
||||
<BackupTargetDialog
|
||||
file={orphan}
|
||||
pending={uploadInto.isPending}
|
||||
error={
|
||||
uploadInto.error instanceof ReproofCancelledError
|
||||
? null
|
||||
: uploadInto.error
|
||||
}
|
||||
onSubmit={(target) => uploadInto.mutate(target)}
|
||||
onClose={closeOrphan}
|
||||
/>
|
||||
) : null}
|
||||
<BackupUpload recentProof={recentProof} />
|
||||
|
||||
{visibleRestoreJobs(restoreJobs, dismissedRestores).map((job) => (
|
||||
<RestoreJobStatus
|
||||
|
||||
@@ -1,9 +1,58 @@
|
||||
import { Loader2, Upload } from "lucide-react";
|
||||
import type { ChangeEvent, RefObject } from "react";
|
||||
|
||||
import {
|
||||
ReproofCancelledError,
|
||||
type useRecentProof,
|
||||
} from "@/components/settings/security-section/use-recent-proof";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
import { BackupTargetDialog } from "./backup-target-dialog";
|
||||
import { useBackupUpload } from "./use-backup-upload";
|
||||
|
||||
/**
|
||||
* The upload card, and for a file whose account is not on this server, the
|
||||
* dialog that sends it again into an account the admin picks. The dialog is
|
||||
* mounted per refused file, so every file starts from a clean form.
|
||||
*/
|
||||
export function BackupUpload({
|
||||
recentProof,
|
||||
}: {
|
||||
recentProof: ReturnType<typeof useRecentProof>;
|
||||
}) {
|
||||
const {
|
||||
upload,
|
||||
fileInputRef,
|
||||
handleFileChange,
|
||||
orphan,
|
||||
uploadInto,
|
||||
closeOrphan,
|
||||
} = useBackupUpload(recentProof);
|
||||
return (
|
||||
<>
|
||||
<BackupUploadCard
|
||||
pending={upload.isPending || uploadInto.isPending}
|
||||
fileInputRef={fileInputRef}
|
||||
onFileChange={handleFileChange}
|
||||
/>
|
||||
{orphan ? (
|
||||
<BackupTargetDialog
|
||||
file={orphan}
|
||||
pending={uploadInto.isPending}
|
||||
error={
|
||||
uploadInto.error instanceof ReproofCancelledError
|
||||
? null
|
||||
: uploadInto.error
|
||||
}
|
||||
onSubmit={(target) => 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
|
||||
|
||||
@@ -141,19 +141,24 @@ export function useBackupUpload(
|
||||
// Upload state — single-file flow, drives the file input + button.
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [orphan, setOrphan] = useState<File | null>(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.
|
||||
|
||||
Reference in new issue
Block a user