From 429fd31d48fead982b7099db5397bf3c78937a2a Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Wed, 27 May 2026 10:43:46 +0200 Subject: [PATCH 01/10] Update flow configuration and adjust handle positioning for improved layout consistency --- .../app/routes/studio/canvas/flow.config.ts | 1 + .../canvas/nodetypes/components/handle.tsx | 2 +- .../studio/canvas/nodetypes/frank-node.tsx | 28 +++++++++++-------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/main/frontend/app/routes/studio/canvas/flow.config.ts b/src/main/frontend/app/routes/studio/canvas/flow.config.ts index 3d0c18a8..5f72e97f 100644 --- a/src/main/frontend/app/routes/studio/canvas/flow.config.ts +++ b/src/main/frontend/app/routes/studio/canvas/flow.config.ts @@ -1,6 +1,7 @@ export const FlowConfig = { NODE_DEFAULT_WIDTH: 300, NODE_MIN_HEIGHT: 80, + NODE_ZOOMED_OUT_HEIGHT: 300, EXIT_DEFAULT_WIDTH: 150, EXIT_DEFAULT_HEIGHT: 100, STICKY_NOTE_DEFAULT_WIDTH: 200, diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle.tsx b/src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle.tsx index b8979921..55c92081 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle.tsx +++ b/src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle.tsx @@ -68,7 +68,7 @@ export function CustomHandle(properties: Readonly) { isConnectableEnd={connections.length === 0} onClick={handleClick} style={{ - top: `${properties.firstHandlePosition + properties.index * properties.handleSpacing}px`, + top: `${properties.firstHandlePosition + (properties.index - 1) * properties.handleSpacing}px`, right: '-15px', width: '15px', height: '15px', diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx index 3402c09e..f9c7f00b 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx +++ b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx @@ -118,8 +118,8 @@ export default function FrankNode(properties: NodeProps) { }, [dimensions.height, properties.data.sourceHandles.length]) const compactFirstHandlePosition = useMemo(() => { - return (minNodeHeight - (properties.data.sourceHandles.length - 1) * handleSpacing) / 2 - }, [minNodeHeight, properties.data.sourceHandles.length]) + return (FlowConfig.NODE_ZOOMED_OUT_HEIGHT - (properties.data.sourceHandles.length - 1) * handleSpacing) / 2 + }, [properties.data.sourceHandles.length]) const allForwardTypesUsed = useMemo(() => { if (availableHandleTypes.length === 0) return true @@ -143,6 +143,10 @@ export default function FrankNode(properties: NodeProps) { } }, [dragOver, properties.id, updateNodeInternals]) + useEffect(() => { + updateNodeInternals(properties.id) + }, [dimensions.height, isCompact, properties.id, updateNodeInternals]) + useEffect(() => { fetchFrankConfigXsd().then((xsd) => { const doc = parseXsd(xsd) @@ -401,21 +405,21 @@ export default function FrankNode(properties: NodeProps) { return ( <>
- + {abbr}
@@ -477,7 +481,7 @@ export default function FrankNode(properties: NodeProps) { className={`bg-background border-border relative flex w-full flex-col items-center overflow-x-visible rounded-md border ${isManuallyResized ? 'h-full overflow-y-hidden' : 'overflow-y-visible'}`} style={{ minWidth: `${minNodeWidth}px`, - ...(properties.selected && { borderColor: `var(${colorVariable})` }), + ...(properties.selected && { borderColor: `${nodeColor}` }), }} ref={containerReference} onDragOver={handleDragOver} @@ -490,10 +494,10 @@ export default function FrankNode(properties: NodeProps) { background: gradientEnabled ? `radial-gradient( ellipse farthest-corner at 20% 20%, - var(${colorVariable}) 0%, + ${nodeColor} 0%, var(--color-background) 100% )` - : `var(${colorVariable})`, + : `${nodeColor}`, }} >

{properties.data.subtype}

@@ -634,7 +638,7 @@ export default function FrankNode(properties: NodeProps) { }} className="nodrag absolute -right-5.75 h-3.75 w-3.75 cursor-pointer justify-center rounded-full border bg-gray-400 text-center text-[8px] font-bold text-white" style={{ - top: `${firstHandlePosition + properties.data.sourceHandles.length * handleSpacing + handleSpacing}px`, + top: `${firstHandlePosition + properties.data.sourceHandles.length * handleSpacing}px`, }} > + From 50551fdb12e77b7c77dae2e2f9cef01538fa10f4 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Wed, 27 May 2026 16:29:28 +0200 Subject: [PATCH 02/10] Update NODE_ZOOMED_OUT_HEIGHT in flow configuration for improved layout, now its bigger --- src/main/frontend/app/routes/studio/canvas/flow.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/frontend/app/routes/studio/canvas/flow.config.ts b/src/main/frontend/app/routes/studio/canvas/flow.config.ts index 5f72e97f..1a88a5d1 100644 --- a/src/main/frontend/app/routes/studio/canvas/flow.config.ts +++ b/src/main/frontend/app/routes/studio/canvas/flow.config.ts @@ -1,7 +1,7 @@ export const FlowConfig = { NODE_DEFAULT_WIDTH: 300, NODE_MIN_HEIGHT: 80, - NODE_ZOOMED_OUT_HEIGHT: 300, + NODE_ZOOMED_OUT_HEIGHT: 380, EXIT_DEFAULT_WIDTH: 150, EXIT_DEFAULT_HEIGHT: 100, STICKY_NOTE_DEFAULT_WIDTH: 200, From 59005baf9071802dbeffde738517ee105d32995b Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Wed, 27 May 2026 16:33:11 +0200 Subject: [PATCH 03/10] Refactor handle positioning and styling for improved layout on zoomed out version in flow --- .../canvas/nodetypes/components/handle.tsx | 3 +- .../studio/canvas/nodetypes/frank-node.tsx | 71 +++++++++++++------ 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle.tsx b/src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle.tsx index 55c92081..53367515 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle.tsx +++ b/src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle.tsx @@ -11,6 +11,7 @@ interface HandleProperties { onChangeType: (newType: string) => void absolutePosition: { x: number; y: number } typesAllowed?: Record + rightOverride?: string } const HANDLE_TYPE_COLOURS: Record = { @@ -69,7 +70,7 @@ export function CustomHandle(properties: Readonly) { onClick={handleClick} style={{ top: `${properties.firstHandlePosition + (properties.index - 1) * properties.handleSpacing}px`, - right: '-15px', + right: properties.rightOverride ?? '-15px', width: '15px', height: '15px', backgroundColor: translateHandleTypeToColour(type), diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx index f9c7f00b..86338a04 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx +++ b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx @@ -117,9 +117,16 @@ export default function FrankNode(properties: NodeProps) { return (dimensions.height - (properties.data.sourceHandles.length - 1) * handleSpacing) / 2 }, [dimensions.height, properties.data.sourceHandles.length]) - const compactFirstHandlePosition = useMemo(() => { - return (FlowConfig.NODE_ZOOMED_OUT_HEIGHT - (properties.data.sourceHandles.length - 1) * handleSpacing) / 2 - }, [properties.data.sourceHandles.length]) + const COMPACT_INITIALS_BOX_SIZE = 160 + const COMPACT_PADDING_TOP = 8 + const COMPACT_HANDLE_SIZE = 15 + const COMPACT_HANDLE_GAP = 4 + + const compactHandleXOffset = `${(FlowConfig.NODE_DEFAULT_WIDTH - COMPACT_INITIALS_BOX_SIZE) / 2 - COMPACT_HANDLE_SIZE - COMPACT_HANDLE_GAP}px` + const compactHandleTop = COMPACT_PADDING_TOP + COMPACT_INITIALS_BOX_SIZE / 2 - COMPACT_HANDLE_SIZE / 2 + 10 + const compactDotTop = compactHandleTop - 8 + const compactDotXOffset = `${(FlowConfig.NODE_DEFAULT_WIDTH - COMPACT_INITIALS_BOX_SIZE) / 2 - COMPACT_HANDLE_SIZE - COMPACT_HANDLE_GAP - 10}px` + const compactTargetXOffset = `${(FlowConfig.NODE_DEFAULT_WIDTH - COMPACT_INITIALS_BOX_SIZE) / 2 - COMPACT_HANDLE_SIZE - COMPACT_HANDLE_GAP + 10}px` const allForwardTypesUsed = useMemo(() => { if (availableHandleTypes.length === 0) return true @@ -405,33 +412,32 @@ export default function FrankNode(properties: NodeProps) { return ( <>
- + {abbr}
- + {properties.data.subtype} {properties.data.name && ( - - {properties.data.name} - + {properties.data.name} )}
@@ -439,20 +445,43 @@ export default function FrankNode(properties: NodeProps) { + )} + + {properties.data.sourceHandles.length > 0 && ( +
)} {properties.data.sourceHandles.map((handle) => ( - changeHandleType(handle.index, newType)} - absolutePosition={{ x: properties.positionAbsoluteX, y: properties.positionAbsoluteY }} - typesAllowed={frankElement?.forwards} + type="source" + position={Position.Right} + id={handle.index.toString()} + style={{ + top: `${compactHandleTop}px`, + right: compactHandleXOffset, + width: '15px', + height: '15px', + opacity: 0, + }} /> ))} From ce80bec7c5b3c884cf2187a0c6a4caed4d06b743 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Wed, 27 May 2026 16:37:35 +0200 Subject: [PATCH 04/10] Fix handle positioning for consistent layout in flow component --- .../app/routes/studio/canvas/nodetypes/components/handle.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle.tsx b/src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle.tsx index 53367515..b8979921 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle.tsx +++ b/src/main/frontend/app/routes/studio/canvas/nodetypes/components/handle.tsx @@ -11,7 +11,6 @@ interface HandleProperties { onChangeType: (newType: string) => void absolutePosition: { x: number; y: number } typesAllowed?: Record - rightOverride?: string } const HANDLE_TYPE_COLOURS: Record = { @@ -69,8 +68,8 @@ export function CustomHandle(properties: Readonly) { isConnectableEnd={connections.length === 0} onClick={handleClick} style={{ - top: `${properties.firstHandlePosition + (properties.index - 1) * properties.handleSpacing}px`, - right: properties.rightOverride ?? '-15px', + top: `${properties.firstHandlePosition + properties.index * properties.handleSpacing}px`, + right: '-15px', width: '15px', height: '15px', backgroundColor: translateHandleTypeToColour(type), From 9ea76f634d3da2cb41b7a68737ebf69a07ce5c78 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Mon, 1 Jun 2026 13:07:12 +0200 Subject: [PATCH 05/10] Refactor handle rendering in flow component for improved layout consistency --- .../studio/canvas/nodetypes/frank-node.tsx | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx index 86338a04..fc5ad429 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx +++ b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx @@ -126,7 +126,6 @@ export default function FrankNode(properties: NodeProps) { const compactHandleTop = COMPACT_PADDING_TOP + COMPACT_INITIALS_BOX_SIZE / 2 - COMPACT_HANDLE_SIZE / 2 + 10 const compactDotTop = compactHandleTop - 8 const compactDotXOffset = `${(FlowConfig.NODE_DEFAULT_WIDTH - COMPACT_INITIALS_BOX_SIZE) / 2 - COMPACT_HANDLE_SIZE - COMPACT_HANDLE_GAP - 10}px` - const compactTargetXOffset = `${(FlowConfig.NODE_DEFAULT_WIDTH - COMPACT_INITIALS_BOX_SIZE) / 2 - COMPACT_HANDLE_SIZE - COMPACT_HANDLE_GAP + 10}px` const allForwardTypesUsed = useMemo(() => { if (availableHandleTypes.length === 0) return true @@ -442,17 +441,30 @@ export default function FrankNode(properties: NodeProps) {
{frankElement?.name && frankElement.name !== 'Receiver' && ( - + <> +
+ + )} {properties.data.sourceHandles.length > 0 && ( From 3d38831bbe2eb31ab0e77ccf47406490c1cfe683 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Tue, 2 Jun 2026 13:20:27 +0200 Subject: [PATCH 06/10] Add compact handle selection for connections in flow component --- .../app/routes/studio/canvas/flow.tsx | 109 +++++++++++++++++- .../studio/canvas/nodetypes/frank-node.tsx | 12 +- .../app/routes/studio/flow-to-xml-parser.ts | 2 +- 3 files changed, 113 insertions(+), 10 deletions(-) diff --git a/src/main/frontend/app/routes/studio/canvas/flow.tsx b/src/main/frontend/app/routes/studio/canvas/flow.tsx index 694b007c..c939d600 100644 --- a/src/main/frontend/app/routes/studio/canvas/flow.tsx +++ b/src/main/frontend/app/routes/studio/canvas/flow.tsx @@ -4,6 +4,7 @@ import { BackgroundVariant, ControlButton, Controls, + type Connection, type Edge, type Node, type OnConnectStart, @@ -54,6 +55,7 @@ import { useShortcut } from '~/hooks/use-shortcut' import CanvasContextMenu from '~/components/flow/canvas-context-menu' import { useSidebarStore, SidebarSide } from '~/stores/sidebar-layout-store' import { openInEditorAtElement } from '~/actions/navigationActions' +import HandleMenu from '~/routes/studio/canvas/nodetypes/components/handle-menu' export type FlowNode = FrankNodeType | ExitNode | StickyNote | GroupNode | Node @@ -206,6 +208,12 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { const [showModal, setShowModal] = useState(false) const [edgeDropPositions, setEdgeDropPositions] = useState<{ x: number; y: number } | null>(null) + const [pendingCompactConnection, setPendingCompactConnection] = useState<{ + connection: Connection + sourceNodeSubtype: string + position: { x: number; y: number } + } | null>(null) + const clipboardRef = useRef<{ nodes: FlowNode[] edges: Edge[] @@ -460,14 +468,76 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { } const handleConnectEnd: OnConnectEnd = (event, connectionState) => { + const mouseEvent = event as MouseEvent if (!connectionState.isValid) { - const mouseEvent = event as MouseEvent - const x = mouseEvent.clientX - const y = mouseEvent.clientY - handleEdgeDropOnCanvas(x, y) + handleEdgeDropOnCanvas(mouseEvent.clientX, mouseEvent.clientY) } } + const handleConnect = useCallback( + (connection: Connection) => { + const zoom = reactFlow.getZoom() + + if (zoom < 0.4 && connection.source) { + const { nodes } = useFlowStore.getState() + const sourceNode = nodes.find((node) => node.id === connection.source) + + if (sourceNode && isFrankNode(sourceNode)) { + const targetNode = nodes.find((node) => node.id === connection.target) + const position = targetNode + ? reactFlow.flowToScreenPosition({ + x: targetNode.position.x + (targetNode.measured?.width ?? FlowConfig.NODE_DEFAULT_WIDTH) / 2, + y: targetNode.position.y + (targetNode.measured?.height ?? FlowConfig.NODE_ZOOMED_OUT_HEIGHT) / 2, + }) + : { x: window.innerWidth / 2, y: window.innerHeight / 2 } + + setPendingCompactConnection({ + connection, + sourceNodeSubtype: sourceNode.data.subtype, + position, + }) + return + } + } + + onConnect(connection) + }, + [onConnect, reactFlow], + ) + + const handleCompactHandleSelect = useCallback( + (type: string) => { + if (!pendingCompactConnection) return + + const { nodes } = useFlowStore.getState() + const sourceNode = nodes.find((node) => node.id === pendingCompactConnection.connection.source) + + if (!sourceNode || !isFrankNode(sourceNode)) { + setPendingCompactConnection(null) + return + } + + const existingHandle = sourceNode.data.sourceHandles.find((handle) => handle.type === type) + + if (existingHandle) { + onConnect({ + ...pendingCompactConnection.connection, + sourceHandle: existingHandle.index.toString(), + }) + } else { + const newIndex = sourceNode.data.sourceHandles.length + 1 + useFlowStore.getState().addHandle(pendingCompactConnection.connection.source!, { type, index: newIndex }) + onConnect({ + ...pendingCompactConnection.connection, + sourceHandle: newIndex.toString(), + }) + } + + setPendingCompactConnection(null) + }, + [pendingCompactConnection, onConnect], + ) + const handleEdgeDropOnCanvas = (x: number, y: number) => { const { screenToFlowPosition } = reactFlow const flowPositions = screenToFlowPosition({ x: x, y: y }) @@ -1158,6 +1228,22 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { if (sourceInfo?.nodeId && sourceInfo.handleType === 'source') { const sourceNode = flowStore.nodes.find((node) => node.id === sourceInfo.nodeId) + if (reactFlow.getZoom() < 0.4 && sourceNode && isFrankNode(sourceNode)) { + setPendingCompactConnection({ + connection: { + source: sourceInfo.nodeId, + sourceHandle: null, + target: newId.toString(), + targetHandle: null, + }, + sourceNodeSubtype: sourceNode.data.subtype, + position: reactFlow.flowToScreenPosition(position), + }) + + sourceInfoReference.current = { nodeId: null, handleId: null, handleType: null } + return + } + const label = getEdgeLabelFromHandle(sourceNode, sourceInfo.handleId) const newEdge: Edge = { @@ -1504,7 +1590,7 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { }} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} - onConnect={onConnect} + onConnect={handleConnect} onReconnect={onReconnect} onNodeClick={handleNodeClick} onNodeDoubleClick={handleNodeDoubleClick} @@ -1560,6 +1646,19 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { sourceInfo={sourceInfoReference.current} /> + {pendingCompactConnection && ( + setPendingCompactConnection(null)} + onSelect={handleCompactHandleSelect} + typesAllowed={ + (elements as Record | null)?.[pendingCompactConnection.sourceNodeSubtype] + ?.forwards + } + /> + )} + {contextMenu && ( ) { const COMPACT_HANDLE_SIZE = 15 const COMPACT_HANDLE_GAP = 4 - const compactHandleXOffset = `${(FlowConfig.NODE_DEFAULT_WIDTH - COMPACT_INITIALS_BOX_SIZE) / 2 - COMPACT_HANDLE_SIZE - COMPACT_HANDLE_GAP}px` + const compactBaseXOffsetPx = + (FlowConfig.NODE_DEFAULT_WIDTH - COMPACT_INITIALS_BOX_SIZE) / 2 - COMPACT_HANDLE_SIZE - COMPACT_HANDLE_GAP + + const compactHandleXOffset = `${compactBaseXOffsetPx}px` const compactHandleTop = COMPACT_PADDING_TOP + COMPACT_INITIALS_BOX_SIZE / 2 - COMPACT_HANDLE_SIZE / 2 + 10 + const compactDotTop = compactHandleTop - 8 - const compactDotXOffset = `${(FlowConfig.NODE_DEFAULT_WIDTH - COMPACT_INITIALS_BOX_SIZE) / 2 - COMPACT_HANDLE_SIZE - COMPACT_HANDLE_GAP - 10}px` + const compactDotXOffset = `${compactBaseXOffsetPx - 10}px` const allForwardTypesUsed = useMemo(() => { if (availableHandleTypes.length === 0) return true @@ -677,9 +681,9 @@ export default function FrankNode(properties: NodeProps) { onClick={(event) => { toggleHandleMenu(event) }} - className="nodrag absolute -right-5.75 h-3.75 w-3.75 cursor-pointer justify-center rounded-full border bg-gray-400 text-center text-[8px] font-bold text-white" + className="nodrag absolute -right-5.5 h-3.75 w-3.75 cursor-pointer justify-center rounded-full border bg-gray-400 text-center text-[8px] font-bold text-white" style={{ - top: `${firstHandlePosition + properties.data.sourceHandles.length * handleSpacing}px`, + top: `${firstHandlePosition + properties.data.sourceHandles.length * handleSpacing + 15}px`, }} > + diff --git a/src/main/frontend/app/routes/studio/flow-to-xml-parser.ts b/src/main/frontend/app/routes/studio/flow-to-xml-parser.ts index a268c07e..a672ee03 100644 --- a/src/main/frontend/app/routes/studio/flow-to-xml-parser.ts +++ b/src/main/frontend/app/routes/studio/flow-to-xml-parser.ts @@ -26,7 +26,7 @@ function hasDataProperty(node: FlowNode): node is Extract', '>').replaceAll('"', '"') } export async function exportFlowToXml( From 719b9db5ac99dd300ee140bad13d0e29851c6af3 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Tue, 2 Jun 2026 15:09:35 +0200 Subject: [PATCH 07/10] Add edge drop handle selection and improve connection handling in flow component --- .../studio/canvas/edgetypes/frank-edge.tsx | 5 +- .../app/routes/studio/canvas/flow.tsx | 88 ++++++++++++++++--- .../nodetypes/components/handle-menu.tsx | 4 +- .../studio/canvas/nodetypes/frank-node.tsx | 44 ++++++---- src/main/frontend/app/stores/flow-store.ts | 2 +- 5 files changed, 108 insertions(+), 35 deletions(-) diff --git a/src/main/frontend/app/routes/studio/canvas/edgetypes/frank-edge.tsx b/src/main/frontend/app/routes/studio/canvas/edgetypes/frank-edge.tsx index 86c87ded..93358c5a 100644 --- a/src/main/frontend/app/routes/studio/canvas/edgetypes/frank-edge.tsx +++ b/src/main/frontend/app/routes/studio/canvas/edgetypes/frank-edge.tsx @@ -68,10 +68,11 @@ export default function FrankEdge({ position: 'absolute', transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`, pointerEvents: 'all', + zIndex: 20, }} - className="flex flex-col items-center" + className="nodrag flex flex-col items-center" > -

+

{sourceHandleType} {selected && (

)} - {isEditing && ( + {isEditing && !pendingCompactConnection && (
@@ -1659,6 +1711,18 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { /> )} + {pendingEdgeDrop && ( + setPendingEdgeDrop(null)} + onSelect={handleEdgeDropHandleSelect} + typesAllowed={ + (elements as Record | null)?.[pendingEdgeDrop.sourceNodeSubtype]?.forwards + } + /> + )} + {contextMenu && (
-
+
{title}
    diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx index 0abe27b8..4c4d0ed6 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx +++ b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx @@ -11,7 +11,7 @@ import { import DangerIcon from '../../../../../icons/solar/Danger Triangle.svg?react' import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import useFlowStore from '~/stores/flow-store' -import { CustomHandle } from '~/routes/studio/canvas/nodetypes/components/handle' +import { CustomHandle, translateHandleTypeToColour } from '~/routes/studio/canvas/nodetypes/components/handle' import { FlowConfig } from '~/routes/studio/canvas/flow.config' import { useNodeContextMenu } from '~/routes/studio/canvas/node-context-menu-context' import useNodeContextStore from '~/stores/node-context-store' @@ -122,14 +122,11 @@ export default function FrankNode(properties: NodeProps) { const COMPACT_HANDLE_SIZE = 15 const COMPACT_HANDLE_GAP = 4 - const compactBaseXOffsetPx = + const compactXOffsetPx = (FlowConfig.NODE_DEFAULT_WIDTH - COMPACT_INITIALS_BOX_SIZE) / 2 - COMPACT_HANDLE_SIZE - COMPACT_HANDLE_GAP - const compactHandleXOffset = `${compactBaseXOffsetPx}px` - const compactHandleTop = COMPACT_PADDING_TOP + COMPACT_INITIALS_BOX_SIZE / 2 - COMPACT_HANDLE_SIZE / 2 + 10 - - const compactDotTop = compactHandleTop - 8 - const compactDotXOffset = `${compactBaseXOffsetPx - 10}px` + const compactHandleTop = + COMPACT_PADDING_TOP + COMPACT_INITIALS_BOX_SIZE / 2 - COMPACT_HANDLE_SIZE / 2 + COMPACT_HANDLE_SIZE const allForwardTypesUsed = useMemo(() => { if (availableHandleTypes.length === 0) return true @@ -442,6 +439,12 @@ export default function FrankNode(properties: NodeProps) { {properties.data.name && ( {properties.data.name} )} + {properties.data.attributes && + Object.entries(properties.data.attributes).map(([key, value]) => ( + + {value || key} + + ))}
{frankElement?.name && frankElement.name !== 'Receiver' && ( @@ -449,8 +452,9 @@ export default function FrankNode(properties: NodeProps) {
) { ) {
@@ -493,7 +499,7 @@ export default function FrankNode(properties: NodeProps) { id={handle.index.toString()} style={{ top: `${compactHandleTop}px`, - right: compactHandleXOffset, + right: compactXOffsetPx, width: '15px', height: '15px', opacity: 0, @@ -654,6 +660,7 @@ export default function FrankNode(properties: NodeProps) { ) { onClick={(event) => { toggleHandleMenu(event) }} - className="nodrag absolute -right-5.5 h-3.75 w-3.75 cursor-pointer justify-center rounded-full border bg-gray-400 text-center text-[8px] font-bold text-white" + className="nodrag absolute h-4 w-4 cursor-pointer justify-center rounded-full border bg-gray-400 text-center text-[8px] font-bold text-white" style={{ - top: `${firstHandlePosition + properties.data.sourceHandles.length * handleSpacing + 15}px`, + top: `${firstHandlePosition + properties.data.sourceHandles.length * handleSpacing + 12.5}px`, + right: '-23px', }} > + diff --git a/src/main/frontend/app/stores/flow-store.ts b/src/main/frontend/app/stores/flow-store.ts index bcef36e7..f96370fa 100644 --- a/src/main/frontend/app/stores/flow-store.ts +++ b/src/main/frontend/app/stores/flow-store.ts @@ -254,7 +254,7 @@ const useFlowStore = create()( if (wouldCreateDuplicateForward(edges, connection.source, connection.target, label)) return get().saveToHistory() - set({ edges: addEdge({ ...connection, type: 'frankEdge', animated: true, data: { label } }, get().edges) }) + set({ edges: addEdge({ ...connection, type: 'frankEdge', data: { label } }, get().edges) }) }, onReconnect: (oldEdge, newConnection) => { const { nodes, edges } = get() From 0ba67ee838988ead5d0bab83d02594c79ce28559 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Tue, 2 Jun 2026 15:18:26 +0200 Subject: [PATCH 08/10] Refactor color handling in frank-node component to use CSS variables for improved consistency --- .../frontend/app/routes/studio/canvas/flow.tsx | 4 +--- .../routes/studio/canvas/nodetypes/frank-node.tsx | 14 +++++++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/frontend/app/routes/studio/canvas/flow.tsx b/src/main/frontend/app/routes/studio/canvas/flow.tsx index 7a901ac2..4434960d 100644 --- a/src/main/frontend/app/routes/studio/canvas/flow.tsx +++ b/src/main/frontend/app/routes/studio/canvas/flow.tsx @@ -1613,9 +1613,7 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { )} {isEditing && !pendingCompactConnection && ( -
+
diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx index 4c4d0ed6..caccea39 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx +++ b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx @@ -417,17 +417,17 @@ export default function FrankNode(properties: NodeProps) { width: `${FlowConfig.NODE_DEFAULT_WIDTH}px`, paddingTop: `${COMPACT_PADDING_TOP}px`, paddingBottom: '8px', - ...(properties.selected && { borderColor: `${nodeColor}` }), + ...(properties.selected && { borderColor: `var(${colorVariable})` }), }} >
- + {abbr}
@@ -532,7 +532,7 @@ export default function FrankNode(properties: NodeProps) { className={`bg-background border-border relative flex w-full flex-col items-center overflow-x-visible rounded-md border ${isManuallyResized ? 'h-full overflow-y-hidden' : 'overflow-y-visible'}`} style={{ minWidth: `${minNodeWidth}px`, - ...(properties.selected && { borderColor: `${nodeColor}` }), + ...(properties.selected && { borderColor: `var(${colorVariable})` }), }} ref={containerReference} onDragOver={handleDragOver} @@ -545,10 +545,10 @@ export default function FrankNode(properties: NodeProps) { background: gradientEnabled ? `radial-gradient( ellipse farthest-corner at 20% 20%, - ${nodeColor} 0%, + var(${colorVariable}) 0%, var(--color-background) 100% )` - : `${nodeColor}`, + : `var(${colorVariable})`, }} >

{properties.data.subtype}

From fdae8f9850ab2fd240df43ea2420b566c017bc64 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Wed, 3 Jun 2026 10:40:30 +0200 Subject: [PATCH 09/10] Update FrankDocService URL and refactor receiver handle condition in frank-node component --- .../app/routes/studio/canvas/nodetypes/frank-node.tsx | 4 ++-- .../org/frankframework/flow/frankdoc/FrankDocService.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx index caccea39..c285d6eb 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx +++ b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx @@ -447,7 +447,7 @@ export default function FrankNode(properties: NodeProps) { ))}
- {frankElement?.name && frankElement.name !== 'Receiver' && ( + {properties.data.subtype !== 'Receiver' && ( <>
) {
{/* Receivers can only have outgoing connections, so we hide the input handle for them */} - {frankElement?.name && frankElement.name !== 'Receiver' && ( + {properties.data.subtype !== 'Receiver' && ( Date: Wed, 3 Jun 2026 10:48:19 +0200 Subject: [PATCH 10/10] Update FrankDocService URL and refactor handle background color in frank-node component --- .../app/routes/studio/canvas/nodetypes/frank-node.tsx | 4 ++-- .../org/frankframework/flow/frankdoc/FrankDocServiceTest.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx index c285d6eb..7cdc76c9 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx +++ b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx @@ -11,7 +11,7 @@ import { import DangerIcon from '../../../../../icons/solar/Danger Triangle.svg?react' import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import useFlowStore from '~/stores/flow-store' -import { CustomHandle, translateHandleTypeToColour } from '~/routes/studio/canvas/nodetypes/components/handle' +import { CustomHandle } from '~/routes/studio/canvas/nodetypes/components/handle' import { FlowConfig } from '~/routes/studio/canvas/flow.config' import { useNodeContextMenu } from '~/routes/studio/canvas/node-context-menu-context' import useNodeContextStore from '~/stores/node-context-store' @@ -485,7 +485,7 @@ export default function FrankNode(properties: NodeProps) { transform: 'translate(50%, -50%)', width: `${COMPACT_HANDLE_SIZE}px`, height: `${COMPACT_HANDLE_SIZE}px`, - backgroundColor: translateHandleTypeToColour(properties.data.sourceHandles[0].type), + backgroundColor: '#B2B2B2', border: '1px solid rgba(107, 114, 128, 0.5)', }} /> diff --git a/src/test/java/org/frankframework/flow/frankdoc/FrankDocServiceTest.java b/src/test/java/org/frankframework/flow/frankdoc/FrankDocServiceTest.java index fc5cafcb..d560c2ef 100644 --- a/src/test/java/org/frankframework/flow/frankdoc/FrankDocServiceTest.java +++ b/src/test/java/org/frankframework/flow/frankdoc/FrankDocServiceTest.java @@ -20,7 +20,7 @@ class FrankDocServiceTest { private FrankDocService frankDocService; - private static final String FRANKDOC_URL = "https://frankdoc.frankframework.org/js/frankdoc.json"; + private static final String FRANKDOC_URL = "https://reference.frankframework.org/js/frankdoc.json"; @BeforeEach void setUp() {