Add destructive-action confirmations in inspector editors

This commit is contained in:
Rbanh 2026-02-18 20:53:21 -05:00
parent fcad4b284b
commit 31d2182258

View File

@ -1701,6 +1701,51 @@ function hasNodeOnNet(net, ref, pin) {
return (net.nodes ?? []).some((n) => n.ref === ref && n.pin === 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 = {}) { function connectPinToNet(ref, pin, netName, opts = {}) {
if (!state.model) { if (!state.model) {
return { ok: false, message: "No model loaded." }; return { ok: false, message: "No model loaded." };
@ -2283,6 +2328,16 @@ function setupEvents() {
return; return;
} }
const ref = state.selectedRef; 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"); pushHistory("delete-component");
state.model.instances = state.model.instances.filter((i) => i.ref !== ref); state.model.instances = state.model.instances.filter((i) => i.ref !== ref);
for (const net of state.model.nets ?? []) { for (const net of state.model.nets ?? []) {
@ -2416,6 +2471,13 @@ function setupEvents() {
return; return;
} }
const netName = btn.getAttribute("data-disconnect-net"); 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"); pushHistory("disconnect-pin");
const out = disconnectPinFromNet(state.selectedPin.ref, state.selectedPin.pin, netName); const out = disconnectPinFromNet(state.selectedPin.ref, state.selectedPin.pin, netName);
if (!out.ok) { if (!out.ok) {
@ -2487,6 +2549,13 @@ function setupEvents() {
const netName = state.selectedNet; const netName = state.selectedNet;
const [ref, ...pinParts] = String(btn.getAttribute("data-remove-node")).split("."); const [ref, ...pinParts] = String(btn.getAttribute("data-remove-node")).split(".");
const pin = pinParts.join("."); 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"); pushHistory("net-node-remove");
const out = disconnectPinFromNet(ref, pin, netName); const out = disconnectPinFromNet(ref, pin, netName);
if (!out.ok) { if (!out.ok) {
@ -2532,6 +2601,22 @@ function setupEvents() {
} }
const row = btn.closest(".symbolPinRow"); const row = btn.closest(".symbolPinRow");
if (row) { 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(); row.remove();
invalidateSymbolMigrationPreview("Pin rows changed. Preview migration again before apply."); invalidateSymbolMigrationPreview("Pin rows changed. Preview migration again before apply.");
} }