+
+ + Requirement
+
+
Delete device
`;
@@ -729,6 +732,18 @@ function renderInspectorDevice(body, id) {
});
});
+ // + Requirement — open the modal pre-filled with this device as the
+ // "from" endpoint. Refuses if the project has fewer than 2 devices
+ // (a requirement needs two distinct endpoints).
+ body.querySelector("#dev-add-req").addEventListener("click", () => {
+ if (!state.active) return;
+ if (state.devices.length < 2) {
+ alert("Add a second device before declaring a requirement.");
+ return;
+ }
+ openRequirementModal(null, { from: d.id });
+ });
+
// +Port — switch the inspector to the new-port form. m fills in
// type + edge + label and clicks Create; no canvas click required.
body.querySelector("#dev-add-port").addEventListener("click", () => {
@@ -1344,46 +1359,11 @@ function bindDebouncedRename(input, persist) {
function render() {
renderProjectPicker();
renderLegend();
- renderRequirements();
renderCanvas();
renderEmptyHint();
renderInspector();
}
-// ---------- requirements sidebar ---------- //
-
-function renderRequirements() {
- const ul = $("#requirement-list");
- ul.innerHTML = "";
- const deviceById = new Map(state.devices.map((d) => [d.id, d]));
- const cableTypeById = new Map(state.cableTypes.map((t) => [t.id, t]));
- for (const r of state.requirements) {
- const a = deviceById.get(r.from_device_id);
- const b = deviceById.get(r.to_device_id);
- if (!a || !b) continue; // a device delete cascade — UI will rerender soon
- const ct = r.preferred_cable_type_id != null ? cableTypeById.get(r.preferred_cable_type_id) : null;
- const li = document.createElement("li");
- li.className = "requirement-row";
- li.dataset.id = String(r.id);
- if (state.selection?.kind === "requirement" && state.selection.id === r.id) {
- li.setAttribute("aria-current", "true");
- }
- const cableLabel = ct ? `${ct.name}` : "solver picks";
- li.innerHTML = `
-
- ${escapeHtml(a.name)} ↔ ${escapeHtml(b.name)}
- · ${escapeHtml(cableLabel)}
-
-
${r.must_connect ? "must" : "nice"}
- `;
- li.addEventListener("click", () => {
- state.selection = { kind: "requirement", id: r.id };
- render();
- });
- ul.append(li);
- }
-}
-
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, (c) => ({
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
@@ -2505,6 +2485,7 @@ function switchAdminTab(name) {
case "cable-types": return renderAdminCableTypes(body);
case "device-types": return renderAdminDeviceTypes(body);
case "setup-templates": return renderAdminSetupTemplates(body);
+ case "requirements": return renderAdminRequirements(body);
}
}
@@ -2793,6 +2774,79 @@ function renderAdminSetupTemplates(body) {
`;
}
+// ---------- admin: requirements (all) ---------- //
+
+function renderAdminRequirements(body) {
+ if (!state.active) {
+ body.innerHTML = `
Pick a project to see its requirements.
`;
+ return;
+ }
+ const deviceById = new Map(state.devices.map((d) => [d.id, d]));
+ const cableTypeBy = new Map(state.cableTypes.map((t) => [t.id, t]));
+ const rows = state.requirements.map((r) => {
+ const a = deviceById.get(r.from_device_id);
+ const b = deviceById.get(r.to_device_id);
+ const ct = r.preferred_cable_type_id != null ? cableTypeBy.get(r.preferred_cable_type_id) : null;
+ return `
+
+
+
+ ${escapeHtml(a?.name ?? "?")} ↔ ${escapeHtml(b?.name ?? "?")}
+ · ${escapeHtml(ct?.name ?? "solver picks")}
+ ${r.must_connect ? "must" : "nice"}
+
+ #${r.id}
+
+ ${r.notes ? `
${escapeHtml(r.notes)}
` : ""}
+
+ Edit
+ Delete
+
+
+ `;
+ }).join("") || `
No requirements yet.
`;
+ body.innerHTML = `
+
+ Requirements are the solver's input — "device A must connect to device B".
+ Add new ones from the per-device inspector (more contextual); manage them here.
+
+ ${rows}
+
+ + Add requirement
+ ${state.devices.length < 2
+ ? '(needs ≥ 2 devices) '
+ : ""}
+
+ `;
+ for (const row of body.querySelectorAll(".admin-row[data-req-id]")) {
+ const rid = Number(row.getAttribute("data-req-id"));
+ row.querySelector(".adm-edit").addEventListener("click", () => {
+ const r = state.requirements.find((x) => x.id === rid);
+ if (!r) return;
+ const dlg = $("#modal-admin");
+ dlg.close();
+ openRequirementModal(r);
+ });
+ row.querySelector(".adm-delete").addEventListener("click", async () => {
+ if (!confirm("Delete this requirement?")) return;
+ try {
+ await deleteRequirement(state.active.id, rid);
+ state.requirements = state.requirements.filter((r) => r.id !== rid);
+ switchAdminTab("requirements");
+ render();
+ } catch (e) { alert(`Delete failed: ${e.message}`); }
+ });
+ }
+ const newBtn = body.querySelector("#adm-req-new");
+ if (newBtn) {
+ newBtn.addEventListener("click", () => {
+ $("#modal-admin").close();
+ openRequirementModal(null);
+ });
+ }
+}
+
// ---------- boot ---------- //
async function boot() {
@@ -2809,11 +2863,6 @@ async function boot() {
$("#btn-add-type").addEventListener("click", () => openCableTypeModal(null));
$("#btn-delete-project").addEventListener("click", openDeleteProjectModal);
$("#btn-admin").addEventListener("click", openAdminModal);
- $("#btn-add-requirement").addEventListener("click", () => {
- if (!state.active) { alert("Pick a project first"); return; }
- if (state.devices.length < 2) { alert("Need at least two devices to add a requirement."); return; }
- openRequirementModal(null);
- });
$("#btn-solve").addEventListener("click", openSolveModal);
$("#btn-apply-template").addEventListener("click", openApplyTemplateModal);
$("#btn-export").addEventListener("click", exportCurrentProject);