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.
This commit is contained in:
Sucukdeluxe
2026-08-15 02:56:44 +02:00
parent 57ae0190ec
commit 8efee74b5c
5 changed files with 93 additions and 19 deletions
+10 -13
View File
@@ -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 (
<Dialog actions={null} danger onClose={() => setDeleteConfirm(null)} open title="Bist Du Dir sicher?">
<p>Möchtest Du wirklich diese Aufräumaktion(en) durchführen?<br />Ausgewählte Links löschen</p>
<p><strong>Zu erledigende Aufgaben:</strong><br />{parts.join(" + ")} löschen ? {totalRemaining} Link(s) verbleiben!</p>
<label className="toggle-line">
<input type="checkbox" checked={deleteConfirm.dontAsk} onChange={(e) => setDeleteConfirm((prev) => prev ? { ...prev, dontAsk: e.target.checked } : prev)} />
Nicht mehr anzeigen
</label>
<div className="modal-actions">
<button className="btn" onClick={() => setDeleteConfirm(null)}>Abbrechen</button>
<button className="btn danger" onClick={() => {
<DeleteConfirmationDialog
dontAsk={deleteConfirm.dontAsk}
parts={parts}
totalRemaining={totalRemaining}
onCancel={() => setDeleteConfirm(null)}
onConfirm={() => {
if (deleteConfirm.dontAsk) {
setSettingsDraft((prev) => ({ ...prev, confirmDeleteSelection: false }));
void window.rd.updateSettings({ confirmDeleteSelection: false }).catch(() => {});
}
executeDeleteSelection(deleteConfirm.ids);
setDeleteConfirm(null);
}}>Fortfahren</button>
</div>
</Dialog>
}}
onDontAskChange={(checked) => setDeleteConfirm((prev) => prev ? { ...prev, dontAsk: checked } : prev)}
/>
);
})() : null}
+10
View File
@@ -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;
@@ -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 (
<Dialog actions={null} danger onClose={onCancel} open title="Bist Du Dir sicher?">
<p>Möchtest Du wirklich diese Aufräumaktion(en) durchführen?<br />Ausgewählte Links löschen</p>
<p><strong>Zu erledigende Aufgaben:</strong><br />{parts.join(" + ")} löschen ? {totalRemaining} Link(s) verbleiben!</p>
<div className="delete-confirm-footer">
<div className="modal-actions">
<button className="btn" onClick={onCancel}>Abbrechen</button>
<button className="btn danger" onClick={onConfirm}>Fortfahren</button>
</div>
<label className="toggle-line delete-confirm-dont-ask">
<input type="checkbox" checked={dontAsk} onChange={(event) => onDontAskChange(event.target.checked)} />
Nicht mehr anzeigen
</label>
</div>
</Dialog>
);
}
+10
View File
@@ -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);
+20
View File
@@ -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(<App />);
}
export function renderDeleteConfirmationDialog(render: (element: ReactElement) => void): void {
render(
<DeleteConfirmationDialog
dontAsk
parts={["2 Paket(e)"]}
totalRemaining={0}
onCancel={() => {}}
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 = {