fix(workshop): make the takedown action work inside the desktop webview

The action used window.prompt, which returns null in the Tauri webview, so the handler aborted before calling the API. Takedown and lift now use an in-app dialog with a required reason, inline errors and a loading state.
This commit is contained in:
HorizonCode committed 2026-11-03 19:07:22 +01:00
1 parent 4b039c9b79
commit 918f8651dd
1 file changed
+134 -19
@@ -157,6 +157,10 @@
let exportingReports = $state(false); let exportingReports = $state(false);
let exportingEvents = $state(false); let exportingEvents = $state(false);
let quarantineId = $state<string | null>(null); let quarantineId = $state<string | null>(null);
let quarantineTarget = $state<CommunityTheme | null>(null);
let quarantineAction = $state<'take_down' | 'lift'>('take_down');
let quarantineReason = $state('');
let quarantineError = $state<string | null>(null);
let tagEditorTarget = $state<CommunityTheme | null>(null); let tagEditorTarget = $state<CommunityTheme | null>(null);
let tagEditorTags = $state<string[]>([]); let tagEditorTags = $state<string[]>([]);
@@ -1055,38 +1059,46 @@
} }
} }
async function handleToggleQuarantine(theme: CommunityTheme) { function handleToggleQuarantine(theme: CommunityTheme) {
if (!canModerate || quarantineId) return; if (!canModerate || quarantineId) return;
const takingDown = !theme.isQuarantined; quarantineTarget = theme;
const reason = takingDown quarantineAction = theme.isQuarantined ? 'lift' : 'take_down';
? window.prompt( quarantineReason = '';
`Take down "${theme.name}"?\n\nReason shown to the owner and stored in the audit log:`, quarantineError = null;
theme.quarantineReason || '' }
)
: window.prompt( function closeQuarantineDialog() {
`Lift the takedown on "${theme.name}"?\n\nOptional note for the audit log:`, if (quarantineId) return;
'' quarantineTarget = null;
); quarantineReason = '';
if (reason === null) return; quarantineError = null;
if (takingDown && reason.trim().length < 3) { }
sileo.error({ title: 'A takedown reason is required' });
async function confirmQuarantine() {
const theme = quarantineTarget;
if (!theme || quarantineId) return;
const takingDown = quarantineAction === 'take_down';
const reason = quarantineReason.trim();
if (takingDown && reason.length < 3) {
quarantineError = 'Please provide a reason of at least 3 characters.';
return; return;
} }
quarantineId = theme.id; quarantineId = theme.id;
quarantineError = null;
try { try {
await setCommunityThemeQuarantine(theme.id, takingDown, reason.trim() || undefined); await setCommunityThemeQuarantine(theme.id, takingDown, reason || undefined);
sileo.success({ sileo.success({
title: takingDown ? 'Theme taken down' : 'Takedown lifted', title: takingDown ? 'Theme taken down' : 'Takedown lifted',
description: `"${theme.name}" was updated.`, description: `"${theme.name}" was updated.`,
}); });
quarantineTarget = null;
quarantineReason = '';
await loadWorkshopThemes(); await loadWorkshopThemes();
if (showModerationPanel) await loadModerationPanel(moderationPage); if (showModerationPanel) await loadModerationPanel(moderationPage);
} catch (err) { } catch (err) {
sileo.error({ quarantineError = err instanceof Error ? err.message : String(err);
title: 'Could not update takedown',
description: err instanceof Error ? err.message : String(err),
});
} finally { } finally {
quarantineId = null; quarantineId = null;
} }
@@ -3152,6 +3164,109 @@
</div> </div>
{/if} {/if}
{#if quarantineTarget}
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
<button
type="button"
class="absolute inset-0 cursor-default bg-black/75 backdrop-blur-sm"
aria-label="Close takedown dialog"
onclick={closeQuarantineDialog}
></button>
<div
class="relative w-full max-w-md overflow-hidden rounded-xl border border-theme-800 bg-theme-950 shadow-2xl"
role="dialog"
aria-modal="true"
aria-labelledby="quarantine-theme-title"
>
<div class="flex items-start justify-between gap-3 border-b border-theme-800/70 p-4">
<div>
<h2
id="quarantine-theme-title"
class="flex items-center gap-2 text-sm font-semibold text-white"
>
<Ban class="size-4 text-red-300" />
{quarantineAction === 'take_down' ? 'Take down theme' : 'Lift takedown'}
</h2>
<p class="mt-1 pr-2 text-[11px] leading-relaxed text-muted-foreground">
{#if quarantineAction === 'take_down'}
"{quarantineTarget.name}" will disappear from public search, downloads, previews and
updates until the takedown is lifted. The owner and open reporters are notified.
{:else}
"{quarantineTarget.name}" will become publicly visible again unless it is hidden or
still awaiting review.
{/if}
</p>
</div>
<Button
variant="ghost"
size="icon"
class="size-7 shrink-0 text-muted-foreground hover:text-white"
onclick={closeQuarantineDialog}
disabled={Boolean(quarantineId)}
aria-label="Close takedown dialog"
>
<X class="size-4" />
</Button>
</div>
<div class="space-y-3 p-4">
<label class="block space-y-1.5">
<span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase"
>{quarantineAction === 'take_down' ? 'Reason' : 'Note (optional)'}</span
>
<textarea
class="min-h-24 w-full resize-y rounded-md border border-theme-800 bg-theme-900/60 p-2.5 text-[11px] leading-relaxed text-white outline-none placeholder:text-muted-foreground/60 focus:border-primary/50"
rows="4"
maxlength="500"
placeholder={quarantineAction === 'take_down'
? 'Explain why the theme is being taken down...'
: 'Optional note for the audit log...'}
bind:value={quarantineReason}
disabled={Boolean(quarantineId)}
oninput={() => (quarantineError = null)}
></textarea>
<span class="block text-right text-[9px] text-muted-foreground">
{quarantineReason.trim().length}/500
</span>
</label>
{#if quarantineError}
<p
class="rounded-lg border border-red-900/40 bg-red-950/20 p-2.5 text-[11px] text-red-300"
>
{quarantineError}
</p>
{/if}
</div>
<div class="flex items-center justify-end gap-2 border-t border-theme-800/70 p-3">
<Button
variant="ghost"
size="sm"
class="h-8 text-xs"
onclick={closeQuarantineDialog}
disabled={Boolean(quarantineId)}
>
Cancel
</Button>
<Button
variant={quarantineAction === 'take_down' ? 'destructive' : 'default'}
size="sm"
class="h-8 gap-1 text-xs"
onclick={() => void confirmQuarantine()}
disabled={Boolean(quarantineId)}
>
{#if quarantineId}
<LoaderCircle class="size-3 animate-spin" /> Updating...
{:else}
<Ban class="size-3" />
{quarantineAction === 'take_down' ? 'Take down theme' : 'Lift takedown'}
{/if}
</Button>
</div>
</div>
</div>
{/if}
{#if reportTarget} {#if reportTarget}
<div class="fixed inset-0 z-50 flex items-center justify-center p-4"> <div class="fixed inset-0 z-50 flex items-center justify-center p-4">
<button <button