Skip to content

fix(workflow): stop a nested block jumping when it leaves its container - #6644

Merged
waleedlatif1 merged 2 commits into
stagingfrom
fix/nested-absolute-position
Aug 12, 2026
Merged

fix(workflow): stop a nested block jumping when it leaves its container#6644
waleedlatif1 merged 2 commits into
stagingfrom
fix/nested-absolute-position

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

getNodeAbsolutePosition added the container's header and padding to a child's position. Those are already in the position — React Flow places a child at its parent's origin plus its own coordinates, and clampPositionToContainer is what holds it clear of the chrome, flooring it at LEFT_PADDING and HEADER_HEIGHT + TOP_PADDING. Counting them twice reported every nested node 16px right and 66px below where it actually renders.

What that looks like:

  • A block drops down-right the moment it is dragged out of a Loop.
  • A block dragged into a Loop from the canvas lands off-target.
  • Container hit-testing during a drag, and the bounds fitView focuses on, are both skewed for nested nodes.

Two callers already knew something was wrong: each subtracted the same three constants straight back off to recover a relative position. They now take the difference of two absolutes, which is what a relative position is.

A third place had its own copy — React Flow's child extent, with a fourth distinct header height (42, against the 40 the card renders and the 50 this helper used). It now reads the same constants as the clamp, so a drag stops where a drop would put it.

And positionAbsolute ?? getNodeAbsolutePosition(...) in the fit-view path stops disagreeing with itself: React Flow's own answer carries no offset, so its two branches returned points 66px apart for the same node.

Type of Change

  • Bug fix

Testing

New unit tests on the hook pinning the convention — a child sits at parent-plus-position, a root node is untouched, and popping a child out round-trips to exactly where it was. Two fail on the old helper, with the double-count visible in the diff:

expected { x: 32, y: 132 } to deeply equal { x: 16, y: 66 }

That is the pop-out case: the block landed 16px right and 66px down instead of staying put.

Blast radius is bounded by the helper's own early return — a node with no parent returns its position unchanged, so nothing about top-level blocks changes. Only nested nodes move, and only onto the position they were already being drawn at.

Full [workflowId] and lib/workflows suites green (1756 tests), type-check and lint clean across all 23 packages.

Not verified on a running canvas. Worth dragging a block out of a Loop and back in before merging — that is the exact interaction this changes.

Notes

  • The extent change tightens the drag floor from 42 to HEADER_HEIGHT + TOP_PADDING. Programmatic placement already used the larger floor, so this makes dragging agree with dropping rather than loosening anything.
  • Remaining follow-ups from the subflow work, deliberately not bundled here: the mcp-dynamic-args row estimate, and writing a container's derived size back so the first painted frame is not the stored default.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

`getNodeAbsolutePosition` added the container's header and padding to a child's
position. Those are already in the position: React Flow places a child at its
parent's origin plus its own coordinates, and `clampPositionToContainer` is what
holds it clear of the chrome, flooring it at `LEFT_PADDING` and
`HEADER_HEIGHT + TOP_PADDING`. Counting them twice put every nested node 16px
right and 66px below where it actually renders.

Visible as a block dropping down-right the moment it is dragged out of a Loop,
and as a block landing off-target when dragged into one from the canvas. Also
skewed container hit-testing during a drag and the bounds `fitView` focuses on.

Two callers already knew: both subtracted the same three constants straight back
off to recover a relative position. They now take the difference of two
absolutes, which is what a relative position is. A third place, React Flow's
child `extent`, had its own copy of the numbers — a fourth distinct header
height, 42, against the 40 the card renders — and now reads the same constants
as the clamp, so a drag stops where a drop would put it.

`positionAbsolute ?? getNodeAbsolutePosition(...)` in the fit-view path can also
stop disagreeing with itself: React Flow's own answer carries no offset, so the
two branches returned points 66px apart for the same node.
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 12, 2026 10:07pm

Request Review

@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches core canvas drag/reparent and hit-testing for nested nodes; behavior change is intentional but affects every container interaction until manually verified on the canvas.

Overview
Fixes nested workflow blocks jumping when dragged in or out of Loop/subflow containers by stopping double-counting of container header and padding in getNodeAbsolutePosition.

getNodeAbsolutePosition now sums parent absolute + child position only (matching React Flow and clampPositionToContainer). Call sites in workflow.tsx no longer subtract header/padding when converting between absolute and relative coordinates. Child drag extent min bounds now use CONTAINER_DIMENSIONS instead of mismatched hardcoded values (e.g. 42px header).

Adds use-node-utilities.test.tsx covering child absolute position, root nodes, and pop-out round-trip. Types blocks as Record<string, BlockState>.

Reviewed by Cursor Bugbot for commit 1c7fc31. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR aligns nested-node coordinates with React Flow by removing duplicate container chrome offsets and using shared container constants.

  • Computes absolute positions as parent position plus child-relative position.
  • Derives reparenting positions from the difference between two absolute positions.
  • Aligns drag extents with the same padding and header dimensions used by placement clamping.
  • Strengthens the hook’s block-map type and adds regression tests for nested, root-level, and pop-out positioning.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported fixture typing issue is fixed by typing the hook and deriving the fixture type from its parameter.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.ts Removes duplicated header and padding offsets, safely reads optional parent links, and replaces the untyped block map with BlockState.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx Updates reparenting calculations and drag extents to use one consistent container coordinate convention.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities.test.tsx Adds typed regression coverage for child absolute positions, root positions, and stable container removal.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Child[Child-relative position] --> Absolute[Parent absolute + child position]
  Parent[Parent absolute position] --> Absolute
  Absolute --> Reparent[Subtract destination parent absolute]
  Reparent --> Clamp[Clamp to shared container padding and header]
  Clamp --> Render[Stable rendered position]
Loading

Reviews (2): Last reviewed commit: "refactor(workflow): type the node-utilit..." | Re-trigger Greptile

`useNodeUtilities` took `Record<string, any>`, so the test fixtures had to be
cast to reach it and nothing in the hook was checked against a real block.

Typing it as `Record<string, BlockState>` surfaced an unsafe read straight away:
the cycle walk re-read `blocks[currentId].data.parentId` after the `while`
condition had tested the same optional chain, on a map where both links are
optional. It now reads the value once and breaks on absence, which is what the
condition was trying to express.

The fixtures follow the hook's own parameter type, so they stay honest without a
cast on either side.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 1c7fc31. Configure here.

@waleedlatif1
waleedlatif1 merged commit c58a642 into staging Aug 12, 2026
30 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/nested-absolute-position branch August 12, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant