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.
This commit is contained in:
@@ -5504,23 +5504,25 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const pkg = this.session.packages[packageId];
|
const pkg = this.session.packages[packageId];
|
||||||
if (!pkg) return;
|
if (!pkg) return;
|
||||||
if (priority !== "high" && priority !== "normal" && priority !== "low") return;
|
if (priority !== "high" && priority !== "normal" && priority !== "low") return;
|
||||||
|
if ((pkg.priority || "normal") === priority) return;
|
||||||
pkg.priority = priority;
|
pkg.priority = priority;
|
||||||
pkg.updatedAt = nowMs();
|
pkg.updatedAt = nowMs();
|
||||||
|
|
||||||
if (priority === "high") {
|
const order = this.session.packageOrder;
|
||||||
const order = this.session.packageOrder;
|
const currentIndex = order.indexOf(packageId);
|
||||||
const idx = order.indexOf(packageId);
|
if (currentIndex >= 0) {
|
||||||
if (idx > 0) {
|
order.splice(currentIndex, 1);
|
||||||
order.splice(idx, 1);
|
const priorityRank: Record<PackagePriority, number> = { high: 0, normal: 1, low: 2 };
|
||||||
let insertAt = 0;
|
let insertAt = order.length;
|
||||||
for (let i = 0; i < order.length; i++) {
|
for (let index = 0; index < order.length; index += 1) {
|
||||||
const p = this.session.packages[order[i]];
|
const other = this.session.packages[order[index]];
|
||||||
if (p && p.priority === "high") {
|
const otherPriority = other?.priority || "normal";
|
||||||
insertAt = i + 1;
|
if (priorityRank[otherPriority] > priorityRank[priority]) {
|
||||||
}
|
insertAt = index;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
order.splice(insertAt, 0, packageId);
|
|
||||||
}
|
}
|
||||||
|
order.splice(insertAt, 0, packageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
|
|||||||
@@ -6114,7 +6114,7 @@ export function App(): ReactElement {
|
|||||||
const label = p === "high" ? "Hoch" : p === "low" ? "Niedrig" : "Standard";
|
const label = p === "high" ? "Hoch" : p === "low" ? "Niedrig" : "Standard";
|
||||||
const pkgIds = selectedPackageIds;
|
const pkgIds = selectedPackageIds;
|
||||||
const allMatch = pkgIds.every((id) => (snapshot.session.packages[id]?.priority || "normal") === p);
|
const allMatch = pkgIds.every((id) => (snapshot.session.packages[id]?.priority || "normal") === p);
|
||||||
return <button key={p} className={`ctx-menu-item${allMatch ? " ctx-menu-active" : ""}`} onClick={() => { for (const id of pkgIds) void window.rd.setPackagePriority(id, p).catch(() => {}); setContextMenu(null); }}>{allMatch ? `[Aktiv] ${label}` : label}</button>;
|
return <button key={p} className={`ctx-menu-item${allMatch ? " ctx-menu-active" : ""}`} disabled={allMatch} onClick={() => { for (const id of pkgIds) void window.rd.setPackagePriority(id, p).catch(() => {}); setContextMenu(null); }}>{allMatch ? `[Aktiv] ${label}` : label}</button>;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13785,3 +13785,86 @@ describe("mega-debrid api/web resolution overlap gate", () => {
|
|||||||
expect((manager as any).shouldDelayStartForItem(candidate)).toBe(true);
|
expect((manager as any).shouldDelayStartForItem(candidate)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("package priority ordering", () => {
|
||||||
|
function buildPriorityManager(priorities: Array<[string, "high" | "normal" | "low"]>): {
|
||||||
|
manager: DownloadManager;
|
||||||
|
session: ReturnType<typeof emptySession>;
|
||||||
|
} {
|
||||||
|
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"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1171,6 +1171,12 @@ describe("downloads view", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("downloads App integration", () => {
|
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", () => {
|
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");
|
const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8").replaceAll("\r\n", "\n");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user