Skip to content

2948 client - Vertical/horizontal/free-form layout alignment in the workflow editor#4195

Open
ivicac wants to merge 27 commits intomasterfrom
2948
Open

2948 client - Vertical/horizontal/free-form layout alignment in the workflow editor#4195
ivicac wants to merge 27 commits intomasterfrom
2948

Conversation

@ivicac
Copy link
Contributor

@ivicac ivicac commented Feb 14, 2026

  • 2948 client - Add layout direction infrastructure and workflow data store enhancements
  • 2948 client - Add workflow mutation guard and simplify query invalidation
  • 2948 client - Add position persistence and node animation utilities
  • 2948 client - Extract post-dagre constraints into testable pipeline
  • 2948 client - Add optimistic task deletion with mutation guard
  • 2948 client - Add layout direction support to node components
  • 2948 client - Add direction-aware edge rendering, button positioning, and routing fixes
  • 2948 client - Update layout engine with direction support and constraint integration
  • 2948 client - Add slide-in panel animation and node appearance effects
  • 2948 client - Update cluster elements canvas with position persistence and reset
  • 2948 client - Integrate all layout features in WorkflowEditor component
  • 2948 client - Add comprehensive tests for layout features

ivicac and others added 12 commits February 14, 2026 09:44
…tore enhancements

Introduce the foundation for multi-directional workflow layout.
Add LayoutDirectionType (TB/LR) and DEFAULT_LAYOUT_DIRECTION constants.
Create useLayoutDirectionStore with per-workflow direction persistence
(includes v0->v1 localStorage migration). Add directionUtils.ts with
mapHandlePosition() for translating handle positions between layout modes.
Extend useWorkflowDataStore with isNodeDragging, savedPositionCrossAxisShift,
and layoutResetCounter state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tion

Centralize workflow mutation concurrency control. Five independent workflow
save paths (definition, positions, clear, remove, delete) previously had
their own or no concurrency guard, allowing concurrent PUT requests that
triggered server-side OptimisticLockingFailureException. Add a shared
workflowMutationGuard module so only one workflow mutation can be in-flight
at a time. Integrate the guard into saveWorkflowDefinition. Simplify
invalidateWorkflowQueries in useProject to directly return
queryClient.invalidateQueries() instead of manually checking query state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add utilities for free node dragging with position persistence.
saveWorkflowNodesPosition saves dragged node positions to task/trigger
metadata with cross-axis shift compensation, handling dispatcher child
co-movement and incremental delta updates for previously positioned
children. clearAllNodePositions and removeWorkflowNodePosition handle
global and per-node position reset with mutation guards.
clearAllClusterElementPositions provides position reset for cluster
element canvases. animateNodePositions implements smooth position
interpolation with rigid-unit dispatcher subtree movement to prevent
ghost node edge jitter. dragTrailingPlaceholder moves the trailing "+"
placeholder in real-time during node drag in both TB and LR modes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract ~1,600 lines of post-dagre layout constraint logic into pure,
testable functions. The pipeline includes: applySavedPositions (restore
user-dragged positions with delta propagation to descendants),
centerLRSmallNodes (center nodes within dagre allocation in LR mode),
alignBranchCaseChildren (fix cross-axis alignment including chain
walking), constrainGhostNodes (position ghost nodes relative to
dispatcher content), centerAfterBottomGhost (center downstream nodes),
positionConditionCasePlaceholders, shiftConditionBranchContent
(constrain content within condition frames), alignChainNodesCrossAxis
(propagate position adjustments through node chains preserving cluster
centering offsets), alignTrailingPlaceholder,
adjustBottomGhostForMovedChildren (BFS propagation of main-axis deltas
from saved child dispatchers), and getClusterRootCrossOffset /
getParentDispatcherId helpers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Refactor task deletion for optimistic UI updates. Move panel close and
store update before the mutation call so the layout recomputes in a
single pass. Add isWorkflowMutating() check to prevent concurrent
mutations. Use onError for rollback to previousWorkflow state and
onSettled for background query invalidation, avoiding multiple
sequential layout passes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make all node components direction-aware for TB/LR layout modes. Each
node type reads layoutDirection from useLayoutDirectionStore and uses
mapHandlePosition() to translate handle positions. Ghost nodes swap
width/height dimensions and positioning CSS for horizontal mode.
WorkflowNode and AiAgentNode gain per-node reset position buttons
(pin-off icon) that call removeWorkflowNodePosition(). Label positions
and text truncation (max-w-[150px]) adapt for LR mode. Remove hardcoded
left-node-handle-placement CSS class from NodeTypes.module.css.
Placeholder nodes are made draggable (remove nodrag class for
non-cluster-element placeholders). AI Agent nodes differentiate dagre
width between nodes with and without cluster elements to fix edge
connection rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… and routing fixes

Make edge rendering fully direction-aware. Extract edge button position
computation into computeEdgeButtonPosition() with fallback to edge center
from getSmoothStepPath when computed position is undefined (fixes buttons
rendering at (0,0)). Bottom ghost source edges always use edge center for
button placement. BranchCaseLabel accepts layout direction and adapts
label transform calculation (LR uses sourceX/targetY, TB uses
targetX/sourceY). Fix createConditionEdges to use correct
component-specific bottom-ghost ID instead of hardcoded
-condition-bottom-ghost. Fix createBranchEdges to mark middle-case
edges with isMiddleCase data flag for proper edge path correction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…int integration

Integrate layout direction, position persistence, animation, and
post-dagre constraints into the core layout engine. layoutUtils.tsx
adds direction parameter throughout, swaps dagre rankdir/nodesep/ranksep
based on direction, integrates the full post-dagre constraint pipeline
(saved positions, LR centering, branch alignment, ghost constraints,
chain alignment, condition frame enforcement), and adds
getRenderedMainAxisSize() helper. useLayout.tsx tracks canvas
cross-dimension for saved position shift calculation, stores initial
direction to detect changes, implements animation lifecycle (cancel on
unmount/drag, skip on initial layout), and passes
savedPositionCrossAxisShift to layout functions. Animation uses
animateNodePositions with pre/target position interpolation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add visual polish with CSS animations. WorkflowNodeDetailsPanel gains a
slide-in-from-right animation (300ms ease-out) that only plays on panel
open, not when switching between nodes - uses a ref to track previous
open state and separates the animated outer div from the keyed inner div
to work correctly under React strict mode double-invocation. DataPillPanel
gets the same slide-in animation. WorkflowEditorLayout.css adds a
nodeAppear keyframe animation (200ms fade-in) applied to all workflow
nodes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e and reset

Enhance the cluster elements canvas with position persistence and layout
reset. Add a reset layout button (BrushCleaningIcon) to Controls that
calls clearAllClusterElementPositions() with resetPendingRef to prevent
concurrent resets. Add mutation pending checks to prevent saves during
resets. Enhanced node metadata with clusterElementTypeIndex and
parentClusterRootElementsTypeCount for intelligent subtree-aware layout
positioning. Replace dagre with custom handle-aligned layout for cluster
elements that positions children below parent handle positions, with
label-aware horizontal overlap resolution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire up all layout features in the main WorkflowEditor component. Add
layout direction toggle button in ReactFlow Controls. Implement node
drag handlers: handleNodeDragStart (collect dispatcher descendants,
build trailing placeholder), handleNodeDragStop (save positions with
cross-axis shift compensation, update dispatcher children incrementally),
and handleNodesChange (move dispatcher children during drag, update
trailing placeholder position). Add handleResetLayout for global position
reset. Add isChildNodeOfDispatcher() and collectAllDescendantNodes()
helpers. Track drag state refs for dispatcher ID, drag start positions,
child start positions, and trailing placeholder. Keep edge plus buttons
visible during drag. Set workflowId on layout direction store for
per-workflow direction persistence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ~5,250 lines of tests covering all major new functionality.
Post-dagre constraints (33 tests): saved position application, branch
case alignment, ghost node constraints, chain alignment, condition
frame enforcement, nested dispatcher propagation, cluster centering
offset preservation. Animation (10+ tests): position equality skipping,
interpolation, cancel behavior, multi-node animation, dispatcher
rigid-unit movement, data preservation. Edge button positioning (13
tests): all edge type combinations including bottom ghost sources.
Branch edges: isMiddleCase propagation, edge types, case distribution,
nested task dispatchers. Drag trailing placeholder: TB and LR modes,
dispatcher-via-bottom-ghost cases. Update task positions (9 tests):
saving, clearing, delta-shifted children, condition branches, nested
dispatchers. Layout direction store: per-workflow persistence, v0->v1
migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ivicac ivicac requested a review from Copilot February 14, 2026 08:49
@ivicac ivicac changed the title 2948 2948 Vertical/horizontal/free-form layout alignment in the workflow editor Feb 14, 2026
@ivicac ivicac changed the title 2948 Vertical/horizontal/free-form layout alignment in the workflow editor 2948 client - Vertical/horizontal/free-form layout alignment in the workflow editor Feb 14, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds direction-aware (vertical TB / horizontal LR) layout capabilities to the workflow editor, along with node position persistence, post-layout constraint processing, and UX improvements (animations, reset controls, optimistic deletion) backed by new tests.

Changes:

  • Introduces layout direction state (persisted per workflow) and direction-aware node/edge rendering.
  • Adds node position persistence utilities (save/clear/remove), post-dagre constraint pipeline, and animated layout transitions.
  • Adds workflow mutation concurrency guard plus optimistic deletion and query invalidation simplifications, with expanded test coverage.

Reviewed changes

Copilot reviewed 45 out of 46 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
client/src/shared/constants.tsx Adds layout direction type/default constant used across editor layout.
client/src/pages/platform/workflow-editor/utils/workflowMutationGuard.ts Adds global mutation guard to prevent concurrent workflow PUTs.
client/src/pages/platform/workflow-editor/utils/updateTaskPositions.test.ts Tests recursive task position updates/clears for nested dispatcher structures.
client/src/pages/platform/workflow-editor/utils/saveWorkflowNodesPosition.ts Persists dragged node positions into workflow definition (recursive).
client/src/pages/platform/workflow-editor/utils/saveWorkflowDefinition.ts Wraps workflow definition mutation with the new mutation guard.
client/src/pages/platform/workflow-editor/utils/removeWorkflowNodePosition.ts Adds single-node (and dispatcher subtree) position removal utility.
client/src/pages/platform/workflow-editor/utils/postDagreConstraints.ts Adds post-processing constraint pipeline to refine dagre layout results.
client/src/pages/platform/workflow-editor/utils/layoutUtils.tsx Adds direction-aware dagre sizing/positioning and integrates post-dagre constraints & saved positions.
client/src/pages/platform/workflow-editor/utils/handleDeleteTask.ts Adds optimistic task deletion + rollback + guard integration.
client/src/pages/platform/workflow-editor/utils/dragTrailingPlaceholder.ts Adds utilities to keep trailing placeholder moving during node drag.
client/src/pages/platform/workflow-editor/utils/dragTrailingPlaceholder.test.ts Tests trailing placeholder drag state and delta-based positioning.
client/src/pages/platform/workflow-editor/utils/directionUtils.ts Adds direction helpers for handle positions and axis selection.
client/src/pages/platform/workflow-editor/utils/createConditionEdges.ts Fixes nested dispatcher bottom-ghost edge IDs.
client/src/pages/platform/workflow-editor/utils/createBranchEdges.ts Adds middle-case metadata/edge-type adjustments for branch case routing.
client/src/pages/platform/workflow-editor/utils/clearAllNodePositions.ts Adds utility to clear all saved workflow node positions recursively (guarded).
client/src/pages/platform/workflow-editor/utils/clearAllClusterElementPositions.ts Adds utility to clear saved positions for cluster elements recursively.
client/src/pages/platform/workflow-editor/utils/animateNodePositions.ts Adds RAF-based animated transitions for layout updates (dispatcher-relative).
client/src/pages/platform/workflow-editor/utils/animateNodePositions.test.ts Comprehensive tests for animation behavior and dispatcher-relative movement.
client/src/pages/platform/workflow-editor/tests/createBranchEdges.test.ts Tests middle-case metadata propagation and edge type selection.
client/src/pages/platform/workflow-editor/stores/useWorkflowDataStore.ts Adds drag/layout state (isNodeDragging, shift, reset counter) to workflow store.
client/src/pages/platform/workflow-editor/stores/useLayoutDirectionStore.ts Adds persisted layout direction state keyed by workflow ID.
client/src/pages/platform/workflow-editor/stores/tests/useLayoutDirectionStore.test.ts Tests persisted-per-workflow direction switching behavior.
client/src/pages/platform/workflow-editor/nodes/WorkflowNode.tsx Makes nodes direction-aware (handles/labels) and adds “remove saved position” UI.
client/src/pages/platform/workflow-editor/nodes/TaskDispatcherTopGhostNode.tsx Makes ghost node handles/layout direction-aware.
client/src/pages/platform/workflow-editor/nodes/TaskDispatcherLeftGhostNode.tsx Makes left ghost node handles/layout direction-aware.
client/src/pages/platform/workflow-editor/nodes/TaskDispatcherBottomGhostNode.tsx Makes bottom ghost node handles/layout direction-aware.
client/src/pages/platform/workflow-editor/nodes/ReadOnlyPlaceholderNode.tsx Makes read-only placeholder handles direction-aware.
client/src/pages/platform/workflow-editor/nodes/ReadOnlyNode.tsx Makes read-only node handles direction-aware.
client/src/pages/platform/workflow-editor/nodes/PlaceholderNode.tsx Makes placeholder handles direction-aware (and adjusts wrapper positioning).
client/src/pages/platform/workflow-editor/nodes/NodeTypes.module.css Adjusts handle styling utilities used by direction-aware handle placement.
client/src/pages/platform/workflow-editor/nodes/AiAgentNode.tsx Adds direction-aware handle placement and remove-saved-position UI for AI Agent nodes.
client/src/pages/platform/workflow-editor/hooks/useLayout.tsx Adds direction/canvasHeight inputs, cross-axis shift compensation, and animated node transitions.
client/src/pages/platform/workflow-editor/edges/computeEdgeButtonPosition.ts Extracts edge button positioning logic and makes it direction-aware.
client/src/pages/platform/workflow-editor/edges/computeEdgeButtonPosition.test.ts Tests edge button positioning across node types and directions.
client/src/pages/platform/workflow-editor/edges/WorkflowEdge.tsx Uses extracted edge button positioning; fixes branch label placement for LR.
client/src/pages/platform/workflow-editor/edges/LabeledBranchCaseEdge.tsx Adjusts smoothstep path calculation for middle-case edges in LR/TB modes.
client/src/pages/platform/workflow-editor/edges/BranchCaseLabel.tsx Makes branch case label placement direction-aware.
client/src/pages/platform/workflow-editor/components/datapills/DataPillPanel.tsx Adds slide-in animation for the data pill panel.
client/src/pages/platform/workflow-editor/components/WorkflowNodeDetailsPanel.tsx Adds slide-in animation behavior and adjusts markup wrapper for animation control.
client/src/pages/platform/workflow-editor/components/WorkflowEditor.tsx Enables dragging, adds direction toggle/reset controls, drag persistence, and placeholder dragging support.
client/src/pages/platform/workflow-editor/WorkflowEditorLayout.css Adds slide-in and node-appear animations.
client/src/pages/platform/cluster-element-editor/utils/createClusterElementsNodes.ts Passes type index/count metadata needed for direction-aware handle positioning.
client/src/pages/platform/cluster-element-editor/utils/clusterElementsNodesUtils.tsx Stores cluster element type index/parent handle count into node data for positioning.
client/src/pages/platform/cluster-element-editor/components/ClusterElementsWorkflowEditor.tsx Adds reset layout control and avoids saves while mutation pending.
client/src/pages/automation/project/hooks/useProject.ts Simplifies workflow query invalidation by returning invalidate promise directly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

ivicac and others added 6 commits February 14, 2026 12:58
…_LAYOUT_DIRECTION

DEFAULT_LAYOUT_DIRECTION was introduced with the layout direction
infrastructure but the old DIRECTION constant ('TB') was left behind.
Having both is confusing and risks future divergence. Remove DIRECTION
since it has no remaining imports and DEFAULT_LAYOUT_DIRECTION serves
the same purpose with proper typing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…desAfterBottomGhost

centerNodesAfterBottomGhost defined a local getClusterRootCrossOffset
helper that duplicated the top-level function with identical logic but
different type handling (accessing node.data directly vs casting to
NodeDataType). This duplication risked TB/LR offsets (-85/-23) drifting
over time if one copy was updated without the other.

Reuse the shared top-level getClusterRootCrossOffset(node, direction)
helper instead. Add tests verifying cluster root offset application
in both TB mode (-85) and LR mode (-23) for AI Agent nodes after a
bottom ghost.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…andleDeleteTask

The optimistic store update (panel close + workflow state change) was
happening before the isWorkflowMutating() guard check. If a mutation
was already in-flight, the function returned early leaving the UI/store
updated optimistically but never persisting to the server, with no
rollback path.

Move the guard check to the top of the mutation block, before any
state modifications. This ensures no UI side effects occur when the
mutation is skipped due to an in-flight request.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The mutation guard was a module-level boolean shared across the entire
app. If multiple workflows/editors existed (or if an editor unmounted
and remounted), one in-flight mutation could block saves for unrelated
workflows, and a missed onSettled would leave the app permanently in
a "mutating" state.

Replace the global boolean with a Set<string> keyed by workflow ID.
isWorkflowMutating(workflowId) now checks only the specific workflow,
and setWorkflowMutating(workflowId, value) scopes the flag per workflow.
The no-argument isWorkflowMutating() overload returns true if ANY
workflow is mutating (used for UI guards like reset button).

Add clearAllWorkflowMutations() for cleanup on editor unmount. Update
all 6 callers (clearAllNodePositions, saveWorkflowNodesPosition,
removeWorkflowNodePosition, handleDeleteTask, saveWorkflowDefinition,
WorkflowEditor) to pass workflow.id. Add 6 unit tests covering
per-workflow scoping, independence, and cleanup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…drag

Ghost nodes (top/bottom/left) and placeholder nodes are synthetic layout
constructs recreated on every dagre layout pass with no corresponding
WorkflowTask in the definition. Dragging these nodes previously triggered
a saveWorkflowNodesPosition mutation that would match no tasks and result
in a wasted server round-trip. Add NON_PERSISTED_NODE_TYPES guard in
handleNodeDragStop to skip position persistence for these node types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… hooks

The scoped mutation guard change introduced workflow.id references in
handleNodeDragStop, handleResetLayout, and a useEffect, but their
dependency arrays were not updated, causing react-hooks/exhaustive-deps
warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… during drag"

This reverts commit 76f0db17410a59d9702c8277779889038d67d0d6.
@ivicac ivicac requested a review from kresimir-coko February 14, 2026 13:36
ivicac and others added 2 commits February 14, 2026 19:12
…ntally in LR mode

Extract edge coordinate correction logic into shared computeEdgeCorrectedCoordinates
utility and add aligned side-case detection that snaps coordinates and overrides handle
positions when the Y mismatch from ghost node handle offsets is within threshold.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
In LR mode, the +15px label-avoidance offset was incorrectly applied
to posY (pushing buttons below horizontal edges) instead of posX
(shifting along the edge).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…onent

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ivicac and others added 4 commits February 15, 2026 07:39
…ements directly from cluster editor

Extract cluster element layout into its own exported function and have
useClusterElementsLayout call it directly (synchronous) instead of going
through the async getLayoutElements wrapper. Remove the isClusterElementsCanvas
parameter from getLayoutElements since it is no longer needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eWorkflowMutation access

useWorkflowEditor() unsafely casts WorkflowReadOnlyStateI to
WorkflowEditorStateI, so updateWorkflowMutation is undefined in
read-only contexts like ReadOnlyWorkflowSheet. Add optional chaining
and null checks to prevent TypeError on isPending access.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…g with balanced spread

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lementTypesCount

Set clusterElementTypesCount on main root node data so the layout
algorithm uses the actual visual width for centering instead of
defaulting to 280px. Also compensate for the 16px dialog right gap
when fixed panels overlap the canvas.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed for 'client'

Failed conditions
55.5% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

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.

[feature] Vertical/horizontal/free-form layout alignment in the workflow editor

1 participant