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 <noreply@anthropic.com>
This commit is contained in:
@@ -119,10 +119,38 @@ async def import_diagram(request: Request, db: Session = Depends(get_db)):
|
|||||||
db.add(new_diag)
|
db.add(new_diag)
|
||||||
db.flush()
|
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", []):
|
for dev_data in data.get("devices", []):
|
||||||
new_dev = models.Device(
|
new_dev = models.Device(
|
||||||
diagram_id=new_diag.id,
|
diagram_id=new_diag.id,
|
||||||
|
sheet_id=sheet_map.get(dev_data.get("sheet_id")),
|
||||||
device_type=dev_data.get("device_type", "generic"),
|
device_type=dev_data.get("device_type", "generic"),
|
||||||
label=dev_data.get("label", ""),
|
label=dev_data.get("label", ""),
|
||||||
reference=dev_data.get("reference", ""),
|
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.add(new_dev)
|
||||||
db.flush()
|
db.flush()
|
||||||
if "id" in dev_data:
|
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", []):
|
for wire_data in data.get("wires", []):
|
||||||
new_wire = models.Wire(
|
new_wire = models.Wire(
|
||||||
diagram_id=new_diag.id,
|
diagram_id=new_diag.id,
|
||||||
|
sheet_id=sheet_map.get(wire_data.get("sheet_id")),
|
||||||
label=wire_data.get("label", ""),
|
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", ""),
|
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", ""),
|
to_pin=wire_data.get("to_pin", ""),
|
||||||
color_primary=wire_data.get("color_primary", "#FF0000"),
|
color_primary=wire_data.get("color_primary", "#FF0000"),
|
||||||
color_stripe=wire_data.get("color_stripe"),
|
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),
|
twist_pitch=wire_data.get("twist_pitch", 16.0),
|
||||||
shielded=wire_data.get("shielded", False),
|
shielded=wire_data.get("shielded", False),
|
||||||
show_size_label=wire_data.get("show_size_label", 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)
|
db.add(new_wire)
|
||||||
|
|
||||||
|
|||||||
@@ -283,13 +283,25 @@ def export_json(diagram_id: int, db: Session = Depends(get_db)):
|
|||||||
if not diagram:
|
if not diagram:
|
||||||
raise HTTPException(status_code=404, detail="Diagram not found")
|
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 = {
|
data = {
|
||||||
"id": diagram.id,
|
"id": diagram.id,
|
||||||
"name": diagram.name,
|
"name": diagram.name,
|
||||||
"description": diagram.description,
|
"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": [
|
"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,
|
"reference": d.reference, "x": d.x, "y": d.y,
|
||||||
"width": d.width, "height": d.height,
|
"width": d.width, "height": d.height,
|
||||||
"properties": d.properties, "pins": d.pins,
|
"properties": d.properties, "pins": d.pins,
|
||||||
@@ -298,12 +310,19 @@ def export_json(diagram_id: int, db: Session = Depends(get_db)):
|
|||||||
],
|
],
|
||||||
"wires": [
|
"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,
|
"from_device_id": w.from_device_id, "from_pin": w.from_pin,
|
||||||
"to_device_id": w.to_device_id, "to_pin": w.to_pin,
|
"to_device_id": w.to_device_id, "to_pin": w.to_pin,
|
||||||
"color_primary": w.color_primary, "color_stripe": w.color_stripe,
|
"color_primary": w.color_primary, "color_stripe": w.color_stripe,
|
||||||
"gauge": w.gauge, "length": w.length, "length_unit": w.length_unit,
|
"gauge": w.gauge, "length": w.length, "length_unit": w.length_unit,
|
||||||
"notes": w.notes,
|
"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
|
for w in diagram.wires
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -42,13 +42,23 @@ def _safe_name(diagram_id: int, name: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _diagram_to_dict(diagram, db: Session) -> dict:
|
def _diagram_to_dict(diagram, db: Session) -> dict:
|
||||||
|
bundle_label = {b.id: b.label for b in diagram.wire_bundles}
|
||||||
return {
|
return {
|
||||||
"id": diagram.id,
|
"id": diagram.id,
|
||||||
"name": diagram.name,
|
"name": diagram.name,
|
||||||
"description": diagram.description or "",
|
"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": [
|
"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,
|
"reference": d.reference, "x": d.x, "y": d.y,
|
||||||
"width": d.width, "height": d.height,
|
"width": d.width, "height": d.height,
|
||||||
"properties": d.properties, "pins": d.pins,
|
"properties": d.properties, "pins": d.pins,
|
||||||
@@ -57,7 +67,7 @@ def _diagram_to_dict(diagram, db: Session) -> dict:
|
|||||||
],
|
],
|
||||||
"wires": [
|
"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,
|
"from_device_id": w.from_device_id, "from_pin": w.from_pin,
|
||||||
"to_device_id": w.to_device_id, "to_pin": w.to_pin,
|
"to_device_id": w.to_device_id, "to_pin": w.to_pin,
|
||||||
"color_primary": w.color_primary, "color_stripe": w.color_stripe,
|
"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 [],
|
"notes": w.notes, "waypoints": w.waypoints or [],
|
||||||
"twisted_pair": bool(w.twisted_pair), "twist_pitch": w.twist_pitch,
|
"twisted_pair": bool(w.twisted_pair), "twist_pitch": w.twist_pitch,
|
||||||
"shielded": bool(w.shielded), "show_size_label": bool(w.show_size_label),
|
"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
|
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.add(new_diag)
|
||||||
db.flush()
|
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", []):
|
for dev in data.get("devices", []):
|
||||||
nd = models.Device(
|
nd = models.Device(
|
||||||
diagram_id=new_diag.id,
|
diagram_id=new_diag.id,
|
||||||
|
sheet_id=sheet_map.get(dev.get("sheet_id")),
|
||||||
device_type=dev.get("device_type", "generic"),
|
device_type=dev.get("device_type", "generic"),
|
||||||
label=dev.get("label", ""),
|
label=dev.get("label", ""),
|
||||||
reference=dev.get("reference", ""),
|
reference=dev.get("reference", ""),
|
||||||
@@ -93,14 +130,16 @@ def _import_data(data: dict, db: Session, name_override: Optional[str] = None) -
|
|||||||
db.add(nd)
|
db.add(nd)
|
||||||
db.flush()
|
db.flush()
|
||||||
if "id" in dev:
|
if "id" in dev:
|
||||||
id_map[dev["id"]] = nd.id
|
dev_map[dev["id"]] = nd.id
|
||||||
|
|
||||||
for w in data.get("wires", []):
|
for w in data.get("wires", []):
|
||||||
nw = models.Wire(
|
nw = models.Wire(
|
||||||
diagram_id=new_diag.id,
|
diagram_id=new_diag.id,
|
||||||
|
sheet_id=sheet_map.get(w.get("sheet_id")),
|
||||||
label=w.get("label", ""),
|
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", ""),
|
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", ""),
|
to_pin=w.get("to_pin", ""),
|
||||||
color_primary=w.get("color_primary", "#FF0000"),
|
color_primary=w.get("color_primary", "#FF0000"),
|
||||||
color_stripe=w.get("color_stripe"),
|
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),
|
twist_pitch=w.get("twist_pitch", 16.0),
|
||||||
shielded=w.get("shielded", False),
|
shielded=w.get("shielded", False),
|
||||||
show_size_label=w.get("show_size_label", False),
|
show_size_label=w.get("show_size_label", False),
|
||||||
|
bundle_id=bundle_map.get(w.get("bundle_id")),
|
||||||
)
|
)
|
||||||
db.add(nw)
|
db.add(nw)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(new_diag)
|
db.refresh(new_diag)
|
||||||
return new_diag
|
return new_diag
|
||||||
@@ -162,6 +203,7 @@ def git_log():
|
|||||||
|
|
||||||
class CommitBody(BaseModel):
|
class CommitBody(BaseModel):
|
||||||
message: str
|
message: str
|
||||||
|
diagram_id: Optional[int] = None # None = all diagrams
|
||||||
|
|
||||||
|
|
||||||
@router.post("/commit")
|
@router.post("/commit")
|
||||||
@@ -171,14 +213,24 @@ def git_commit(body: CommitBody, db: Session = Depends(get_db)):
|
|||||||
repo_root = _repo_root()
|
repo_root = _repo_root()
|
||||||
diagrams_dir = _diagrams_dir(repo_root)
|
diagrams_dir = _diagrams_dir(repo_root)
|
||||||
|
|
||||||
# Export all diagrams to diagrams/ folder
|
if body.diagram_id is not None:
|
||||||
diagrams = db.query(models.Diagram).all()
|
# Export only the specified diagram
|
||||||
for diagram in diagrams:
|
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)
|
data = _diagram_to_dict(diagram, db)
|
||||||
fname = _safe_name(diagram.id, diagram.name)
|
fname = _safe_name(diagram.id, diagram.name)
|
||||||
(diagrams_dir / fname).write_text(json.dumps(data, indent=2), encoding="utf-8")
|
(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:
|
if add_r.returncode != 0:
|
||||||
raise HTTPException(500, f"git add failed: {add_r.stderr}")
|
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:
|
if commit_r.returncode != 0:
|
||||||
out = commit_r.stdout + commit_r.stderr
|
out = commit_r.stdout + commit_r.stderr
|
||||||
if "nothing to commit" in out:
|
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}")
|
raise HTTPException(500, f"git commit failed: {commit_r.stderr}")
|
||||||
|
|
||||||
return {"status": "committed", "output": commit_r.stdout.strip()}
|
return {"status": "committed", "output": commit_r.stdout.strip()}
|
||||||
|
|||||||
+6
-1
@@ -430,7 +430,12 @@
|
|||||||
|
|
||||||
<!-- Commit section -->
|
<!-- Commit section -->
|
||||||
<div class="git-section">
|
<div class="git-section">
|
||||||
<label class="git-section-title">Commit all diagrams</label>
|
<div style="display:flex;align-items:center;justify-content:space-between">
|
||||||
|
<label class="git-section-title">Commit diagrams</label>
|
||||||
|
<label style="font-size:11px;color:#8899bb;display:flex;align-items:center;gap:5px;cursor:pointer">
|
||||||
|
<input type="checkbox" id="git-scope-current"> Current diagram only
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div style="display:flex;gap:6px;align-items:flex-start">
|
<div style="display:flex;gap:6px;align-items:flex-start">
|
||||||
<textarea id="git-commit-msg" placeholder="Describe what changed…" rows="2"
|
<textarea id="git-commit-msg" placeholder="Describe what changed…" rows="2"
|
||||||
style="flex:1;resize:vertical;font-size:12px;padding:5px;background:#0d0d20;color:#ccd;border:1px solid #2a2a44;border-radius:4px"></textarea>
|
style="flex:1;resize:vertical;font-size:12px;padding:5px;background:#0d0d20;color:#ccd;border:1px solid #2a2a44;border-radius:4px"></textarea>
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ const api = {
|
|||||||
git: {
|
git: {
|
||||||
status: () => apiFetch("/git/status"),
|
status: () => apiFetch("/git/status"),
|
||||||
log: () => apiFetch("/git/log"),
|
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" }),
|
push: () => apiFetch("/git/push", { method: "POST" }),
|
||||||
history: (hash) => apiFetch(`/git/history/${hash}`),
|
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 }) }),
|
restore: (commit_hash, filepath, name) => apiFetch("/git/restore", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ commit_hash, filepath, name }) }),
|
||||||
|
|||||||
+5
-2
@@ -2079,12 +2079,15 @@ class WiringApp {
|
|||||||
async _gitCommit() {
|
async _gitCommit() {
|
||||||
const msg = document.getElementById("git-commit-msg")?.value?.trim();
|
const msg = document.getElementById("git-commit-msg")?.value?.trim();
|
||||||
if (!msg) { alert("Enter a commit message first."); return; }
|
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");
|
const status = document.getElementById("git-op-status");
|
||||||
status.textContent = "Committing…"; status.style.color = "#aabb88";
|
status.textContent = "Committing…"; status.style.color = "#aabb88";
|
||||||
try {
|
try {
|
||||||
const r = await api.git.commit(msg);
|
const r = await api.git.commit(msg, diagramId);
|
||||||
if (r.status === "nothing_to_commit") {
|
if (r.status === "nothing_to_commit") {
|
||||||
status.textContent = "Nothing to commit — diagrams unchanged.";
|
status.textContent = "Nothing to commit — diagram unchanged.";
|
||||||
status.style.color = "#8899bb";
|
status.style.color = "#8899bb";
|
||||||
} else {
|
} else {
|
||||||
status.textContent = "✓ Committed: " + r.output.split("\n")[0];
|
status.textContent = "✓ Committed: " + r.output.split("\n")[0];
|
||||||
|
|||||||
Reference in New Issue
Block a user