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`