From 31d2182258acc0a893e3c67895b7c92e888660b0 Mon Sep 17 00:00:00 2001 From: Rbanh Date: Wed, 18 Feb 2026 20:53:21 -0500 Subject: [PATCH] Add destructive-action confirmations in inspector editors --- frontend/app.js | 85 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/frontend/app.js b/frontend/app.js index 7d2c704..482f238 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -1701,6 +1701,51 @@ function hasNodeOnNet(net, ref, pin) { return (net.nodes ?? []).some((n) => n.ref === ref && n.pin === pin); } +function refsForSymbol(symbolId) { + return new Set((state.model?.instances ?? []).filter((i) => i.symbol === symbolId).map((i) => i.ref)); +} + +function symbolPinUsage(symbolId, pinName) { + const refs = refsForSymbol(symbolId); + const nets = new Set(); + let nodes = 0; + for (const net of state.model?.nets ?? []) { + for (const node of net.nodes ?? []) { + if (refs.has(node.ref) && node.pin === pinName) { + nodes += 1; + nets.add(net.name); + } + } + } + return { refs: refs.size, nodes, nets: [...nets].sort() }; +} + +function disconnectImpact(netName, ref, pin) { + const net = netByName(netName); + if (!net) { + return { remainingNodes: 0, orphaned: false }; + } + const remainingNodes = (net.nodes ?? []).filter((n) => !(n.ref === ref && n.pin === pin)).length; + return { + remainingNodes, + orphaned: remainingNodes < 2 + }; +} + +function deleteComponentImpact(ref) { + const impacted = []; + let removedNodes = 0; + for (const net of state.model?.nets ?? []) { + const before = net.nodes?.length ?? 0; + const after = (net.nodes ?? []).filter((n) => n.ref !== ref).length; + if (before !== after) { + removedNodes += before - after; + impacted.push({ name: net.name, removed: before - after, orphaned: after < 2 }); + } + } + return { removedNodes, impacted }; +} + function connectPinToNet(ref, pin, netName, opts = {}) { if (!state.model) { return { ok: false, message: "No model loaded." }; @@ -2283,6 +2328,16 @@ function setupEvents() { return; } const ref = state.selectedRef; + const impact = deleteComponentImpact(ref); + const orphaned = impact.impacted.filter((x) => x.orphaned).map((x) => x.name); + const summary = + `Delete ${ref}?\n` + + `- Net nodes removed: ${impact.removedNodes}\n` + + `- Nets touched: ${impact.impacted.length}\n` + + (orphaned.length ? `- Nets that will be removed (<2 nodes): ${orphaned.slice(0, 8).join(", ")}${orphaned.length > 8 ? " ..." : ""}\n` : ""); + if (!window.confirm(summary)) { + return; + } pushHistory("delete-component"); state.model.instances = state.model.instances.filter((i) => i.ref !== ref); for (const net of state.model.nets ?? []) { @@ -2416,6 +2471,13 @@ function setupEvents() { return; } const netName = btn.getAttribute("data-disconnect-net"); + const impact = disconnectImpact(netName, state.selectedPin.ref, state.selectedPin.pin); + const message = impact.orphaned + ? `Disconnect ${state.selectedPin.ref}.${state.selectedPin.pin} from ${netName}?\nThis will leave fewer than 2 nodes and remove net '${netName}'.` + : `Disconnect ${state.selectedPin.ref}.${state.selectedPin.pin} from ${netName}?`; + if (!window.confirm(message)) { + return; + } pushHistory("disconnect-pin"); const out = disconnectPinFromNet(state.selectedPin.ref, state.selectedPin.pin, netName); if (!out.ok) { @@ -2487,6 +2549,13 @@ function setupEvents() { const netName = state.selectedNet; const [ref, ...pinParts] = String(btn.getAttribute("data-remove-node")).split("."); const pin = pinParts.join("."); + const impact = disconnectImpact(netName, ref, pin); + const message = impact.orphaned + ? `Remove ${ref}.${pin} from ${netName}?\nThis will leave fewer than 2 nodes and remove net '${netName}'.` + : `Remove ${ref}.${pin} from ${netName}?`; + if (!window.confirm(message)) { + return; + } pushHistory("net-node-remove"); const out = disconnectPinFromNet(ref, pin, netName); if (!out.ok) { @@ -2532,6 +2601,22 @@ function setupEvents() { } const row = btn.closest(".symbolPinRow"); if (row) { + const pinName = String(row.querySelector(".pinName")?.value ?? row.getAttribute("data-old-pin") ?? "").trim(); + const inst = state.selectedRef ? instanceByRef(state.selectedRef) : null; + if (inst && pinName) { + const usage = symbolPinUsage(inst.symbol, pinName); + if (usage.nodes > 0) { + const netSample = usage.nets.slice(0, 8).join(", "); + const suffix = usage.nets.length > 8 ? " ..." : ""; + const prompt = + `Remove pin row '${pinName}' from symbol '${inst.symbol}' editor?\n` + + `Current usage: ${usage.nodes} net node(s) across ${usage.nets.length} net(s): ${netSample}${suffix}\n` + + "These references will be dropped when you apply symbol changes."; + if (!window.confirm(prompt)) { + return; + } + } + } row.remove(); invalidateSymbolMigrationPreview("Pin rows changed. Preview migration again before apply."); }