diff --git a/workspaces/ballerina/component-diagram/src/components/nodes/ConnectionNode/ConnectionNodeWidget.tsx b/workspaces/ballerina/component-diagram/src/components/nodes/ConnectionNode/ConnectionNodeWidget.tsx index 9dad8e2f8ed..115b4ecc113 100644 --- a/workspaces/ballerina/component-diagram/src/components/nodes/ConnectionNode/ConnectionNodeWidget.tsx +++ b/workspaces/ballerina/component-diagram/src/components/nodes/ConnectionNode/ConnectionNodeWidget.tsx @@ -164,6 +164,14 @@ export function ConnectionNodeWidget(props: ConnectionNodeWidgetProps) { setMenuAnchorEl(null); }; + const handleMenuMouseDown = (event: React.MouseEvent) => { + event.stopPropagation(); + }; + + const handleMenuMouseUp = (event: React.MouseEvent) => { + event.stopPropagation(); + }; + const getNodeTitle = () => { return model.node.symbol; }; @@ -199,7 +207,12 @@ export function ConnectionNodeWidget(props: ConnectionNodeWidgetProps) { {getNodeTitle()} {getNodeDescription()} - + diff --git a/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/components/AIServiceWidget.tsx b/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/components/AIServiceWidget.tsx index 0ef835f0750..3a9fe32b58b 100644 --- a/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/components/AIServiceWidget.tsx +++ b/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/components/AIServiceWidget.tsx @@ -81,6 +81,14 @@ export function AIServiceWidget({ model, engine }: BaseNodeWidgetProps) { setMenuAnchorEl(null); }; + const handleMenuMouseDown = (event: React.MouseEvent) => { + event.stopPropagation(); + }; + + const handleMenuMouseUp = (event: React.MouseEvent) => { + event.stopPropagation(); + }; + const menuItems: Item[] = [ { id: "edit", label: "Edit", onClick: () => handleOnClick() }, { id: "delete", label: "Delete", onClick: () => onDeleteComponent(model.node) }, @@ -101,7 +109,12 @@ export function AIServiceWidget({ model, engine }: BaseNodeWidgetProps) { {getNodeTitle(model)} {getNodeDescription(model)} - + diff --git a/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/components/GeneralWidget.tsx b/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/components/GeneralWidget.tsx index 5e24186426a..128d13e62ed 100644 --- a/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/components/GeneralWidget.tsx +++ b/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/components/GeneralWidget.tsx @@ -197,6 +197,14 @@ export function GeneralServiceWidget({ model, engine }: BaseNodeWidgetProps) { setMenuAnchorEl(null); }; + const handleMenuMouseDown = (event: React.MouseEvent) => { + event.stopPropagation(); + }; + + const handleMenuMouseUp = (event: React.MouseEvent) => { + event.stopPropagation(); + }; + const menuItems: Item[] = [ { id: "edit", label: "Edit", onClick: () => handleOnClick() }, { id: "delete", label: "Delete", onClick: () => onDeleteComponent(model.node) }, @@ -251,7 +259,12 @@ export function GeneralServiceWidget({ model, engine }: BaseNodeWidgetProps) { {getNodeTitle(model)} {getNodeDescription(model)} - + diff --git a/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/components/GraphQLServiceWidget.tsx b/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/components/GraphQLServiceWidget.tsx index 9b9cfa21ee9..d55833bdba0 100644 --- a/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/components/GraphQLServiceWidget.tsx +++ b/workspaces/ballerina/component-diagram/src/components/nodes/EntryNode/components/GraphQLServiceWidget.tsx @@ -191,6 +191,14 @@ export function GraphQLServiceWidget({ model, engine }: BaseNodeWidgetProps) { setMenuAnchorEl(null); }; + const handleMenuMouseDown = (event: React.MouseEvent) => { + event.stopPropagation(); + }; + + const handleMenuMouseUp = (event: React.MouseEvent) => { + event.stopPropagation(); + }; + const menuItems: Item[] = [ { id: "edit", label: "Edit", onClick: () => handleOnClick() }, { id: "delete", label: "Delete", onClick: () => onDeleteComponent(model.node) }, @@ -245,7 +253,12 @@ export function GraphQLServiceWidget({ model, engine }: BaseNodeWidgetProps) { {getNodeTitle(model)} {getNodeDescription(model)} - + diff --git a/workspaces/ballerina/component-diagram/src/components/nodes/ListenerNode/ListenerNodeWidget.tsx b/workspaces/ballerina/component-diagram/src/components/nodes/ListenerNode/ListenerNodeWidget.tsx index 35bd8a204e1..0441df85f7e 100644 --- a/workspaces/ballerina/component-diagram/src/components/nodes/ListenerNode/ListenerNodeWidget.tsx +++ b/workspaces/ballerina/component-diagram/src/components/nodes/ListenerNode/ListenerNodeWidget.tsx @@ -174,6 +174,14 @@ export function ListenerNodeWidget(props: ListenerNodeWidgetProps) { setMenuAnchorEl(null); }; + const handleMenuMouseDown = (event: React.MouseEvent) => { + event.stopPropagation(); + }; + + const handleMenuMouseUp = (event: React.MouseEvent) => { + event.stopPropagation(); + }; + const menuItems: Item[] = [ { id: "edit", label: "Edit", onClick: () => handleOnClick() }, @@ -196,7 +204,12 @@ export function ListenerNodeWidget(props: ListenerNodeWidgetProps) { - +
diff --git a/workspaces/ballerina/component-diagram/src/utils/diagram.ts b/workspaces/ballerina/component-diagram/src/utils/diagram.ts index 0bde6be9e87..51ee6666b2d 100644 --- a/workspaces/ballerina/component-diagram/src/utils/diagram.ts +++ b/workspaces/ballerina/component-diagram/src/utils/diagram.ts @@ -24,7 +24,7 @@ import { NodeModel } from "./types"; import { EntryNodeFactory, EntryNodeModel } from "../components/nodes/EntryNode"; import { ConnectionNodeFactory } from "../components/nodes/ConnectionNode/ConnectionNodeFactory"; import { ListenerNodeFactory } from "../components/nodes/ListenerNode/ListenerNodeFactory"; -import { LISTENER_NODE_WIDTH, NodeTypes, NODE_GAP_X, ENTRY_NODE_WIDTH } from "../resources/constants"; +import { LISTENER_NODE_WIDTH, NodeTypes, NODE_GAP_X, ENTRY_NODE_WIDTH, NODE_GAP_Y, LISTENER_NODE_HEIGHT } from "../resources/constants"; import { ListenerNodeModel } from "../components/nodes/ListenerNode"; import { ConnectionNodeModel } from "../components/nodes/ConnectionNode"; import { CDConnection, CDResourceFunction, CDFunction, CDService } from "@wso2/ballerina-core"; @@ -64,16 +64,26 @@ export function autoDistribute(engine: DiagramEngine) { const entryX = listenerX + LISTENER_NODE_WIDTH + NODE_GAP_X; const connectionX = entryX + ENTRY_NODE_WIDTH + NODE_GAP_X; - // Position listeners while maintaining relative Y positions of their services + // Separate listeners into connected and unconnected + const connectedListeners: ListenerNodeModel[] = []; + const unconnectedListeners: ListenerNodeModel[] = []; + listenerNodes.forEach((node) => { const listenerNode = node as ListenerNodeModel; const attachedServices = listenerNode.node.attachedServices; - // Find the average Y position of attached services + // Find the attached service nodes const serviceNodes = entryNodes.filter((n) => attachedServices.includes(n.getID())); - const avgY = serviceNodes.reduce((sum, n) => sum + n.getY(), 0) / serviceNodes.length; - listenerNode.setPosition(listenerX, avgY); + if (serviceNodes.length > 0) { + // Has attached services - position at average Y of services + const avgY = serviceNodes.reduce((sum, n) => sum + n.getY(), 0) / serviceNodes.length; + listenerNode.setPosition(listenerX, avgY); + connectedListeners.push(listenerNode); + } else { + // No attached services - will position later + unconnectedListeners.push(listenerNode); + } }); // Update X positions for entry nodes while keeping their Y positions @@ -88,6 +98,26 @@ export function autoDistribute(engine: DiagramEngine) { connectionNode.setPosition(connectionX, node.getY()); }); + // Position unconnected listeners below all other nodes + if (unconnectedListeners.length > 0) { + // Find the maximum Y position among all nodes + const allNodes = [...connectedListeners, ...entryNodes, ...connectionNodes]; + let maxY = 100; // Default starting position if no other nodes + + if (allNodes.length > 0) { + maxY = Math.max(...allNodes.map(node => { + const nodeHeight = node.height || LISTENER_NODE_HEIGHT; + return node.getY() + nodeHeight; + })); + } + + // Position unconnected listeners below, with spacing + unconnectedListeners.forEach((listenerNode, index) => { + const yPosition = maxY + NODE_GAP_Y/2 + (index * (LISTENER_NODE_HEIGHT + NODE_GAP_Y/2)); + listenerNode.setPosition(listenerX, yPosition); + }); + } + engine.repaintCanvas(); } diff --git a/workspaces/common-libs/ui-toolkit/src/components/Button/Button.tsx b/workspaces/common-libs/ui-toolkit/src/components/Button/Button.tsx index 4d71b2f4692..7f6984b0338 100644 --- a/workspaces/common-libs/ui-toolkit/src/components/Button/Button.tsx +++ b/workspaces/common-libs/ui-toolkit/src/components/Button/Button.tsx @@ -32,6 +32,8 @@ export interface ButtonProps { sx?: React.CSSProperties; buttonSx?: React.CSSProperties; onClick?: (() => void) | ((event: React.MouseEvent) => void); + onMouseDown?: (event: React.MouseEvent) => void; + onMouseUp?: (event: React.MouseEvent) => void; } export const IconLabel = styled.div`