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 {
|
.planner-page .header-row-top {
|
||||||
max-width: 100%;
|
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;
|
el.className = className;
|
||||||
return el;
|
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() {
|
async function load() {
|
||||||
const current = ++generation;
|
const current = ++generation;
|
||||||
const params = new URLSearchParams(new FormData(form));
|
const params = new URLSearchParams(new FormData(form));
|
||||||
@@ -92,14 +127,11 @@ async function load() {
|
|||||||
const remove = node("button", "Delete", "ghost-btn small");
|
const remove = node("button", "Delete", "ghost-btn small");
|
||||||
remove.type = "button";
|
remove.type = "button";
|
||||||
remove.setAttribute("aria-label", "Delete " + plan.title);
|
remove.setAttribute("aria-label", "Delete " + plan.title);
|
||||||
remove.onclick = async () => {
|
remove.onclick = () => {
|
||||||
if (!confirm(`Delete “${plan.title}”?`)) return;
|
pendingDelete = plan;
|
||||||
try {
|
deleteName.textContent = plan.title;
|
||||||
await api("/api/plans/" + plan.id, { method: "DELETE" });
|
deleteError.textContent = "";
|
||||||
await load();
|
deleteDialog.showModal();
|
||||||
} catch (e) {
|
|
||||||
showToast(e.message, "fail");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
actions.append(clone, rename, remove);
|
actions.append(clone, rename, remove);
|
||||||
tr.append(actions);
|
tr.append(actions);
|
||||||
|
|||||||
@@ -151,6 +151,16 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<script type="module" src="/js/plans.js?v=__ASSET_VERSION__"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -717,3 +717,47 @@ test(
|
|||||||
assert.deepEqual(errors, []);
|
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