graph: interactive liveliness (drag/pan/zoom, unlinked toggle, label colors)
Backend /api/graph now returns ALL non-trashed notes as nodes, each with its first label's color; edges unchanged. GraphView reworked: drag a node to reposition it (pins to cursor + reheats the sim), pan the background, wheel- zoom toward the cursor, a "Show unlinked notes" toggle (connected-only by default), a Reset view button, and nodes filled by label color. Click (a press without a drag) still opens the note; re-fetches on view open. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
+28
-11
@@ -6,6 +6,7 @@ from sqlalchemy.orm import aliased
|
||||
|
||||
from .auth import login_required
|
||||
from .db import session_scope
|
||||
from .models.label import Label, NoteLabel
|
||||
from .models.note import Note
|
||||
from .models.note_link import NoteLink
|
||||
|
||||
@@ -15,11 +16,13 @@ bp = Blueprint("graph", __name__, url_prefix="/api/graph")
|
||||
@bp.get("")
|
||||
@login_required
|
||||
async def get_graph():
|
||||
"""Wiki-link graph: nodes are the owner's connected notes, edges are resolved
|
||||
[[links]] (note_links.target_norm matched to a note's normalized title)."""
|
||||
"""Wiki-link graph. Nodes are ALL of the owner's non-trashed notes (the frontend
|
||||
toggles whether to show unlinked ones); each carries its first label's color for
|
||||
clustering. Edges are resolved [[links]] (note_links.target_norm matched to a
|
||||
note's normalized title)."""
|
||||
source = aliased(Note)
|
||||
target = aliased(Note)
|
||||
stmt = (
|
||||
edge_stmt = (
|
||||
select(source.id, target.id)
|
||||
.select_from(NoteLink)
|
||||
.join(source, source.id == NoteLink.source_id)
|
||||
@@ -33,9 +36,8 @@ async def get_graph():
|
||||
)
|
||||
)
|
||||
async with session_scope() as db:
|
||||
rows = (await db.execute(stmt)).all()
|
||||
rows = (await db.execute(edge_stmt)).all()
|
||||
edges = []
|
||||
node_ids: set = set()
|
||||
seen: set = set()
|
||||
for src_id, tgt_id in rows:
|
||||
key = (src_id, tgt_id)
|
||||
@@ -43,10 +45,25 @@ async def get_graph():
|
||||
continue
|
||||
seen.add(key)
|
||||
edges.append({"source": str(src_id), "target": str(tgt_id)})
|
||||
node_ids.add(src_id)
|
||||
node_ids.add(tgt_id)
|
||||
nodes = []
|
||||
if node_ids:
|
||||
note_rows = (await db.scalars(select(Note).where(Note.id.in_(node_ids)))).all()
|
||||
nodes = [{"id": str(n.id), "title": n.title or "Untitled"} for n in note_rows]
|
||||
|
||||
# First label color per note (labels ordered by name) → node color.
|
||||
color_rows = (
|
||||
await db.execute(
|
||||
select(NoteLabel.note_id, Label.color)
|
||||
.join(Label, Label.id == NoteLabel.label_id)
|
||||
.where(Label.owner_id == g.user_id)
|
||||
.order_by(Label.name)
|
||||
)
|
||||
).all()
|
||||
first_color: dict = {}
|
||||
for note_id, color in color_rows:
|
||||
first_color.setdefault(note_id, color)
|
||||
|
||||
note_rows = (
|
||||
await db.scalars(select(Note).where(Note.owner_id == g.user_id, Note.deleted_at.is_(None)))
|
||||
).all()
|
||||
nodes = [
|
||||
{"id": str(n.id), "title": n.title or "Untitled", "color": first_color.get(n.id, "default")}
|
||||
for n in note_rows
|
||||
]
|
||||
return jsonify({"nodes": nodes, "edges": edges})
|
||||
|
||||
Reference in New Issue
Block a user