From b5854a22279b2d05b13dedc577b1b6985e2392d4 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sat, 15 Aug 2026 01:53:03 +0200 Subject: [PATCH] fix(accounts): clear multi-selection globally on Escape Route account selection clearing through the existing window-level Escape handler so it works regardless of which account surface owns focus. Preserve text editing and open overlay behavior, and remove the misleading focus-dependent workspace handler. --- src/renderer/App.tsx | 17 +++++++++-------- src/renderer/selection.ts | 15 +++++++++++++++ .../views/settings/AccountWorkspace.tsx | 12 +----------- tests/selection.test.ts | 15 +++++++++++++++ tests/settings-view.test.tsx | 8 ++------ 5 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 6c1c705..07f4678 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -39,7 +39,7 @@ import { getProviderUsageDayKey } from "../shared/provider-daily-limits"; import { preservePackageOrderForDisplay, sortPackageOrderByName } from "./package-order"; -import { pruneSelection, shouldClearDownloadSelection, shouldClearDownloadSelectionOnEscape } from "./selection"; +import { pruneSelection, resolveEscapeSelectionScope, shouldClearDownloadSelection } from "./selection"; import { buildConfiguredProviderOrder, buildScopedAccountEnabledState, getAccountDialogSelectableOptions, matchesAccountModeFilter, pruneAccountRowSelections, resolveAccountStatusState, resolveAccountUsername, resolveVisibleAccountKind, runOptimisticAccountUpdate, updateAccountRowSelection } from "./account-ui"; import type { AccountModeFilter } from "./account-ui"; import { buildAccountDeleteCommand, buildAccountReplaceCommand, buildAccountSecretRequest, createAccountEditState, validateAccountEdit } from "./account-edit"; @@ -4156,11 +4156,13 @@ export function App(): ReactElement { if (e.key === "Escape") { const target = e.target as HTMLElement; const inputType = target.tagName === "INPUT" ? (target as HTMLInputElement).type : ""; - if (shouldClearDownloadSelectionOnEscape(target.tagName, inputType)) { - if (document.querySelector(".ctx-menu") || document.querySelector(".modal-backdrop")) return; - if (tabRef.current === "downloads") setSelectedIds(new Set()); - else if (tabRef.current === "history") setSelectedHistoryIds(new Set()); - } + const selectionScope = resolveEscapeSelectionScope(tabRef.current, settingsSubTab, target.tagName, inputType); + if (selectionScope) { + if (document.querySelector(".ctx-menu") || document.querySelector(".modal-backdrop")) return; + if (selectionScope === "downloads") setSelectedIds(new Set()); + else if (selectionScope === "history") setSelectedHistoryIds(new Set()); + else setSelectedAccountRowKeys(new Set()); + } } if (e.key === "Delete" && tabRef.current === "downloads" && selectedIds.size > 0) { const target = e.target as HTMLElement; @@ -4177,7 +4179,7 @@ export function App(): ReactElement { window.addEventListener("keydown", onKey); window.addEventListener("mousedown", onDown); return () => { window.removeEventListener("keydown", onKey); window.removeEventListener("mousedown", onDown); }; - }, [selectedIds, requestDeleteSelection]); + }, [requestDeleteSelection, selectedIds, settingsSubTab]); const onExportBackup = async (): Promise => { closeMenus(); @@ -4959,7 +4961,6 @@ export function App(): ReactElement { } setSelectedAccountRowKeys((current) => new Set(updateAccountRowSelection([...current], rowKey, additive))); }, - onClearSelection: () => setSelectedAccountRowKeys(new Set()), onToggleEnabled: (rowId) => { const row = accountRowBindings.get(rowId); if (row) toggleAccountTableRow(row); diff --git a/src/renderer/selection.ts b/src/renderer/selection.ts index 0340d75..b423885 100644 --- a/src/renderer/selection.ts +++ b/src/renderer/selection.ts @@ -13,6 +13,21 @@ export function shouldClearDownloadSelectionOnEscape(tagName: string, inputType return ["checkbox", "radio", "button"].includes(inputType.toLowerCase()); } +export function resolveEscapeSelectionScope( + view: string, + settingsSection: string, + tagName: string, + inputType = "" +): "downloads" | "history" | "accounts" | null { + if (!shouldClearDownloadSelectionOnEscape(tagName, inputType)) { + return null; + } + if (view === "downloads" || view === "history") { + return view; + } + return view === "settings" && settingsSection === "accounts" ? "accounts" : null; +} + /** * Drop selected ids whose package OR item no longer exists in the session. * The selection set mixes package and item ids; when entries vanish (delta diff --git a/src/renderer/views/settings/AccountWorkspace.tsx b/src/renderer/views/settings/AccountWorkspace.tsx index a953f64..728b405 100644 --- a/src/renderer/views/settings/AccountWorkspace.tsx +++ b/src/renderer/views/settings/AccountWorkspace.tsx @@ -108,7 +108,6 @@ export interface AccountWorkspaceViewModel { export interface AccountWorkspaceActions { onPanelChange: (panel: AccountWorkspacePanel) => void; onSelect: (rowId: string, additive: boolean) => void; - onClearSelection: () => void; onToggleEnabled: (rowId: string) => void; onEdit: (rowId: string) => void; onContextMenu: (rowId: string, x: number, y: number) => void; @@ -583,16 +582,7 @@ function AccountRules({ model, actions }: AccountWorkspaceProps): ReactElement { export function AccountWorkspace({ model, actions }: AccountWorkspaceProps): ReactElement { return ( -
{ - if (event.key !== "Escape" || model.selectedIds.length === 0) { - return; - } - event.preventDefault(); - actions.onClearSelection(); - }} - > +
{model.busy ? Accountdaten werden aktualisiert. : null} {model.error ? {model.error} : null}
diff --git a/tests/selection.test.ts b/tests/selection.test.ts index bf6d8d9..17fec51 100644 --- a/tests/selection.test.ts +++ b/tests/selection.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { pruneSelection, shouldClearDownloadSelection, shouldClearDownloadSelectionOnEscape } from "../src/renderer/selection"; +import * as selection from "../src/renderer/selection"; import type { SessionState } from "../src/shared/types"; function session(packageIds: string[], itemIds: string[]): Pick { @@ -62,3 +63,17 @@ describe("download selection clearing", () => { expect(shouldClearDownloadSelectionOnEscape("TEXTAREA")).toBe(false); }); }); + +describe("global Escape selection routing", () => { + it("routes Escape to the account selection even when focus is outside the account workspace", () => { + const api = selection as typeof selection & { + resolveEscapeSelectionScope?: (view: string, settingsSection: string, tagName: string, inputType?: string) => string | null; + }; + + expect(api.resolveEscapeSelectionScope).toBeTypeOf("function"); + expect(api.resolveEscapeSelectionScope?.("settings", "accounts", "BODY")).toBe("accounts"); + expect(api.resolveEscapeSelectionScope?.("settings", "accounts", "INPUT", "text")).toBeNull(); + expect(api.resolveEscapeSelectionScope?.("downloads", "allgemein", "DIV")).toBe("downloads"); + expect(api.resolveEscapeSelectionScope?.("history", "accounts", "DIV")).toBe("history"); + }); +}); diff --git a/tests/settings-view.test.tsx b/tests/settings-view.test.tsx index 47e1513..858c171 100644 --- a/tests/settings-view.test.tsx +++ b/tests/settings-view.test.tsx @@ -301,7 +301,6 @@ function workspaceActions(overrides: Partial = {}): Acc return { onPanelChange: () => {}, onSelect: () => {}, - onClearSelection: () => {}, onToggleEnabled: () => {}, onEdit: () => {}, onContextMenu: () => {}, @@ -799,18 +798,15 @@ describe("account workspace", () => { ]); }); - it("shows the selected account count and clears the selection with Escape", () => { - let cleared = 0; + it("shows the selected account count", () => { const model = workspaceModel(); const tree = AccountWorkspace({ - actions: workspaceActions({ onClearSelection: () => { cleared += 1; } }), + actions: workspaceActions(), model: { ...model, selectedIds: model.rows.slice(0, 3).map((row) => row.id) } }); const html = renderToStaticMarkup(tree); expect(html).toContain("− Entfernen (3)"); - tree.props.onKeyDown({ key: "Escape", preventDefault: () => {} }); - expect(cleared).toBe(1); }); it("removes the redundant global account activation switch", () => {