Make plan deletion visible and reliable with in-page confirmation
Test and deploy / test-and-deploy (push) Successful in 1m28s
Test and deploy / test-and-deploy (push) Successful in 1m28s
This commit is contained in:
1 parent
f2abdcefe1
commit
a72aec9f86
4 files changed
+100
-8
No files matched your search
@@ -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
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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,[]);
|
||||
});
|
||||
Reference in new issue
Block a user