From 072e620d567d65b48054873b40d8ebc561934bd3 Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 24 Apr 2026 20:02:19 -0400 Subject: [PATCH] Scope git commit to current diagram + fix complete JSON export/import - Git commit: "Current diagram only" checkbox stages only that diagram's JSON file instead of all diagrams - JSON export now includes: waypoints, twisted_pair, twist_pitch, shielded, show_size_label, bundle_id, bundle_label, sheet_id, sheets, wire_bundles - JSON import reconstructs sheets and bundles with remapped IDs so restored diagrams are exact copies Co-Authored-By: Claude Sonnet 4.6 --- backend/app/routers/diagrams.py | 38 +++++++++++++++-- backend/app/routers/export.py | 23 +++++++++- backend/app/routers/git_mgr.py | 74 ++++++++++++++++++++++++++++----- frontend/index.html | 7 +++- frontend/js/api.js | 2 +- frontend/js/app.js | 7 +++- 6 files changed, 130 insertions(+), 21 deletions(-) diff --git a/backend/app/routers/diagrams.py b/backend/app/routers/diagrams.py index 3172353..c38471f 100644 --- a/backend/app/routers/diagrams.py +++ b/backend/app/routers/diagrams.py @@ -119,10 +119,38 @@ async def import_diagram(request: Request, db: Session = Depends(get_db)): db.add(new_diag) db.flush() - id_map = {} + # Sheets: old id → new Sheet object + sheet_map = {} + for s in data.get("sheets", []): + ns = models.Sheet( + diagram_id=new_diag.id, + name=s.get("name", "Sheet 1"), + position=s.get("position", 0), + ) + db.add(ns) + db.flush() + if "id" in s: + sheet_map[s["id"]] = ns.id + + # Wire bundles: old id → new bundle id + bundle_map = {} + for b in data.get("wire_bundles", []): + nb = models.WireBundle( + diagram_id=new_diag.id, + label=b.get("label", ""), + jacket_color=b.get("jacket_color", "#2a2a2a"), + ) + db.add(nb) + db.flush() + if "id" in b: + bundle_map[b["id"]] = nb.id + + # Devices: old id → new device id + dev_map = {} for dev_data in data.get("devices", []): new_dev = models.Device( diagram_id=new_diag.id, + sheet_id=sheet_map.get(dev_data.get("sheet_id")), device_type=dev_data.get("device_type", "generic"), label=dev_data.get("label", ""), reference=dev_data.get("reference", ""), @@ -136,15 +164,16 @@ async def import_diagram(request: Request, db: Session = Depends(get_db)): db.add(new_dev) db.flush() if "id" in dev_data: - id_map[dev_data["id"]] = new_dev.id + dev_map[dev_data["id"]] = new_dev.id for wire_data in data.get("wires", []): new_wire = models.Wire( diagram_id=new_diag.id, + sheet_id=sheet_map.get(wire_data.get("sheet_id")), label=wire_data.get("label", ""), - from_device_id=id_map.get(wire_data.get("from_device_id")), + from_device_id=dev_map.get(wire_data.get("from_device_id")), from_pin=wire_data.get("from_pin", ""), - to_device_id=id_map.get(wire_data.get("to_device_id")), + to_device_id=dev_map.get(wire_data.get("to_device_id")), to_pin=wire_data.get("to_pin", ""), color_primary=wire_data.get("color_primary", "#FF0000"), color_stripe=wire_data.get("color_stripe"), @@ -157,6 +186,7 @@ async def import_diagram(request: Request, db: Session = Depends(get_db)): twist_pitch=wire_data.get("twist_pitch", 16.0), shielded=wire_data.get("shielded", False), show_size_label=wire_data.get("show_size_label", False), + bundle_id=bundle_map.get(wire_data.get("bundle_id")), ) db.add(new_wire) diff --git a/backend/app/routers/export.py b/backend/app/routers/export.py index 7344e89..ec6190d 100644 --- a/backend/app/routers/export.py +++ b/backend/app/routers/export.py @@ -283,13 +283,25 @@ def export_json(diagram_id: int, db: Session = Depends(get_db)): if not diagram: raise HTTPException(status_code=404, detail="Diagram not found") + # Build bundle id→label map for wire export + bundle_label = {b.id: b.label for b in diagram.wire_bundles} + data = { "id": diagram.id, "name": diagram.name, "description": diagram.description, + "sheets": [ + {"id": s.id, "name": s.name, "position": s.position} + for s in diagram.sheets + ], + "wire_bundles": [ + {"id": b.id, "label": b.label, "jacket_color": b.jacket_color} + for b in diagram.wire_bundles + ], "devices": [ { - "id": d.id, "device_type": d.device_type, "label": d.label, + "id": d.id, "sheet_id": d.sheet_id, + "device_type": d.device_type, "label": d.label, "reference": d.reference, "x": d.x, "y": d.y, "width": d.width, "height": d.height, "properties": d.properties, "pins": d.pins, @@ -298,12 +310,19 @@ def export_json(diagram_id: int, db: Session = Depends(get_db)): ], "wires": [ { - "id": w.id, "label": w.label, + "id": w.id, "sheet_id": w.sheet_id, "label": w.label, "from_device_id": w.from_device_id, "from_pin": w.from_pin, "to_device_id": w.to_device_id, "to_pin": w.to_pin, "color_primary": w.color_primary, "color_stripe": w.color_stripe, "gauge": w.gauge, "length": w.length, "length_unit": w.length_unit, "notes": w.notes, + "waypoints": w.waypoints or [], + "twisted_pair": bool(w.twisted_pair), + "twist_pitch": w.twist_pitch, + "shielded": bool(w.shielded), + "show_size_label": bool(w.show_size_label), + "bundle_id": w.bundle_id, + "bundle_label": bundle_label.get(w.bundle_id), } for w in diagram.wires ], diff --git a/backend/app/routers/git_mgr.py b/backend/app/routers/git_mgr.py index 4a928c3..c7f50ad 100644 --- a/backend/app/routers/git_mgr.py +++ b/backend/app/routers/git_mgr.py @@ -42,13 +42,23 @@ def _safe_name(diagram_id: int, name: str) -> str: def _diagram_to_dict(diagram, db: Session) -> dict: + bundle_label = {b.id: b.label for b in diagram.wire_bundles} return { "id": diagram.id, "name": diagram.name, "description": diagram.description or "", + "sheets": [ + {"id": s.id, "name": s.name, "position": s.position} + for s in diagram.sheets + ], + "wire_bundles": [ + {"id": b.id, "label": b.label, "jacket_color": b.jacket_color} + for b in diagram.wire_bundles + ], "devices": [ { - "id": d.id, "device_type": d.device_type, "label": d.label, + "id": d.id, "sheet_id": d.sheet_id, + "device_type": d.device_type, "label": d.label, "reference": d.reference, "x": d.x, "y": d.y, "width": d.width, "height": d.height, "properties": d.properties, "pins": d.pins, @@ -57,7 +67,7 @@ def _diagram_to_dict(diagram, db: Session) -> dict: ], "wires": [ { - "id": w.id, "label": w.label, + "id": w.id, "sheet_id": w.sheet_id, "label": w.label, "from_device_id": w.from_device_id, "from_pin": w.from_pin, "to_device_id": w.to_device_id, "to_pin": w.to_pin, "color_primary": w.color_primary, "color_stripe": w.color_stripe, @@ -65,6 +75,7 @@ def _diagram_to_dict(diagram, db: Session) -> dict: "notes": w.notes, "waypoints": w.waypoints or [], "twisted_pair": bool(w.twisted_pair), "twist_pitch": w.twist_pitch, "shielded": bool(w.shielded), "show_size_label": bool(w.show_size_label), + "bundle_id": w.bundle_id, "bundle_label": bundle_label.get(w.bundle_id), } for w in diagram.wires ], @@ -78,10 +89,36 @@ def _import_data(data: dict, db: Session, name_override: Optional[str] = None) - ) db.add(new_diag) db.flush() - id_map = {} + + sheet_map = {} + for s in data.get("sheets", []): + ns = models.Sheet( + diagram_id=new_diag.id, + name=s.get("name", "Sheet 1"), + position=s.get("position", 0), + ) + db.add(ns) + db.flush() + if "id" in s: + sheet_map[s["id"]] = ns.id + + bundle_map = {} + for b in data.get("wire_bundles", []): + nb = models.WireBundle( + diagram_id=new_diag.id, + label=b.get("label", ""), + jacket_color=b.get("jacket_color", "#2a2a2a"), + ) + db.add(nb) + db.flush() + if "id" in b: + bundle_map[b["id"]] = nb.id + + dev_map = {} for dev in data.get("devices", []): nd = models.Device( diagram_id=new_diag.id, + sheet_id=sheet_map.get(dev.get("sheet_id")), device_type=dev.get("device_type", "generic"), label=dev.get("label", ""), reference=dev.get("reference", ""), @@ -93,14 +130,16 @@ def _import_data(data: dict, db: Session, name_override: Optional[str] = None) - db.add(nd) db.flush() if "id" in dev: - id_map[dev["id"]] = nd.id + dev_map[dev["id"]] = nd.id + for w in data.get("wires", []): nw = models.Wire( diagram_id=new_diag.id, + sheet_id=sheet_map.get(w.get("sheet_id")), label=w.get("label", ""), - from_device_id=id_map.get(w.get("from_device_id")), + from_device_id=dev_map.get(w.get("from_device_id")), from_pin=w.get("from_pin", ""), - to_device_id=id_map.get(w.get("to_device_id")), + to_device_id=dev_map.get(w.get("to_device_id")), to_pin=w.get("to_pin", ""), color_primary=w.get("color_primary", "#FF0000"), color_stripe=w.get("color_stripe"), @@ -113,8 +152,10 @@ def _import_data(data: dict, db: Session, name_override: Optional[str] = None) - twist_pitch=w.get("twist_pitch", 16.0), shielded=w.get("shielded", False), show_size_label=w.get("show_size_label", False), + bundle_id=bundle_map.get(w.get("bundle_id")), ) db.add(nw) + db.commit() db.refresh(new_diag) return new_diag @@ -162,6 +203,7 @@ def git_log(): class CommitBody(BaseModel): message: str + diagram_id: Optional[int] = None # None = all diagrams @router.post("/commit") @@ -171,14 +213,24 @@ def git_commit(body: CommitBody, db: Session = Depends(get_db)): repo_root = _repo_root() diagrams_dir = _diagrams_dir(repo_root) - # Export all diagrams to diagrams/ folder - diagrams = db.query(models.Diagram).all() - for diagram in diagrams: + if body.diagram_id is not None: + # Export only the specified diagram + diagram = db.query(models.Diagram).filter(models.Diagram.id == body.diagram_id).first() + if not diagram: + raise HTTPException(404, "Diagram not found") + to_export = [diagram] + else: + to_export = db.query(models.Diagram).all() + + exported_files = [] + for diagram in to_export: data = _diagram_to_dict(diagram, db) fname = _safe_name(diagram.id, diagram.name) (diagrams_dir / fname).write_text(json.dumps(data, indent=2), encoding="utf-8") + exported_files.append(f"diagrams/{fname}") - add_r = _run("add", "diagrams/", cwd=repo_root) + # Stage only the exported files (not the whole diagrams/ dir) + add_r = _run("add", *exported_files, cwd=repo_root) if add_r.returncode != 0: raise HTTPException(500, f"git add failed: {add_r.stderr}") @@ -186,7 +238,7 @@ def git_commit(body: CommitBody, db: Session = Depends(get_db)): if commit_r.returncode != 0: out = commit_r.stdout + commit_r.stderr if "nothing to commit" in out: - return {"status": "nothing_to_commit", "output": "Nothing to commit — diagrams unchanged since last commit"} + return {"status": "nothing_to_commit", "output": "Nothing to commit — diagram unchanged since last commit"} raise HTTPException(500, f"git commit failed: {commit_r.stderr}") return {"status": "committed", "output": commit_r.stdout.strip()} diff --git a/frontend/index.html b/frontend/index.html index ca536c5..b0ec60c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -430,7 +430,12 @@
- +
+ + +
diff --git a/frontend/js/api.js b/frontend/js/api.js index 50b8b64..3bb63c2 100644 --- a/frontend/js/api.js +++ b/frontend/js/api.js @@ -63,7 +63,7 @@ const api = { git: { status: () => apiFetch("/git/status"), log: () => apiFetch("/git/log"), - commit: (message) => apiFetch("/git/commit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message }) }), + commit: (message, diagram_id) => apiFetch("/git/commit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message, diagram_id: diagram_id ?? null }) }), push: () => apiFetch("/git/push", { method: "POST" }), history: (hash) => apiFetch(`/git/history/${hash}`), restore: (commit_hash, filepath, name) => apiFetch("/git/restore", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ commit_hash, filepath, name }) }), diff --git a/frontend/js/app.js b/frontend/js/app.js index 98cc6a5..25247a9 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -2079,12 +2079,15 @@ class WiringApp { async _gitCommit() { const msg = document.getElementById("git-commit-msg")?.value?.trim(); if (!msg) { alert("Enter a commit message first."); return; } + const currentOnly = document.getElementById("git-scope-current")?.checked; + if (currentOnly && !this.diagramId) { alert("No diagram is currently open."); return; } + const diagramId = currentOnly ? this.diagramId : null; const status = document.getElementById("git-op-status"); status.textContent = "Committing…"; status.style.color = "#aabb88"; try { - const r = await api.git.commit(msg); + const r = await api.git.commit(msg, diagramId); if (r.status === "nothing_to_commit") { - status.textContent = "Nothing to commit — diagrams unchanged."; + status.textContent = "Nothing to commit — diagram unchanged."; status.style.color = "#8899bb"; } else { status.textContent = "✓ Committed: " + r.output.split("\n")[0];