From 8efee74b5c583a1b012f46753af72a0bd0e13515 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sat, 15 Aug 2026 02:56:44 +0200 Subject: [PATCH] fix(ui): place cleanup opt-out below dialog actions Extract the cleanup confirmation into a reusable production component, align its action group to the right, and place the opt-out directly below the buttons. Add an isolated visual fixture so the exact dialog can be reviewed without touching real queue data. --- src/renderer/App.tsx | 35 ++++++++---------- src/renderer/styles.css | 10 +++++ .../downloads/DeleteConfirmationDialog.tsx | 37 +++++++++++++++++++ tests/app-shell.test.tsx | 10 +++++ tests/visual/main.tsx | 20 ++++++++++ 5 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 src/renderer/views/downloads/DeleteConfirmationDialog.tsx diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 3908234..fa36604 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -96,6 +96,7 @@ import { } from "./views/statistics/StatisticsView"; import { buildDownloadsViewModel, formatRemainingDownloadBytes, getDownloadQueueTotalBytes, getDownloadSpeedBps, getPendingDownloadItemCount, getRemainingDownloadBytes, type DownloadDisplayMode, type DownloadSidebarFilter } from "./views/downloads/downloads-model"; import { downloadColumnDefinitions, type DownloadSortColumn } from "./views/downloads/DownloadsTable"; +import { DeleteConfirmationDialog } from "./views/downloads/DeleteConfirmationDialog"; import { beginDownloadColumnDrag, clearDownloadColumnDrag, settleDownloadColumnDrag, updateDownloadColumnDrag, type DownloadColumnDragSession } from "./views/downloads/column-drag"; import { DownloadsContent, @@ -5911,25 +5912,21 @@ export function App(): ReactElement { if (pkgCount > 0) parts.push(`${pkgCount} Paket(e)`); if (itemCount > 0) parts.push(`${itemCount} Link(s)`); return ( - setDeleteConfirm(null)} open title="Bist Du Dir sicher?"> -

Möchtest Du wirklich diese Aufräumaktion(en) durchführen?
Ausgewählte Links löschen

-

Zu erledigende Aufgaben:
{parts.join(" + ")} löschen ? {totalRemaining} Link(s) verbleiben!

- -
- - -
-
+ setDeleteConfirm(null)} + onConfirm={() => { + if (deleteConfirm.dontAsk) { + setSettingsDraft((prev) => ({ ...prev, confirmDeleteSelection: false })); + void window.rd.updateSettings({ confirmDeleteSelection: false }).catch(() => {}); + } + executeDeleteSelection(deleteConfirm.ids); + setDeleteConfirm(null); + }} + onDontAskChange={(checked) => setDeleteConfirm((prev) => prev ? { ...prev, dontAsk: checked } : prev)} + /> ); })() : null} diff --git a/src/renderer/styles.css b/src/renderer/styles.css index 5c92b24..b3c73d1 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -3253,6 +3253,16 @@ td { flex-wrap: wrap; } +.delete-confirm-footer { + display: grid; + justify-items: end; + gap: 8px; +} + +.delete-confirm-dont-ask { + justify-content: flex-end; +} + .account-modal-actions { justify-content: space-between; position: sticky; diff --git a/src/renderer/views/downloads/DeleteConfirmationDialog.tsx b/src/renderer/views/downloads/DeleteConfirmationDialog.tsx new file mode 100644 index 0000000..b20f860 --- /dev/null +++ b/src/renderer/views/downloads/DeleteConfirmationDialog.tsx @@ -0,0 +1,37 @@ +import type { ReactElement } from "react"; +import { Dialog } from "../../ui/Dialog"; + +interface DeleteConfirmationDialogProps { + dontAsk: boolean; + parts: string[]; + totalRemaining: number; + onCancel: () => void; + onConfirm: () => void; + onDontAskChange: (checked: boolean) => void; +} + +export function DeleteConfirmationDialog({ + dontAsk, + parts, + totalRemaining, + onCancel, + onConfirm, + onDontAskChange +}: DeleteConfirmationDialogProps): ReactElement { + return ( + +

Möchtest Du wirklich diese Aufräumaktion(en) durchführen?
Ausgewählte Links löschen

+

Zu erledigende Aufgaben:
{parts.join(" + ")} löschen ? {totalRemaining} Link(s) verbleiben!

+
+
+ + +
+ +
+
+ ); +} diff --git a/tests/app-shell.test.tsx b/tests/app-shell.test.tsx index 7fe37f2..7239ad4 100644 --- a/tests/app-shell.test.tsx +++ b/tests/app-shell.test.tsx @@ -35,6 +35,16 @@ describe("desktop shell", () => { expect(removal).toContain('title: "Ausgewählte Links löschen"'); }); + it("places the delete confirmation opt-out below the right-aligned actions", () => { + const source = readFileSync(new URL("../src/renderer/views/downloads/DeleteConfirmationDialog.tsx", import.meta.url), "utf8"); + const css = readFileSync(new URL("../src/renderer/styles.css", import.meta.url), "utf8"); + + expect(source).toContain('className="delete-confirm-footer"'); + expect(source.indexOf('className="modal-actions"')).toBeLessThan(source.indexOf("Nicht mehr anzeigen")); + expect(source).toContain('className="toggle-line delete-confirm-dont-ask"'); + expect(css).toMatch(/\.delete-confirm-footer\s*{[^}]*display:\s*grid;[^}]*justify-items:\s*end;/s); + }); + it("does not stack renderer latency on the manager cadence for large active queues", () => { expect(getSnapshotRenderDelay(2_470, true, "downloads")).toBe(0); expect(getSnapshotRenderDelay(2_470, true, "statistics")).toBe(800); diff --git a/tests/visual/main.tsx b/tests/visual/main.tsx index 8ad148e..fd9d671 100644 --- a/tests/visual/main.tsx +++ b/tests/visual/main.tsx @@ -1,6 +1,7 @@ import type { ReactElement } from "react"; import { createRoot } from "react-dom/client"; import { App } from "../../src/renderer/App"; +import { DeleteConfirmationDialog } from "../../src/renderer/views/downloads/DeleteConfirmationDialog"; import type { ElectronApi } from "../../src/shared/preload-api"; import "../../src/renderer/theme.css"; import "../../src/renderer/styles.css"; @@ -75,6 +76,19 @@ export function renderVisualApp(render: (element: ReactElement) => void): void { render(); } +export function renderDeleteConfirmationDialog(render: (element: ReactElement) => void): void { + render( + {}} + onConfirm={() => {}} + onDontAskChange={() => {}} + /> + ); +} + export async function markVisualReady( scenario: VisualScenario, rootElement: VisualHarnessRoot, @@ -154,6 +168,12 @@ export async function startVisualHarness( runtime.marker.visualScenario = scenario; const root = runtime.createRoot(rootElement); + if (new URLSearchParams(runtime.search).get("dialog") === "delete-confirmation") { + renderDeleteConfirmationDialog((element) => root.render(element)); + await waitForVisualFrames(runtime.requestFrame); + runtime.marker.visualReady = "true"; + return; + } renderVisualApp((element) => root.render(element)); const readyOptions = {