Add inline validation feedback for symbol pin row edits

This commit is contained in:
Rbanh 2026-02-18 20:34:25 -05:00
parent 9ee97ffa8e
commit c02b14649e
3 changed files with 56 additions and 2 deletions

View File

@ -74,6 +74,7 @@ const el = {
symbolHeightInput: document.getElementById("symbolHeightInput"), symbolHeightInput: document.getElementById("symbolHeightInput"),
addSymbolPinBtn: document.getElementById("addSymbolPinBtn"), addSymbolPinBtn: document.getElementById("addSymbolPinBtn"),
applySymbolBtn: document.getElementById("applySymbolBtn"), applySymbolBtn: document.getElementById("applySymbolBtn"),
symbolValidation: document.getElementById("symbolValidation"),
symbolPinsList: document.getElementById("symbolPinsList"), symbolPinsList: document.getElementById("symbolPinsList"),
pinMeta: document.getElementById("pinMeta"), pinMeta: document.getElementById("pinMeta"),
pinNameInput: document.getElementById("pinNameInput"), pinNameInput: document.getElementById("pinNameInput"),
@ -880,10 +881,18 @@ function renderSymbolEditorForRef(ref) {
el.symbolCategoryInput.value = String(sym.category ?? ""); el.symbolCategoryInput.value = String(sym.category ?? "");
el.symbolWidthInput.value = String(Number(sym.body?.width ?? 120)); el.symbolWidthInput.value = String(Number(sym.body?.width ?? 120));
el.symbolHeightInput.value = String(Number(sym.body?.height ?? 80)); el.symbolHeightInput.value = String(Number(sym.body?.height ?? 80));
el.symbolValidation.textContent = "";
el.symbolValidation.classList.remove("symbolValidationError");
el.symbolPinsList.innerHTML = (sym.pins ?? []).map((pin) => symbolPinRowHtml(pin)).join(""); el.symbolPinsList.innerHTML = (sym.pins ?? []).map((pin) => symbolPinRowHtml(pin)).join("");
el.symbolEditor.classList.remove("hidden"); el.symbolEditor.classList.remove("hidden");
} }
function clearSymbolRowValidation(rows) {
for (const row of rows) {
row.classList.remove("invalidRow");
}
}
function renderPinEditor() { function renderPinEditor() {
if (!state.selectedPin || !state.model) { if (!state.selectedPin || !state.model) {
el.pinEditor.classList.add("hidden"); el.pinEditor.classList.add("hidden");
@ -2284,6 +2293,17 @@ function setupEvents() {
} }
}); });
el.symbolPinsList.addEventListener("input", (evt) => {
const row = evt.target.closest(".symbolPinRow");
if (row) {
row.classList.remove("invalidRow");
}
if (el.symbolValidation.textContent) {
el.symbolValidation.textContent = "";
el.symbolValidation.classList.remove("symbolValidationError");
}
});
el.applySymbolBtn.addEventListener("click", () => { el.applySymbolBtn.addEventListener("click", () => {
if (!state.selectedRef) { if (!state.selectedRef) {
return; return;
@ -2302,12 +2322,18 @@ function setupEvents() {
} }
const rows = [...el.symbolPinsList.querySelectorAll(".symbolPinRow")]; const rows = [...el.symbolPinsList.querySelectorAll(".symbolPinRow")];
clearSymbolRowValidation(rows);
el.symbolValidation.textContent = "";
el.symbolValidation.classList.remove("symbolValidationError");
if (!rows.length) { if (!rows.length) {
el.jsonFeedback.textContent = "Symbol must have at least one pin."; el.jsonFeedback.textContent = "Symbol must have at least one pin.";
el.symbolValidation.textContent = "Symbol must contain at least one pin row.";
el.symbolValidation.classList.add("symbolValidationError");
return; return;
} }
const parsedPins = []; const parsedPins = [];
const rowErrors = [];
for (const row of rows) { for (const row of rows) {
const name = String(row.querySelector(".pinName")?.value ?? "").trim(); const name = String(row.querySelector(".pinName")?.value ?? "").trim();
const number = String(row.querySelector(".pinNumber")?.value ?? "").trim(); const number = String(row.querySelector(".pinNumber")?.value ?? "").trim();
@ -2315,8 +2341,9 @@ function setupEvents() {
const offset = Number(row.querySelector(".pinOffset")?.value ?? 0); const offset = Number(row.querySelector(".pinOffset")?.value ?? 0);
const type = String(row.querySelector(".pinType")?.value ?? ""); const type = String(row.querySelector(".pinType")?.value ?? "");
if (!name || !number || !PIN_SIDES.includes(side) || !PIN_TYPES.includes(type) || !Number.isFinite(offset) || offset < 0) { if (!name || !number || !PIN_SIDES.includes(side) || !PIN_TYPES.includes(type) || !Number.isFinite(offset) || offset < 0) {
el.jsonFeedback.textContent = "Invalid symbol pin row values."; row.classList.add("invalidRow");
return; rowErrors.push("Each pin row needs name, number, valid side/type, and offset >= 0.");
continue;
} }
parsedPins.push({ parsedPins.push({
oldName: row.getAttribute("data-old-pin") ?? name, oldName: row.getAttribute("data-old-pin") ?? name,
@ -2324,9 +2351,24 @@ function setupEvents() {
}); });
} }
if (rowErrors.length) {
el.jsonFeedback.textContent = "Fix invalid symbol pin rows before applying.";
el.symbolValidation.textContent = rowErrors[0];
el.symbolValidation.classList.add("symbolValidationError");
return;
}
const unique = new Set(parsedPins.map((p) => p.pin.name)); const unique = new Set(parsedPins.map((p) => p.pin.name));
if (unique.size !== parsedPins.length) { if (unique.size !== parsedPins.length) {
el.jsonFeedback.textContent = "Duplicate pin names are not allowed."; el.jsonFeedback.textContent = "Duplicate pin names are not allowed.";
el.symbolValidation.textContent = "Duplicate pin names detected. Each pin name must be unique.";
el.symbolValidation.classList.add("symbolValidationError");
for (const row of rows) {
const name = String(row.querySelector(".pinName")?.value ?? "").trim();
if (name && parsedPins.filter((p) => p.pin.name === name).length > 1) {
row.classList.add("invalidRow");
}
}
return; return;
} }
@ -2368,6 +2410,8 @@ function setupEvents() {
if (state.selectedPin && !pinExists(state.selectedPin.ref, state.selectedPin.pin)) { if (state.selectedPin && !pinExists(state.selectedPin.ref, state.selectedPin.pin)) {
state.selectedPin = null; state.selectedPin = null;
} }
el.symbolValidation.textContent = "";
el.symbolValidation.classList.remove("symbolValidationError");
const removedPinCount = [...beforePins].filter((p) => !allowedPins.has(p)).length; const removedPinCount = [...beforePins].filter((p) => !allowedPins.has(p)).length;
el.jsonFeedback.textContent = removedPinCount el.jsonFeedback.textContent = removedPinCount
? `Updated symbol ${inst.symbol}. Removed ${removedPinCount} pin mappings from nets/UI metadata.` ? `Updated symbol ${inst.symbol}. Removed ${removedPinCount} pin mappings from nets/UI metadata.`

View File

@ -108,6 +108,7 @@
<button id="addSymbolPinBtn">Add Pin</button> <button id="addSymbolPinBtn">Add Pin</button>
<button id="applySymbolBtn">Apply Symbol</button> <button id="applySymbolBtn">Apply Symbol</button>
</div> </div>
<div id="symbolValidation" class="hintText"></div>
<div id="symbolPinsList" class="miniList"></div> <div id="symbolPinsList" class="miniList"></div>
</div> </div>
<div id="pinEditor" class="editorCard hidden"> <div id="pinEditor" class="editorCard hidden">

View File

@ -269,6 +269,15 @@ textarea {
font-size: 0.75rem; font-size: 0.75rem;
} }
.symbolPinRow.invalidRow {
background: #fff3f2;
}
.symbolValidationError {
color: var(--error);
font-size: 0.76rem;
}
.canvasTools { .canvasTools {
display: flex; display: flex;
gap: 8px; gap: 8px;