13 KiB
Make the project dependency graph behave like the approved prototype
Context
This branch (uncommitted work) adds a project-level dependency graph view to n8n: a canvas
showing the workflows and folders in a project, connected by their cross-workflow
relationships. A standalone HTML prototype of the intended interaction design lives at
./project-graph-prototype.html in the repo root (open it directly in a browser; do not
commit it). It is the source of truth for behavior and feel. Your job is to bring the
branch's Vue implementation up to parity with that prototype.
What exists on this branch
Backend (mostly fine, two small additions needed — see "Backend changes"):
packages/cli/src/modules/project-dependency-graph/project-dependency-graph.service.ts+ controller —GET /projects/:projectId/dependency-graph?folderId&explode&draft&relationshipTypesreturns{ nodes, edges, members, variables, stats }scoped to one folder level (folderId=0= project root). Workflow→workflow edges:calls-workflow,uses-as-tool,handles-errors-for. Referenced workflows outside the scoped folder are included as nodes withexpanded: false.packages/@n8n/api-types/src/schemas/project-dependency-graph.schema.ts— types.
Frontend (packages/frontend/editor-ui/src/features/projects/canvas/):
ProjectCanvas.vue— vue-flow canvas replacingWorkflowsViewon theprojects/:projectId/workflowsandprojects/:projectId/folders/:folderId?/workflowsroutes (features/collaboration/projects/projects.routes.ts). Merges all cached folder graphs, rebuilds nodes/edges, full dagre re-layout +fitViewon every change.composables/useProjectDependencyGraph.ts— per-folder fetch cache +expandedFoldersset.composables/useProjectCanvasLayout.ts— dagre LR layout, every node laid out flat.components/ProjectCanvasNode.vue— card with generic workflow icon, Active/Inactive badge, Creds/Tables/Tags count badges; folder card with chevron. Handles:target-left,source-right,source-bottom(no top target handle).components/ProjectCanvasEdge.vue— calls = orange bezier, tool = violet dashed smoothstep.
Problems with the current behavior (all observed in code)
- No folder aggregation. Workflows inside collapsed folders leak onto the canvas as the server's "referenced" ghost nodes, and edges point at them directly. Folders and their contents float as disconnected flat nodes — folder membership is invisible.
- Expanding a folder just dumps more flat nodes and re-runs dagre over everything; every expand/collapse/filter toggle scrambles all positions and re-fits the viewport.
- Tool edges are wired wrong:
targetHandle: 'source-right'— they enter the target's right side (and via a handle of typesource). The prototype's rule is bottom-of-source → top-of-target. There is no top handle on the node component at all. - No containers, no animation, no overlap handling, no drag-to-file, no hover highlighting, generic node icons, and edges have no arrowheads.
- Missing edges from a stale dependency index. The draft/published filter is already
handled (
getGraphpassesoptions.draftthrough untouched;undefined= both draft and published rows inWorkflowDependencyQueryService). ButaiToolWorkflowCallis a brand-new dependency type on this branch, and the indexer only writes rows on workflow save/activate events or for workflows the startup backfill considers "needing indexing". Workflows indexed before this branch never re-index, souses-as-tooledges are missing at the source (a dev database can have literally zeroaiToolWorkflowCallrows while showing plenty of other dependency rows). Make the backfill invalidate on indexer version — bump a version constant stored with theworkflowIndexedmarker sobuildIndexreprocesses every workflow once after the extraction logic changes — and verify with:SELECT "dependencyType", COUNT(*) FROM workflow_dependency GROUP BY 1;
Target behavior (match project-graph-prototype.html exactly unless noted)
1. Folders aggregate their contents
- Default view: only root-level workflows and root-level folder nodes. Nothing inside a collapsed folder appears on canvas.
- Any edge whose endpoint is hidden inside a collapsed folder re-routes to that workflow's outermost collapsed ancestor folder, deduplicated per (visibleSource, visibleTarget, type). Edges whose endpoints resolve to the same visible node are dropped.
- Collapsed folder card shows a folder icon, name, "N workflows inside" subtitle, and a count pill (N = descendant workflow count, recursive).
2. Expand / collapse in place
- Clicking a folder expands it in place into a dashed rounded container with a header (uppercase folder name, workflow count, collapse affordance). Children animate outward from the folder's position into a wrap-grid inside the container (4 per row).
- Edges immediately re-resolve to the real child workflows, including edges that cross the container boundary. Internal edges render between children.
- Nested folders render as folder nodes inside the parent container and expand into nested containers. Collapsing a folder auto-collapses its expanded descendants, animates direct children back into a collapsed folder node placed at the container's origin.
- The vue-flow-idiomatic mapping is sub-flows: container = a non-draggable group/parent
node, children with
parentNodeset and relative positions; z-order parents behind children. If group nodes fight the animation model, absolutely-positioned overlay containers (as in the prototype: container bbox derived each frame from child rects + padding + header) are acceptable.
3. Overlap resolution instead of global re-layout
- Never re-run the full layout after the initial render. Positions are stable; expansion pushes neighbours out of the way, collapse leaves them where they are.
- On expand, compute the container's final rect, then push every other same-level unit
(workflow cards, collapsed folder cards, other containers — a container moves with all
its children as one unit) clear of it with ~40px clearance, along the axis of minimal
displacement. Iterate to convergence (cap ~40 passes): clear overlaps with the fixed
(newly expanded) rect first each pass, then separate movable pairs by splitting the push
between both; finish with a guaranteed sweep so nothing remains under the expanded
container. For nested expansion, resolve at each ancestor level (siblings inside the
parent container first, then the grown parent container against root-level units).
Port this directly from
resolveLevel/resolveAround/separationVectorin the prototype — the naive "move one node per pass" version oscillates and fails to converge. - All movement animates (~260 ms cubic ease-out). Add a completion watchdog per tween (setTimeout at duration + ~120 ms that snaps to target and fires the callback if the rAF loop was throttled) — background/embedded tabs otherwise leave moves half-applied.
4. Initial automatic layout (replace flat dagre-over-everything)
- Lay out root-level units only (root workflows + root folder nodes), using the folder-aggregated edge set. Dagre LR with the aggregated edges may be acceptable, but match the prototype's rules: columns = longest path along trigger edges only (left → right); a node referenced only by tool edges sits in its caller's column, below it; order within a column by barycenter of already-placed neighbours.
- Units with no edges go into a separate grid (5 per row) below the connected graph.
fitViewonce on first load only — never on expand/collapse/filter changes.- Nodes are never freely draggable (
nodes-draggablestays effectively false; see §7).
5. Edges
calls-workflow("Triggered by"): solid, muted grey (not orange), bezier, from right of source to left of target, small arrowhead at the target (vue-flowmarkerEnd). n8n token: something like--color-foreground-dark-ish muted grey; the prototype uses#a8a294.uses-as-tool: violet dashed bezier, from bottom of source to top of target (add atarget-tophandle of typetargetto the node component and use it), arrowhead at the target. Keephandles-errors-foron the trigger styling for now.- No edge labels of any kind.
- Hovering a node highlights its connected edges (thicker/darker) and dims all others (~0.18 opacity); leaving clears it.
6. Node cards (n8n visual language, per the prototype)
- Workflow card: 38px rounded icon tile + name (semibold, ellipsized) + subtitle row of
status dot (green when
active) and the trigger type label. - Icon tile per trigger type: Chat/Webhook/Slack → brand-tinted, Schedule/Form → teal,
Error → amber, MCP → violet, Sub-workflow/Manual → grey. A workflow that is the target
of any
uses-as-tooledge overrides to the violet tool icon and "Tool workflow" label. The backend must supply the trigger type (see "Backend changes"); until it does, derive what's available and default to Sub-workflow. - Drop the Creds/Tables/Tags badges from the card for this view (keep
DependencyPillwork untouched elsewhere). - Folder card: dashed border, amber folder icon tile, count pill in the top-right corner.
- Keep the existing behavior that clicking a workflow card navigates to that workflow; folders toggle expand/collapse. (Prototype has no navigation; this is a deliberate keep.)
7. Drag = filing only (no free repositioning)
- Dragging a workflow card lifts it (shadow, follows cursor, its edges follow live; the source container's bbox ignores it while lifted).
- Valid drop targets highlight as the cursor moves: collapsed folder cards take priority,
otherwise the deepest expanded container under the cursor. Dropping:
- on a collapsed folder → card glides into the folder node and disappears; count pill, edge re-routing, everything updates;
- into an expanded container → the container's grid re-packs with the workflow slotted in, and overlap resolution runs;
- from inside a container onto empty canvas → moves the workflow to project root at the drop point; the source container re-packs (a folder emptied this way collapses away);
- anything else (root card onto empty canvas, onto another workflow, back into its own folder) → snaps back to its position. Free repositioning must be impossible.
- Persist the move: call the existing workflow update/move API (the same endpoint the
workflows list uses to move a workflow between folders — look at how
WorkflowsView/folder move actions do it) with the newparentFolderId, optimistic UI, toast + revert on failure. - Folders are not draggable.
8. Keep from the current branch
- The relationship-type filter checkboxes (calls/tool/error) — they compose fine with aggregation; re-resolve visible edges when toggled, without re-layout.
- Back-to-projects toolbar, loading/empty states, MiniMap/Controls, route wiring.
- The per-folder fetch cache in
useProjectDependencyGraph(lazy fetch on expand is right).
Backend changes (small, in project-dependency-graph.service.ts + schema)
- Referenced workflow nodes need
metadata.parentFolderId(and ideally the full ancestor folder chain up to the scoped root, e.g.metadata.folderPath: string[]). Without it the client cannot re-route an edge to the correct collapsed folder — today referenced nodes have emptymetadata. This is the one blocking backend gap. - Folder nodes need a descendant workflow count (
metadata.workflowCount) for count pills, computed recursively over the folder hierarchy. - Workflow nodes should carry a
metadata.triggerType(one of: chat, webhook, schedule, slack, error, form, mcp, subworkflow, manual, none) derived from the workflow's trigger nodes, so the card icons work. If that's too costly here, put it behind the existing workflow-index data and default the UI gracefully.
Verification
pnpm --filter n8n-editor-ui typecheck && pnpm --filter n8n-editor-ui lintclean; build the cli package if backend types changed.- Run the app (
pnpm dev), open a project with folders (including one nested folder) and cross-folder trigger + tool relationships, and walk the whole matrix againstproject-graph-prototype.htmlside by side: root aggregation and edge dedup → expand each folder (neighbours pushed, nothing overlaps, edges reconnect to children) → nested expand → collapse (descendants auto-collapse) → hover highlighting → all four drag outcomes (into collapsed folder, into open container, out to root, snap-back) including persistence after reload → filter toggles → confirm positions never globally re-shuffle and fitView only fires once. - Add/update component tests for: edge re-routing to collapsed ancestors (incl. nested), edge dedup, overlap-resolution convergence (no unit overlaps the fixed rect after resolve), drag-drop target resolution (collapsed folder beats container, deepest container wins), and snap-back.
Work through this incrementally — aggregation + expand/collapse first, then layout
stability, then drag-to-file — keeping the branch compiling at each step. Do not commit
project-graph-prototype.html or this prompt file.