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:
+16
-19
@@ -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={() => {
|
||||
if (deleteConfirm.dontAsk) {
|
||||
setSettingsDraft((prev) => ({ ...prev, confirmDeleteSelection: false }));
|
||||
void window.rd.updateSettings({ confirmDeleteSelection: false }).catch(() => {});
|
||||
}
|
||||
executeDeleteSelection(deleteConfirm.ids);
|
||||
setDeleteConfirm(null);
|
||||
}}>Fortfahren</button>
|
||||
</div>
|
||||
</Dialog>
|
||||
<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);
|
||||
}}
|
||||
onDontAskChange={(checked) => setDeleteConfirm((prev) => prev ? { ...prev, dontAsk: checked } : prev)}
|
||||
/>
|
||||
);
|
||||
})() : null}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user