Make plan deletion visible and reliable with in-page confirmation
Test and deploy / test-and-deploy (push) Successful in 1m28s

This commit is contained in:
snowspeeder committed 2026-09-07 06:22:43 -04:00
1 parent f2abdcefe1
commit a72aec9f86
4 files changed
+100 -8

No files matched your search

+6
View File
@@ -4422,3 +4422,9 @@ select.f {
.planner-page .header-row-top {
max-width: 100%;
}
.delete-plan-dialog { width: min(500px, 94vw); }
#delete-plan-name { font-weight: 700; overflow-wrap: anywhere; }
#delete-plan-description { line-height: 1.6; }
#delete-plan-error { color: #a32920; }
.delete-plan-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 24px; }
+40 -8
View File
@@ -18,6 +18,41 @@ function node(tag, text, className = "") {
el.className = className;
return el;
}
const deleteDialog = document.getElementById("delete-plan-dialog"),
deleteName = document.getElementById("delete-plan-name"),
deleteError = document.getElementById("delete-plan-error"),
deleteConfirm = document.getElementById("delete-plan-confirm"),
deleteCancel = document.getElementById("delete-plan-cancel");
let pendingDelete = null, deleting = false;
deleteCancel.onclick = () => deleteDialog.close();
deleteDialog.addEventListener("cancel", (event) => {
if (deleting) event.preventDefault();
});
deleteDialog.addEventListener("close", () => { pendingDelete = null; });
deleteConfirm.onclick = async () => {
if (!pendingDelete || deleting) return;
deleting = true;
deleteConfirm.disabled = deleteCancel.disabled = true;
deleteConfirm.textContent = "Deleting…";
deleteError.textContent = "";
try {
try {
await api("/api/plans/" + pendingDelete.id, { method: "DELETE" });
} catch (error) {
// Another tab may already have removed this plan.
if (error.status !== 404) throw error;
}
deleteDialog.close();
await load();
showToast("Plan deleted.");
} catch (error) {
deleteError.textContent = "Could not delete this plan. " + error.message + " Try again.";
} finally {
deleting = false;
deleteConfirm.disabled = deleteCancel.disabled = false;
deleteConfirm.textContent = "Delete plan";
}
};
async function load() {
const current = ++generation;
const params = new URLSearchParams(new FormData(form));
@@ -92,14 +127,11 @@ async function load() {
const remove = node("button", "Delete", "ghost-btn small");
remove.type = "button";
remove.setAttribute("aria-label", "Delete " + plan.title);
remove.onclick = async () => {
if (!confirm(`Delete “${plan.title}”?`)) return;
try {
await api("/api/plans/" + plan.id, { method: "DELETE" });
await load();
} catch (e) {
showToast(e.message, "fail");
}
remove.onclick = () => {
pendingDelete = plan;
deleteName.textContent = plan.title;
deleteError.textContent = "";
deleteDialog.showModal();
};
actions.append(clone, rename, remove);
tr.append(actions);
+10
View File
@@ -151,6 +151,16 @@
</main>
</div>
</div>
<dialog id="delete-plan-dialog" class="workspace-dialog delete-plan-dialog" aria-labelledby="delete-plan-heading" aria-describedby="delete-plan-description">
<h2 id="delete-plan-heading">Delete this plan?</h2>
<p id="delete-plan-name"></p>
<p id="delete-plan-description">This removes the saved plan and its revision history. Completed roast records and inventory history will be kept. This cannot be undone.</p>
<p id="delete-plan-error" role="alert"></p>
<div class="delete-plan-actions">
<button type="button" class="ghost-btn" id="delete-plan-cancel" autofocus>Cancel</button>
<button type="button" class="primary-btn" id="delete-plan-confirm">Delete plan</button>
</div>
</dialog>
<script type="module" src="/js/plans.js?v=__ASSET_VERSION__"></script>
</body>
</html>
+44
View File
@@ -717,3 +717,47 @@ test(
assert.deepEqual(errors, []);
},
);
test("browser: deleting a plan confirms in-page, supports retry, and preserves roast records", browserOptions, async (t) => {
const { page, base, create, db, csrfToken, errors } = await session(t);
const plan = blankPlan();
const saved = await create(plan, "Burundi — previous light roast plan");
const keep = await create(plan, "Keep this plan");
await page.request.put(base + "/api/plans/" + saved.id, {
headers: { "x-csrf-token": csrfToken }, data: { name: "Burundi — previous light roast plan revised" },
});
const owner = (await db.query("SELECT user_id FROM roast_plans WHERE id=$1", [saved.id])).rows[0].user_id;
const roast = (await db.query("INSERT INTO actual_roasts(user_id,roast_plan_id,filename,original_content,parsed,plan_snapshot) VALUES($1,$2,'saved.alog','{}',$3,$4) RETURNING id", [owner,saved.id,{roast:{}},plan])).rows[0];
// Browser popup suppression must not turn Delete into a silent no-op.
await page.addInitScript(() => { window.confirm = () => false; });
await page.goto(base + "/plans");
const remove = page.getByRole("button", {name: "Delete Burundi — previous light roast plan revised", exact:true});
await remove.click();
const dialog = page.getByRole("dialog", {name:"Delete this plan?"});
await dialog.waitFor();
await page.screenshot({path:"/tmp/delete-plan-desktop.png"});
await page.setViewportSize({width:390,height:844});
await page.screenshot({path:"/tmp/delete-plan-mobile.png"});
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
await dialog.getByRole("button",{name:"Cancel",exact:true}).click();
assert.equal((await page.request.get(base + "/api/plans/" + saved.id)).status(),200);
await remove.click();
await page.route("**/api/plans/" + saved.id, async (route) => {
if (route.request().method() === "DELETE") await route.fulfill({status:503,contentType:"application/json",body:JSON.stringify({error:"Temporary service failure"})});
else await route.continue();
});
await dialog.getByRole("button",{name:"Delete plan",exact:true}).click();
await page.getByRole("alert").filter({hasText:"Temporary service failure"}).waitFor();
assert.equal(await dialog.isVisible(),true);
await page.unroute("**/api/plans/" + saved.id);
await dialog.getByRole("button",{name:"Delete plan",exact:true}).click();
await dialog.waitFor({state:"hidden"});
await page.waitForFunction(() => document.getElementById("plans-status").textContent === "1–1 of 1 plans");
assert.equal((await page.request.get(base + "/api/plans/" + saved.id)).status(),404);
assert.equal((await page.request.get(base + "/api/plans/" + keep.id)).status(),200);
assert.equal((await db.query("SELECT * FROM plan_revisions WHERE plan_id=$1",[saved.id])).rows.length,0);
const remaining = (await db.query("SELECT * FROM actual_roasts WHERE id=$1",[roast.id])).rows[0];
assert.equal(remaining.roast_plan_id,null);
assert.deepEqual(remaining.plan_snapshot,plan);
assert.deepEqual(errors,[]);
});