diff --git a/src/renderer/src/features/installations/pages/ManageInstallationBackups.tsx b/src/renderer/src/features/installations/pages/ManageInstallationBackups.tsx
index 7f975f66..6662bcd8 100644
--- a/src/renderer/src/features/installations/pages/ManageInstallationBackups.tsx
+++ b/src/renderer/src/features/installations/pages/ManageInstallationBackups.tsx
@@ -96,6 +96,7 @@ function ManageInstallationBackups(): JSX.Element {
if (!installation) return addNotification(t("features.installations.noInstallationFound"), "error")
if (!backup) return addNotification(t("features.backups.cantDeleteWhileinUse"), "error")
+ configDispatch({ type: CONFIG_ACTIONS.EDIT_INSTALLATION_BACKUP, payload: { id: installation.id, backupId: backup.id, updates: { _deleting: true } } })
const result = await deleteInstallationBackup(createBackupDeletionPorts(), { backup: toBackupSnapshot(backup) })
if (result.ok) {
@@ -103,6 +104,9 @@ function ManageInstallationBackups(): JSX.Element {
return addNotification(t("features.backups.backupDeletedSuccesfully"), "success")
}
+ if (result.reason !== "backup-in-use") {
+ configDispatch({ type: CONFIG_ACTIONS.EDIT_INSTALLATION_BACKUP, payload: { id: installation.id, backupId: backup.id, updates: { _deleting: false } } })
+ }
const { messageKey, logged } = describeBackupDeletionFailure(result.reason)
if (logged) {
@@ -153,10 +157,10 @@ function ManageInstallationBackups(): JSX.Element {
-
setBackupToRestore(backup)}>
+ setBackupToRestore(backup)} disabled={backup._deleting || backup._restoring}>
- setBackupToDelete(backup)} title={t("generic.delete")} className="p-1">
+ setBackupToDelete(backup)} title={t("generic.delete")} className="p-1" disabled={backup._deleting || backup._restoring}>
openPathInExplorer(backup.path, { parentOfFile: true })} title={`${t("generic.openOnFileExplorer")} ยท ${backup.path}`} className="p-1">
diff --git a/tests/renderer-dom/installationsRestoreBackup.test.tsx b/tests/renderer-dom/installationsRestoreBackup.test.tsx
index 31f2da09..27c182dd 100644
--- a/tests/renderer-dom/installationsRestoreBackup.test.tsx
+++ b/tests/renderer-dom/installationsRestoreBackup.test.tsx
@@ -96,4 +96,95 @@ describe("ManageInstallationBackups", () => {
// The confirm dialog closes as part of the same click.
await waitFor(() => expect(screen.queryByText("Are you sure you want to restore this Backup?")).toBeNull())
})
+
+ it("deletes the backup archive after the delete confirmation", async () => {
+ const user = userEvent.setup()
+ const deletePath = vi.fn(async () => true)
+ installMockWindowApi({
+ configManager: { getConfig: vi.fn(async () => createMockConfig({ installations: [anInstallationWithBackup()] })) },
+ pathsManager: {
+ deletePath,
+ extractOnPath: vi.fn(async () => true)
+ }
+ })
+
+ renderManageBackups("install-a")
+
+ await user.click(await screen.findByTitle("Delete"))
+ await screen.findByText("Are you sure you want to delete this Backup?")
+ await user.click(screen.getAllByTitle("Delete")[1]!)
+
+ await waitFor(() => expect(deletePath).toHaveBeenCalledTimes(1))
+ expect(deletePath.mock.calls[0]?.[0]).toBe("/backups/a/backup-1.zip")
+ await waitFor(() => expect(screen.queryByTitle("Delete")).toBeNull())
+ })
+
+ it("disables the row trash button while a deletion is in flight", async () => {
+ const user = userEvent.setup()
+ let resolveDelete: (result: boolean) => void = () => {}
+ const deletePath = vi.fn(
+ () =>
+ new Promise((resolve) => {
+ resolveDelete = resolve
+ })
+ )
+ installMockWindowApi({
+ configManager: { getConfig: vi.fn(async () => createMockConfig({ installations: [anInstallationWithBackup()] })) },
+ pathsManager: {
+ deletePath,
+ extractOnPath: vi.fn(async () => true)
+ }
+ })
+
+ renderManageBackups("install-a")
+
+ // Acquire the row's trash button. Before deletion it must be enabled.
+ const trashButton = await screen.findByTitle("Delete")
+ expect(trashButton.hasAttribute("disabled")).toBe(false)
+
+ // Start the first deletion through the confirm dialog.
+ await user.click(trashButton)
+ await screen.findByText("Are you sure you want to delete this Backup?")
+ const confirmButtons = screen.getAllByTitle("Delete")
+ await user.click(confirmButtons[confirmButtons.length - 1]!)
+
+ await waitFor(() => expect(deletePath).toHaveBeenCalledTimes(1))
+
+ // While the deletion is in flight, the row trash button must be disabled.
+ // This assertion fails on dev (where _deleting is never set) and passes on
+ // this branch (where configDispatch sets _deleting: true before the call).
+ await waitFor(() => {
+ const buttons = screen.getAllByRole("button")
+ const disabledButtons = buttons.filter((btn) => btn.hasAttribute("disabled"))
+ expect(disabledButtons.length).toBeGreaterThan(0)
+ })
+
+ resolveDelete(true)
+ await waitFor(() => expect(deletePath).toHaveBeenCalledTimes(1))
+ })
+
+ it("clears the deleting state when the archive deletion fails", async () => {
+ const user = userEvent.setup()
+ const deletePath = vi.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true)
+ installMockWindowApi({
+ configManager: { getConfig: vi.fn(async () => createMockConfig({ installations: [anInstallationWithBackup()] })) },
+ pathsManager: {
+ deletePath,
+ extractOnPath: vi.fn(async () => true)
+ }
+ })
+
+ renderManageBackups("install-a")
+
+ await user.click(await screen.findByTitle("Delete"))
+ await screen.findByText("Are you sure you want to delete this Backup?")
+ await user.click(screen.getAllByTitle("Delete")[1]!)
+ await waitFor(() => expect(deletePath).toHaveBeenCalledTimes(1))
+ await waitFor(() => expect(screen.getByTitle("Delete")).toBeTruthy())
+
+ await user.click(screen.getByTitle("Delete"))
+ await screen.findByText("Are you sure you want to delete this Backup?")
+ await user.click(screen.getAllByTitle("Delete")[1]!)
+ await waitFor(() => expect(deletePath).toHaveBeenCalledTimes(2))
+ })
})