|
|
|
@ -33,7 +33,6 @@ import { workflowEdgeTypes, workflowNodeTypes } from "./workflowTypes"; |
|
|
|
import { ConnectionDropMenu, type ConnectionIntent } from "./ConnectionDropMenu"; |
|
|
|
import { MultiSelectToolbar } from "./MultiSelectToolbar"; |
|
|
|
import { EdgeToolbar } from "./EdgeToolbar"; |
|
|
|
import { NodeDoubleClickZoomProvider } from "./nodes/NodeDoubleClickZoomContext"; |
|
|
|
import { NodeType, NanoBananaNodeData, HandleType, PromptNodeData, SmartTextNodeData, LLMGenerateNodeData, PromptConstructorNodeData, AvailableVariable, SelectedModel, WorkflowNode } from "@/types"; |
|
|
|
import { DEFAULT_NODE_DIMENSIONS, defaultNodeDimensions } from "@/store/utils/nodeDefaults"; |
|
|
|
import { getWorkflowNodeStyleDimensions } from "@/utils/nodeDimensions"; |
|
|
|
@ -99,6 +98,7 @@ import { getNodeAcceptedConnectionGroups } from "@/utils/nodeConnectionRegistry" |
|
|
|
import { suppressBoxSelectionClick } from "@/utils/canvasInteractionGuards"; |
|
|
|
import { calculateCanvasAutoLayout } from "@/utils/canvasAutoLayout"; |
|
|
|
import { canConnectByNodeConnectionSpec } from "@/utils/nodeConnectionSpec"; |
|
|
|
import { REQUEST_SMART_TEXT_EDIT_EVENT } from "@/utils/smartTextEditRequest"; |
|
|
|
|
|
|
|
const BOX_SELECTION_EXPAND_MIN_DISTANCE = 4; |
|
|
|
const MULTI_SELECTION_BOUNDS_PADDING = 40; |
|
|
|
@ -131,8 +131,27 @@ const CANVAS_LOCAL_INTERACTION_SELECTOR = [ |
|
|
|
'[contenteditable="true"]', |
|
|
|
'[data-canvas-keyboard-scope="local"]', |
|
|
|
].join(", "); |
|
|
|
// Node double-click should still zoom even though React Flow marks every node
|
|
|
|
// wrapper with `.nopan`/`.nodrag`; only bail on genuinely interactive controls
|
|
|
|
// (and the header title editor, which owns its own double-click to rename).
|
|
|
|
const NODE_DOUBLE_CLICK_IGNORE_SELECTOR = [ |
|
|
|
"button", |
|
|
|
"input", |
|
|
|
"textarea", |
|
|
|
"select", |
|
|
|
'[contenteditable="true"]', |
|
|
|
"[data-node-title-editor-trigger]", |
|
|
|
].join(", "); |
|
|
|
const NODE_DOUBLE_CLICK_ZOOM_DURATION = 260; |
|
|
|
|
|
|
|
function shouldIgnoreCanvasGesture(target: EventTarget | null): boolean { |
|
|
|
return target instanceof HTMLElement && Boolean(target.closest(CANVAS_LOCAL_INTERACTION_SELECTOR)); |
|
|
|
} |
|
|
|
|
|
|
|
function shouldIgnoreNodeDoubleClick(target: EventTarget | null): boolean { |
|
|
|
return target instanceof HTMLElement && Boolean(target.closest(NODE_DOUBLE_CLICK_IGNORE_SELECTOR)); |
|
|
|
} |
|
|
|
|
|
|
|
function isWorkflowNode(candidate: Node): candidate is WorkflowNode { |
|
|
|
return candidate.type !== "batchConnection"; |
|
|
|
} |
|
|
|
@ -1172,7 +1191,7 @@ export function WorkflowCanvas() { |
|
|
|
const zoomNodeIntoFocusById = useCallback((nodeId: string) => { |
|
|
|
const currentNodes = useWorkflowStore.getState().nodes; |
|
|
|
const node = currentNodes.find((candidate) => candidate.id === nodeId) ?? nodes.find((candidate) => candidate.id === nodeId); |
|
|
|
if (!node) return; |
|
|
|
if (!node) return Promise.resolve(false); |
|
|
|
|
|
|
|
const { width, height } = getNodeDimensions(node); |
|
|
|
const nodePosition = absoluteNodePositions.get(node.id) ?? node.position; |
|
|
|
@ -1195,23 +1214,48 @@ export function WorkflowCanvas() { |
|
|
|
window.getSelection()?.removeAllRanges(); |
|
|
|
selectSingleNode(node.id); |
|
|
|
setZoomPercent(Math.round(nextZoom * 100)); |
|
|
|
setCenter(center.x, center.y, { |
|
|
|
return Promise.resolve(setCenter(center.x, center.y, { |
|
|
|
duration: NODE_DOUBLE_CLICK_ZOOM_DURATION, |
|
|
|
zoom: nextZoom, |
|
|
|
}); |
|
|
|
})); |
|
|
|
|
|
|
|
}, [absoluteNodePositions, nodes, selectSingleNode, setCenter]); |
|
|
|
|
|
|
|
const zoomNodeById = useCallback((nodeId: string) => { |
|
|
|
zoomNodeIntoFocusById(nodeId); |
|
|
|
}, [zoomNodeIntoFocusById]); |
|
|
|
// Handle node double-click in the capture phase on the canvas wrapper, ahead of
|
|
|
|
// any node-internal double-click handlers (e.g. legacy inline editors). This lets
|
|
|
|
// a double-click focus/zoom the node without also triggering its own edit affordance;
|
|
|
|
// smart text opts back into editing explicitly via REQUEST_SMART_TEXT_EDIT_EVENT.
|
|
|
|
const handleCanvasDoubleClickCapture = useCallback((event: ReactMouseEvent) => { |
|
|
|
const target = event.target as HTMLElement | null; |
|
|
|
const nodeElement = target?.closest<HTMLElement>(".react-flow__node[data-id]"); |
|
|
|
if (!nodeElement) return; // pane double-click is handled by handlePaneClick
|
|
|
|
if (shouldIgnoreNodeDoubleClick(target)) return; // let interactive controls / title rename run
|
|
|
|
|
|
|
|
const nodeId = nodeElement.getAttribute("data-id"); |
|
|
|
if (!nodeId) return; |
|
|
|
|
|
|
|
const node = |
|
|
|
useWorkflowStore.getState().nodes.find((candidate) => candidate.id === nodeId) ?? |
|
|
|
nodes.find((candidate) => candidate.id === nodeId); |
|
|
|
if (node && !isWorkflowNode(node)) return; |
|
|
|
|
|
|
|
event.preventDefault(); |
|
|
|
event.stopPropagation(); |
|
|
|
|
|
|
|
void zoomNodeIntoFocusById(nodeId).then(() => { |
|
|
|
if (node?.type !== "smartText") return; |
|
|
|
window.dispatchEvent(new CustomEvent(REQUEST_SMART_TEXT_EDIT_EVENT, { |
|
|
|
detail: { nodeId }, |
|
|
|
})); |
|
|
|
}); |
|
|
|
}, [nodes, zoomNodeIntoFocusById]); |
|
|
|
|
|
|
|
const handlePaneClick = useCallback((event: ReactMouseEvent) => { |
|
|
|
if (event.detail !== 2) return; |
|
|
|
|
|
|
|
const target = event.target as HTMLElement | null; |
|
|
|
|
|
|
|
if (target?.closest(CANVAS_LOCAL_INTERACTION_SELECTOR)) return; |
|
|
|
if (shouldIgnoreCanvasGesture(target)) return; |
|
|
|
|
|
|
|
event.preventDefault(); |
|
|
|
window.getSelection()?.removeAllRanges(); |
|
|
|
@ -2963,6 +3007,7 @@ export function WorkflowCanvas() { |
|
|
|
}`}
|
|
|
|
onMouseDownCapture={handleCanvasMouseDownCapture} |
|
|
|
onMouseMoveCapture={handleCanvasMouseMoveCapture} |
|
|
|
onDoubleClickCapture={handleCanvasDoubleClickCapture} |
|
|
|
onMouseMove={updateLastCanvasPointerPosition} |
|
|
|
onDragOver={handleDragOver} |
|
|
|
onDragLeave={handleDragLeave} |
|
|
|
@ -2991,7 +3036,6 @@ export function WorkflowCanvas() { |
|
|
|
|
|
|
|
<NodeHandleMenuProvider value={nodeHandleMenuValue}> |
|
|
|
<NodeHeaderProvider value={{ onExpandNode: handleExpandNode }}> |
|
|
|
<NodeDoubleClickZoomProvider value={{ zoomNodeById }}> |
|
|
|
<ReactFlow |
|
|
|
nodes={reactFlowNodes} |
|
|
|
edges={reactFlowEdges} |
|
|
|
@ -3106,7 +3150,6 @@ export function WorkflowCanvas() { |
|
|
|
<NodeToolSelectionController /> |
|
|
|
</ViewportPortal> |
|
|
|
</ReactFlow> |
|
|
|
</NodeDoubleClickZoomProvider> |
|
|
|
</NodeHeaderProvider> |
|
|
|
</NodeHandleMenuProvider> |
|
|
|
|
|
|
|
|