From 520ad5c4f1dcce0494b8022fd122b5f3ca4dff4f Mon Sep 17 00:00:00 2001 From: HorizonCode Date: Tue, 3 Nov 2026 18:10:34 +0100 Subject: [PATCH] feat(workshop): confirm trash restore and permanent deletion Add confirmation dialogs for restoring and permanently deleting trashed themes, and wire the permanent-delete API call. --- src/lib/communityThemes.ts | 19 ++ .../theme/CommunityWorkshopView.svelte | 190 +++++++++++++++++- 2 files changed, 200 insertions(+), 9 deletions(-) diff --git a/src/lib/communityThemes.ts b/src/lib/communityThemes.ts index 76e4791..9facaea 100644 --- a/src/lib/communityThemes.ts +++ b/src/lib/communityThemes.ts @@ -345,6 +345,25 @@ export async function deleteCommunityTheme(themeId: string): Promise { } } +export async function permanentlyDeleteCommunityTheme(themeId: string): Promise { + const baseUrl = await getThemesServiceUrl(); + const headers = await getAuthHeaders(); + + if (!headers['X-Username'] || !headers['X-Password']) { + throw new Error('Please log in to permanently delete community themes.'); + } + + const res = await fetch(`${baseUrl}/v1/themes/${encodeURIComponent(themeId)}/permanent`, { + method: 'DELETE', + headers, + }); + + if (!res.ok) { + const errorJson = (await res.json().catch(() => ({}))) as { error?: string }; + throw new Error(errorJson.error || `Failed to permanently delete theme (HTTP ${res.status})`); + } +} + export async function reportCommunityTheme( themeId: string, reason: string, diff --git a/src/lib/components/theme/CommunityWorkshopView.svelte b/src/lib/components/theme/CommunityWorkshopView.svelte index f3249e3..daad70f 100644 --- a/src/lib/components/theme/CommunityWorkshopView.svelte +++ b/src/lib/components/theme/CommunityWorkshopView.svelte @@ -8,6 +8,7 @@ downloadAndInstallCommunityTheme, forkCommunityTheme, deleteCommunityTheme, + permanentlyDeleteCommunityTheme, restoreCommunityTheme, setCommunityThemeVisibility, setCommunityThemeQuarantine, @@ -28,6 +29,7 @@ type ThemeReportQuery, type ThemeReportStatus, } from '$lib/communityThemes'; + import * as AlertDialog from '$lib/components/ui/alert-dialog'; import Badge from '$lib/components/ui/badge/badge.svelte'; import { Button } from '$lib/components/ui/button'; import { Checkbox } from '$lib/components/ui/checkbox'; @@ -121,6 +123,10 @@ let deletingId = $state(null); let moderatingId = $state(null); let restoringId = $state(null); + let purgingId = $state(null); + let restoreTarget = $state(null); + let purgeTarget = $state(null); + let trashActionError = $state(null); let canModerate = $state(false); let showModerationPanel = $state(false); @@ -199,6 +205,7 @@ restore: 'Visibility restored', delete: 'Moved to trash', restore_deleted: 'Restored from trash', + delete_permanent: 'Permanently deleted', report: 'Reported', report_resolved: 'Report resolved', report_dismissed: 'Report dismissed', @@ -216,6 +223,7 @@ { id: 'report_dismissed', label: 'Report dismissed' }, { id: 'delete', label: 'Trashed' }, { id: 'restore_deleted', label: 'Restored from trash' }, + { id: 'delete_permanent', label: 'Permanently deleted' }, ]; const localThemeMap = $derived.by(() => { @@ -780,30 +788,82 @@ } } - async function handleRestoreTheme(theme: CommunityTheme) { - if (restoringId || downloadingId || forkingId || deletingId || moderatingId) return; - if (!window.confirm(`Restore "${theme.name}" to the workshop?`)) return; + function openRestoreDialog(theme: CommunityTheme) { + if (restoringId || purgingId || downloadingId || forkingId || deletingId || moderatingId) + return; + purgeTarget = null; + trashActionError = null; + restoreTarget = theme; + } + + function openPermanentDeleteDialog(theme: CommunityTheme) { + if (restoringId || purgingId || downloadingId || forkingId || deletingId || moderatingId) + return; + restoreTarget = null; + trashActionError = null; + purgeTarget = theme; + } + + function closeRestoreDialog() { + if (restoringId) return; + restoreTarget = null; + trashActionError = null; + } + + function closePermanentDeleteDialog() { + if (purgingId) return; + purgeTarget = null; + trashActionError = null; + } + + async function handleRestoreTheme() { + const theme = restoreTarget; + if (!theme || restoringId || purgingId) return; restoringId = theme.id; + trashActionError = null; try { await restoreCommunityTheme(theme.id); themes = themes.filter((item) => item.id !== theme.id); totalThemes = Math.max(0, totalThemes - 1); + restoreTarget = null; sileo.success({ title: 'Theme restored', description: `"${theme.name}" is back in the workshop.`, }); void loadWorkshopThemes(); + void loadWorkshopTags(); } catch (err) { - sileo.error({ - title: 'Could not restore theme', - description: err instanceof Error ? err.message : String(err), - }); + trashActionError = err instanceof Error ? err.message : String(err); } finally { restoringId = null; } } + async function handlePermanentDeleteTheme() { + const theme = purgeTarget; + if (!theme || restoringId || purgingId) return; + + purgingId = theme.id; + trashActionError = null; + try { + await permanentlyDeleteCommunityTheme(theme.id); + themes = themes.filter((item) => item.id !== theme.id); + totalThemes = Math.max(0, totalThemes - 1); + purgeTarget = null; + sileo.success({ + title: 'Theme permanently deleted', + description: `"${theme.name}" and its server files were removed.`, + }); + void loadWorkshopThemes(); + void loadWorkshopTags(); + } catch (err) { + trashActionError = err instanceof Error ? err.message : String(err); + } finally { + purgingId = null; + } + } + function openReportDialog(theme: CommunityTheme) { if (theme.deletedAt || theme.isHidden) return; if (!$currentUser) { @@ -1866,8 +1926,8 @@ variant="secondary" size="sm" class="h-7 gap-1 text-xs" - onclick={() => handleRestoreTheme(theme)} - disabled={restoringId === theme.id} + onclick={() => openRestoreDialog(theme)} + disabled={restoringId === theme.id || purgingId === theme.id} > {#if restoringId === theme.id} @@ -1877,6 +1937,21 @@ Restore {/if} + {:else} {/if} + { + if (!open) closeRestoreDialog(); + }} + > + + + + Restore theme? + + + Restore {restoreTarget?.name} to the workshop? + It will return with its existing visibility and moderation state. + + + {#if trashActionError} +

+ {trashActionError} +

+ {/if} + + + + +
+
+ + { + if (!open) closePermanentDeleteDialog(); + }} + > + + + + Permanently delete theme? + + + Permanently delete {purgeTarget?.name}? + This removes the theme package, preview, likes, downloads and reports from the server. + This cannot be undone. Installed copies on computers are not affected. + + + {#if trashActionError} +

+ {trashActionError} +

+ {/if} + + + + +
+