Conversation
Contributor
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
…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>
Contributor
There was a problem hiding this comment.
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.
client/src/pages/platform/workflow-editor/utils/handleDeleteTask.ts
Outdated
Show resolved
Hide resolved
client/src/pages/platform/workflow-editor/components/WorkflowEditor.tsx
Outdated
Show resolved
Hide resolved
client/src/pages/platform/workflow-editor/utils/workflowMutationGuard.ts
Outdated
Show resolved
Hide resolved
client/src/pages/platform/workflow-editor/utils/postDagreConstraints.ts
Outdated
Show resolved
Hide resolved
…_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.
…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>
…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>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

