From 135b2e731613a87a74f83edb9b49c67aaf3417cf Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sat, 15 Aug 2026 03:23:24 +0200 Subject: [PATCH] fix(downloads): keep active priority menu stable Treat the selected priority as a disabled no-op so clicking it no longer closes the context menu. Reorder packages consistently across high, normal, and low priority groups while preserving stable order within each group. Add regression coverage for all priority transitions and the active menu state. --- src/main/download-manager.ts | 26 ++++++----- src/renderer/App.tsx | 2 +- tests/download-manager.test.ts | 83 ++++++++++++++++++++++++++++++++++ tests/downloads-view.test.tsx | 6 +++ 4 files changed, 104 insertions(+), 13 deletions(-) diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 5ab9fd1..eb0756f 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -5504,23 +5504,25 @@ export class DownloadManager extends EventEmitter { const pkg = this.session.packages[packageId]; if (!pkg) return; if (priority !== "high" && priority !== "normal" && priority !== "low") return; + if ((pkg.priority || "normal") === priority) return; pkg.priority = priority; pkg.updatedAt = nowMs(); - if (priority === "high") { - const order = this.session.packageOrder; - const idx = order.indexOf(packageId); - if (idx > 0) { - order.splice(idx, 1); - let insertAt = 0; - for (let i = 0; i < order.length; i++) { - const p = this.session.packages[order[i]]; - if (p && p.priority === "high") { - insertAt = i + 1; - } + const order = this.session.packageOrder; + const currentIndex = order.indexOf(packageId); + if (currentIndex >= 0) { + order.splice(currentIndex, 1); + const priorityRank: Record = { high: 0, normal: 1, low: 2 }; + let insertAt = order.length; + for (let index = 0; index < order.length; index += 1) { + const other = this.session.packages[order[index]]; + const otherPriority = other?.priority || "normal"; + if (priorityRank[otherPriority] > priorityRank[priority]) { + insertAt = index; + break; } - order.splice(insertAt, 0, packageId); } + order.splice(insertAt, 0, packageId); } this.persistSoon(); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index fa36604..c1ba912 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -6114,7 +6114,7 @@ export function App(): ReactElement { const label = p === "high" ? "Hoch" : p === "low" ? "Niedrig" : "Standard"; const pkgIds = selectedPackageIds; const allMatch = pkgIds.every((id) => (snapshot.session.packages[id]?.priority || "normal") === p); - return ; + return ; })} diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index 8302b80..48bfba0 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -13785,3 +13785,86 @@ describe("mega-debrid api/web resolution overlap gate", () => { expect((manager as any).shouldDelayStartForItem(candidate)).toBe(true); }); }); + +describe("package priority ordering", () => { + function buildPriorityManager(priorities: Array<[string, "high" | "normal" | "low"]>): { + manager: DownloadManager; + session: ReturnType; + } { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-priority-order-")); + tempDirs.push(root); + const session = emptySession(); + const createdAt = Date.now(); + for (const [id, priority] of priorities) { + session.packageOrder.push(id); + session.packages[id] = { + id, + name: id, + outputDir: path.join(root, "downloads", id), + extractDir: path.join(root, "extract", id), + status: "queued", + itemIds: [], + cancelled: false, + enabled: true, + priority, + createdAt, + updatedAt: createdAt + }; + } + return { + manager: new DownloadManager(defaultSettings(), session, createStoragePaths(path.join(root, "state"))), + session + }; + } + + it("moves a package set to low priority behind every normal package", () => { + const { manager, session } = buildPriorityManager([ + ["high-a", "high"], + ["target", "normal"], + ["normal-b", "normal"], + ["low-a", "low"] + ]); + + manager.setPackagePriority("target", "low"); + + expect(session.packageOrder).toEqual(["high-a", "normal-b", "low-a", "target"]); + }); + + it("moves a package set to high priority ahead of every normal package", () => { + const { manager, session } = buildPriorityManager([ + ["high-a", "high"], + ["normal-a", "normal"], + ["target", "normal"], + ["low-a", "low"] + ]); + + manager.setPackagePriority("target", "high"); + + expect(session.packageOrder).toEqual(["high-a", "target", "normal-a", "low-a"]); + }); + + it("moves a package returned to normal between high and low priority groups", () => { + const { manager, session } = buildPriorityManager([ + ["high-a", "high"], + ["normal-a", "normal"], + ["low-a", "low"], + ["target", "low"] + ]); + + manager.setPackagePriority("target", "normal"); + + expect(session.packageOrder).toEqual(["high-a", "normal-a", "target", "low-a"]); + }); + + it("keeps an unchanged priority in place", () => { + const { manager, session } = buildPriorityManager([ + ["high-a", "high"], + ["high-b", "high"], + ["normal-a", "normal"] + ]); + + manager.setPackagePriority("high-a", "high"); + + expect(session.packageOrder).toEqual(["high-a", "high-b", "normal-a"]); + }); +}); diff --git a/tests/downloads-view.test.tsx b/tests/downloads-view.test.tsx index ee37c08..24fa585 100644 --- a/tests/downloads-view.test.tsx +++ b/tests/downloads-view.test.tsx @@ -1171,6 +1171,12 @@ describe("downloads view", () => { }); describe("downloads App integration", () => { + it("keeps the priority menu open when the selected priority is already active", () => { + const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8").replaceAll("\r\n", "\n"); + + expect(source).toMatch(/disabled=\{allMatch\}[\s\S]{0,260}window\.rd\.setPackagePriority/); + }); + it("updates the clipboard checkbox optimistically before IPC reconciliation", () => { const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8").replaceAll("\r\n", "\n");