diff --git a/operations/static/operations/job_panel.js b/operations/static/operations/job_panel.js
index 9e718d45..c7157a4f 100644
--- a/operations/static/operations/job_panel.js
+++ b/operations/static/operations/job_panel.js
@@ -1,6 +1,7 @@
// The job panel of a resource page (operations framework). It polls the
// resource's jobs, shows progress and outcomes, and keeps the page's own
// status poll from reloading (backgroundJobRunning) while it shows anything.
+// A job that succeeds without a warning reloads the page instead.
// The Bootstrap colour of each job state, for badges and alerts; the jobs
// pages use the same map (operations/templatetags/operations.py)
@@ -113,6 +114,20 @@ function jobPanel(element, urls, csrfToken, initialIds) {
holding = busy;
}
+ // a job that did what it was asked, with nothing more to read: the page shows its effect
+ function quiet(job) {
+ var result = job.result || {};
+ return job.state === "succeeded" && !result.warning && !(result.left_behind || []).length;
+ }
+
+ // a dialog open, a field in use or an XML edit not saved: its outcome waits in the panel, with Refresh
+ function interacting() {
+ var active = document.activeElement;
+ return !!document.querySelector(".modal.show")
+ || !!(active && active.matches && active.matches("input, select, textarea"))
+ || !!(window.xmlEditUnsaved && window.xmlEditUnsaved());
+ }
+
function fetchOutcome(id) {
if (inflight[id]) { return; }
inflight[id] = true;
@@ -123,6 +138,12 @@ function jobPanel(element, urls, csrfToken, initialIds) {
// ends; the next poll asks for it again
if (["queued", "running", "cancelling"].indexOf(job.state) >= 0) { return; }
delete pending[id];
+ // only when no other job runs, is shown or awaited: a reload would lose its outcome
+ var others = Object.keys(outcomes).length + Object.keys(pending).length + Object.keys(shown).length;
+ if (quiet(job) && !interacting() && !others) {
+ window.location.reload(); // the job is not remembered again: no loop
+ return;
+ }
outcomes[id] = job;
render();
})
diff --git a/operations/tests/js/job_panel.test.js b/operations/tests/js/job_panel.test.js
index 98e2119d..f56e34fd 100644
--- a/operations/tests/js/job_panel.test.js
+++ b/operations/tests/js/job_panel.test.js
@@ -18,7 +18,7 @@ function advance(ms) {
}
const realNow = Date.now;
Date.now = () => 1700000000000 + now;
-global.document = { hidden: false, addEventListener() {} };
+global.document = { hidden: false, addEventListener() {}, activeElement: null, querySelector: () => null };
global.window = { backgroundJobRunning: false, location: { reload() { window.reloaded = true; } } };
// requests settle only when the test says so; callbacks run in registration order
@@ -259,8 +259,65 @@ assert.strictEqual(window.backgroundJobRunning, true, "still followed");
advance(3000);
answer("/r", empty);
assert.strictEqual(open.filter((r) => !r.settled && r.url === "/j/u").length, 1, "asked again once per poll");
+window.reloaded = false;
answer("/j/u", { ...running, id: "u", label: "Upload", state: "succeeded" });
-assert.ok(el11.innerHTML.includes("Upload") && el11.innerHTML.includes('succeeded'),
- "the outcome shows once the job ends");
+assert.ok(window.reloaded, "a job that succeeds without a warning reloads the page: it shows the job's effect");
+assert.ok(!el11.innerHTML.includes('succeeded'), "not shown as well");
+
+// a dialog open or a field in use: the outcome waits in the panel, with Refresh
+for (const [label, doc] of [["dialog", { querySelector: (s) => s === ".modal.show" ? {} : null }],
+ ["field", { activeElement: { matches: (s) => s === "input, select, textarea" } }]]) {
+ open = []; timers = []; window.reloaded = false;
+ Object.assign(document, { activeElement: null, querySelector: () => null }, doc);
+ const elm = { innerHTML: "", addEventListener() {} };
+ jobPanel(elm, urls, "tok", ["m"]);
+ advance(0);
+ answer("/r", empty);
+ answer("/j/m", { ...running, id: "m", label: "Clone", state: "succeeded" });
+ assert.ok(!window.reloaded && elm.innerHTML.includes('succeeded')
+ && elm.innerHTML.includes("data-refresh"), "with a " + label + ": the outcome with Refresh, no reload");
+}
+Object.assign(document, { activeElement: null, querySelector: () => null });
+
+// an XML edit not saved: no reload, as for the page's status poll
+open = []; timers = []; window.reloaded = false;
+window.xmlEditUnsaved = () => true;
+const elx = { innerHTML: "", addEventListener() {} };
+jobPanel(elx, urls, "tok", ["x"]);
+advance(0);
+answer("/r", empty);
+answer("/j/x", { ...running, id: "x", state: "succeeded" });
+assert.ok(!window.reloaded && elx.innerHTML.includes("data-refresh"), "an unsaved XML edit keeps the page");
+delete window.xmlEditUnsaved;
+
+// another job's outcome shown or still awaited: a quiet success does not wipe it
+open = []; timers = []; window.reloaded = false;
+const elo = { innerHTML: "", addEventListener() {} };
+jobPanel(elo, urls, "tok", ["f", "s", "w"]);
+advance(0);
+answer("/r", empty);
+answer("/j/f", { ...running, id: "f", label: "Migrate", state: "failed", error: "no route" });
+answer("/j/s", { ...running, id: "s", label: "Clone", state: "succeeded" });
+assert.ok(!window.reloaded && elo.innerHTML.includes("no route") && elo.innerHTML.includes("Clone"),
+ "the failure stays, the success shows beside it");
+answer("/j/w", { ...running, id: "w", label: "Snapshot", state: "succeeded" });
+assert.ok(!window.reloaded, "still not while an outcome is shown");
+open = []; timers = []; window.reloaded = false;
+const elp = { innerHTML: "", addEventListener() {} };
+jobPanel(elp, urls, "tok", ["s2", "f2"]);
+advance(0);
+answer("/r", empty);
+answer("/j/s2", { ...running, id: "s2", state: "succeeded" });
+assert.ok(!window.reloaded, "not while another outcome is awaited");
+answer("/j/f2", { ...running, id: "f2", state: "failed", error: "refused" });
+assert.ok(elp.innerHTML.includes("refused"), "which then shows");
+// another job still running: its outcome would be lost to a reload
+open = []; timers = []; window.reloaded = false;
+const elr = { innerHTML: "", addEventListener() {} };
+jobPanel(elr, urls, "tok", ["s3"]);
+advance(0);
+answer("/r", { ...empty, jobs: [{ ...running, id: "r3" }] });
+answer("/j/s3", { ...running, id: "s3", state: "succeeded" });
+assert.ok(!window.reloaded, "not while another job runs");
Date.now = realNow;
console.log("ok");