Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ The new template for functional specification is located in `doc/iterations/YYYY
The technical specifications will now rely on a new architectural decision record template located in `doc/adrs/adr.adoc`.
This new template for ADRs will have several additional requirements compared to the previous one.
It will require contributors to specify the persons involved in the decision process, to details the qualities expected of the solution, to list the various options considered, to clarify why a specific option has been selected, its advantages and drawbacks, which steps will be taken for the implementation among other things.
- https://github.com/eclipse-sirius/sirius-web/issues/6338[#6338] [diagram] Move the execution of connector tools in a dedicated hook



Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023, 2024 Obeo.
* Copyright (c) 2023, 2026 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
Expand All @@ -22,7 +22,7 @@ import { GQLNodeDescription } from './useConnector.types';

const defaultValue: ConnectorContextValue = {
connection: null,
position: null,
position: { x: 0, y: 0 },
candidates: [],
isNewConnection: false,
setConnection: () => {},
Expand All @@ -37,7 +37,7 @@ export const ConnectorContext = React.createContext<ConnectorContextValue>(defau
export const ConnectorContextProvider = ({ children }: ConnectorContextProviderProps) => {
const [state, setState] = useState<ConnectorContextProviderState>({
connection: null,
position: null,
position: { x: 0, y: 0 },
candidates: [],
isNewConnection: false,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023, 2024 Obeo.
* Copyright (c) 2023, 2026 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
Expand All @@ -16,7 +16,7 @@ import { GQLNodeDescription } from './useConnector.types';

export interface ConnectorContextValue {
connection: Connection | null;
position: XYPosition | null;
position: XYPosition;
candidates: GQLNodeDescription[];
isNewConnection: boolean;
setConnection: (connection: Connection) => void;
Expand All @@ -32,7 +32,7 @@ export interface ConnectorContextProviderProps {

export interface ConnectorContextProviderState {
connection: Connection | null;
position: XYPosition | null;
position: XYPosition;
candidates: GQLNodeDescription[];
isNewConnection: boolean;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023, 2025 Obeo.
* Copyright (c) 2023, 2026 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
Expand All @@ -11,37 +11,27 @@
* Obeo - initial API and implementation
*******************************************************************************/

import { gql, useMutation, useQuery } from '@apollo/client';
import { gql, useQuery } from '@apollo/client';
import { IconOverlay, useMultiToast } from '@eclipse-sirius/sirius-components-core';
import ListItemIcon from '@mui/material/ListItemIcon';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Typography from '@mui/material/Typography';
import { Edge, Node, useReactFlow, useStoreApi, XYPosition } from '@xyflow/react';
import { Edge, Node, useReactFlow } from '@xyflow/react';
import { memo, useContext, useEffect } from 'react';
import { DiagramContext } from '../../contexts/DiagramContext';
import { DiagramContextValue } from '../../contexts/DiagramContext.types';
import { DiagramDialogVariable } from '../../dialog/DialogContextExtensionPoints.types';
import { useDialog } from '../../dialog/useDialog';
import { EdgeData, NodeData } from '../DiagramRenderer.types';
import { isCursorNearCenterOfTheNode } from '../edge/EdgeLayout';
import {
ConnectorContextualMenuProps,
GetConnectorToolsData,
GetConnectorToolsVariables,
GQLDiagramDescription,
GQLErrorPayload,
GQLInvokeSingleClickOnTwoDiagramElementsToolData,
GQLInvokeSingleClickOnTwoDiagramElementsToolInput,
GQLInvokeSingleClickOnTwoDiagramElementsToolPayload,
GQLInvokeSingleClickOnTwoDiagramElementsToolSuccessPayload,
GQLInvokeSingleClickOnTwoDiagramElementsToolVariables,
GQLRepresentationDescription,
GQLTool,
GQLToolVariable,
} from './ConnectorContextualMenu.types';
import { useConnector } from './useConnector';
import { GQLSingleClickOnTwoDiagramElementsTool } from './useConnector.types';
import { useSingleClickOnTwoDiagramElementTool } from './useSingleClickOnTwoDiagramElementTool';

export const getConnectorToolsQuery = gql`
query getConnectorTools(
Expand Down Expand Up @@ -74,56 +64,18 @@ export const getConnectorToolsQuery = gql`
}
`;

export const invokeSingleClickOnTwoDiagramElementsToolMutation = gql`
mutation invokeSingleClickOnTwoDiagramElementsTool($input: InvokeSingleClickOnTwoDiagramElementsToolInput!) {
invokeSingleClickOnTwoDiagramElementsTool(input: $input) {
__typename
... on InvokeSingleClickOnTwoDiagramElementsToolSuccessPayload {
id
newSelection {
entries {
id
}
}
messages {
body
level
}
}
... on ErrorPayload {
messages {
body
level
}
}
}
}
`;

const isDiagramDescription = (
representationDescription: GQLRepresentationDescription
): representationDescription is GQLDiagramDescription => representationDescription.__typename === 'DiagramDescription';

const isErrorPayload = (payload: GQLInvokeSingleClickOnTwoDiagramElementsToolPayload): payload is GQLErrorPayload =>
payload.__typename === 'ErrorPayload';
const isSuccessPayload = (
payload: GQLInvokeSingleClickOnTwoDiagramElementsToolPayload
): payload is GQLInvokeSingleClickOnTwoDiagramElementsToolSuccessPayload =>
payload.__typename === 'InvokeSingleClickOnTwoDiagramElementsToolSuccessPayload';

const isSingleClickOnTwoDiagramElementsTool = (tool: GQLTool): tool is GQLSingleClickOnTwoDiagramElementsTool =>
tool.__typename === 'SingleClickOnTwoDiagramElementsTool';

const ConnectorContextualMenuComponent = memo(({}: ConnectorContextualMenuProps) => {
const { editingContextId, diagramId, registerPostToolSelection } = useContext<DiagramContextValue>(DiagramContext);
const store = useStoreApi<Node<NodeData>, Edge<EdgeData>>();
const { editingContextId, diagramId } = useContext<DiagramContextValue>(DiagramContext);
const { connection, position, onConnectorContextualMenuClose, addTempConnectionLine, removeTempConnectionLine } =
useConnector();
const { addMessages, addErrorMessage } = useMultiToast();

const { showDialog, isOpened } = useDialog();

const { getNodes, screenToFlowPosition } = useReactFlow<Node<NodeData>, Edge<EdgeData>>();
const { screenToFlowPosition } = useReactFlow<Node<NodeData>, Edge<EdgeData>>();
const { invokeConnectorTool, data: invokeSingleClickOnTwoDiagramElementToolCalled } =
useSingleClickOnTwoDiagramElementTool();

const connectionSource: HTMLElement | null = connection
? document.querySelector(`[data-id="${connection.source}"]`)
Expand All @@ -147,116 +99,12 @@ const ConnectorContextualMenuComponent = memo(({}: ConnectorContextualMenuProps)
skip: !connectionSource || !connectionTarget,
});

const invokeOpenSelectionDialog = (tool: GQLSingleClickOnTwoDiagramElementsTool) => {
const onConfirm = (variables: GQLToolVariable[]) => {
invokeToolMutation(tool, variables);
};

const onClose = () => {
onShouldConnectorContextualMenuClose();
};

const sourceNode = getNodes().find((node) => node.id === sourceDiagramElementId);
const targetNode = getNodes().find((node) => node.id === targetDiagramElementId);
if (sourceNode && targetNode) {
const variables: DiagramDialogVariable[] = [
{ name: 'targetObjectId', value: sourceNode.data.targetObjectId },
{ name: 'sourceDiagramElementTargetObjectId', value: sourceNode.data.targetObjectId },
{ name: 'targetDiagramElementTargetObjectId', value: targetNode.data.targetObjectId },
];
showDialog(tool.dialogDescriptionId, variables, onConfirm, onClose);
}
};

useEffect(() => {
if (error) {
addErrorMessage(error.message);
}
}, [error]);

const [
invokeSingleClickOnTwoDiagramElementsTool,
{
data: invokeSingleClickOnTwoDiagramElementToolData,
error: invokeSingleClickOnTwoDiagramElementToolError,
called: invokeSingleClickOnTwoDiagramElementToolCalled,
reset,
},
] = useMutation<
GQLInvokeSingleClickOnTwoDiagramElementsToolData,
GQLInvokeSingleClickOnTwoDiagramElementsToolVariables
>(invokeSingleClickOnTwoDiagramElementsToolMutation);

const invokeTool = (tool: GQLTool) => {
if (isSingleClickOnTwoDiagramElementsTool(tool)) {
if (tool.dialogDescriptionId) {
if (!isOpened) {
invokeOpenSelectionDialog(tool);
}
} else {
invokeToolMutation(tool, []);
}
}
};

const invokeToolMutation = (tool: GQLTool, variables: GQLToolVariable[]) => {
let targetHandlePosition: XYPosition = { x: 0, y: 0 };
if (position) {
targetHandlePosition = screenToFlowPosition({ x: position.x, y: position.y });
}
const target = store.getState().nodeLookup.get(targetDiagramElementId);
if (target && position) {
const isNearCenter = isCursorNearCenterOfTheNode(target, {
x: targetHandlePosition.x,
y: targetHandlePosition.y,
});

if (isNearCenter) {
targetHandlePosition = { x: 0, y: 0 };
}
}

const input: GQLInvokeSingleClickOnTwoDiagramElementsToolInput = {
id: crypto.randomUUID(),
editingContextId,
representationId: diagramId,
diagramSourceElementId: sourceDiagramElementId,
diagramTargetElementId: targetDiagramElementId,
toolId: tool.id,
sourcePositionX: 0,
sourcePositionY: 0,
targetPositionX: targetHandlePosition.x,
targetPositionY: targetHandlePosition.y,
variables,
};
invokeSingleClickOnTwoDiagramElementsTool({ variables: { input } });
};

const onShouldConnectorContextualMenuClose = () => {
onConnectorContextualMenuClose();
reset();
};

useEffect(() => {
if (invokeSingleClickOnTwoDiagramElementToolData) {
const payload = invokeSingleClickOnTwoDiagramElementToolData.invokeSingleClickOnTwoDiagramElementsTool;
if (isErrorPayload(payload)) {
addMessages(payload.messages);
}
if (isSuccessPayload(payload)) {
const { id, newSelection } = payload;
if (newSelection?.entries.length ?? 0 > 0) {
registerPostToolSelection(id, newSelection);
}
addMessages(payload.messages);
onShouldConnectorContextualMenuClose();
}
}
if (invokeSingleClickOnTwoDiagramElementToolError?.message) {
addErrorMessage(invokeSingleClickOnTwoDiagramElementToolError.message);
}
}, [invokeSingleClickOnTwoDiagramElementToolData, invokeSingleClickOnTwoDiagramElementToolError]);

const connectorTools: GQLTool[] = [];
const representationDescription: GQLRepresentationDescription | null | undefined =
data?.viewer.editingContext?.representation?.description;
Expand All @@ -272,6 +120,7 @@ const ConnectorContextualMenuComponent = memo(({}: ConnectorContextualMenuProps)

useEffect(() => {
if (!loading && connection && data && connectorTools.length === 0) {
onConnectorContextualMenuClose();
addMessages([{ body: 'No edge found between source and target selected', level: 'WARNING' }]);
}
}, [loading, data, connection, connectorTools.length]);
Expand All @@ -284,7 +133,12 @@ const ConnectorContextualMenuComponent = memo(({}: ConnectorContextualMenuProps)
if (!invokeSingleClickOnTwoDiagramElementToolCalled && connectorTools.length === 1 && connectorTools[0]) {
invokeTool(connectorTools[0]);
}
}, [connectorTools]);
}, [connectorTools.length]);

const invokeTool = (tool: GQLTool) => {
const { x: cursorPositionX, y: cursorPositionY } = screenToFlowPosition({ x: position.x, y: position.y });
invokeConnectorTool(tool, sourceDiagramElementId, targetDiagramElementId, cursorPositionX, cursorPositionY);
};

if (!data || connectorTools.length <= 1) {
return null;
Expand All @@ -293,7 +147,7 @@ const ConnectorContextualMenuComponent = memo(({}: ConnectorContextualMenuProps)
return (
<Menu
open={!!connection}
onClose={onShouldConnectorContextualMenuClose}
onClose={onConnectorContextualMenuClose}
anchorEl={connectionTarget}
anchorReference="anchorPosition"
data-testid="connectorContextualMenu"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023, 2025 Obeo.
* Copyright (c) 2023, 2026 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
Expand Down Expand Up @@ -113,8 +113,6 @@ export const useConnector = (): UseConnectorValue => {
[]
);

const onConnectorContextualMenuClose = () => resetConnection();

const onConnectionStartElementClick = useCallback((event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (event.button === 0) {
setIsNewConnection(true);
Expand Down Expand Up @@ -198,7 +196,7 @@ export const useConnector = (): UseConnectorValue => {
onConnect,
onConnectStart,
onConnectEnd,
onConnectorContextualMenuClose,
onConnectorContextualMenuClose: resetConnection,
onConnectionStartElementClick,
addTempConnectionLine,
removeTempConnectionLine,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023, 2025 Obeo.
* Copyright (c) 2023, 2026 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
Expand All @@ -23,7 +23,7 @@ export interface UseConnectorValue {
addTempConnectionLine: () => void;
removeTempConnectionLine: () => void;
connection: Connection | null;
position: XYPosition | null;
position: XYPosition;
isConnectionInProgress: () => boolean;
isReconnectionInProgress: () => boolean;
candidates: GQLNodeDescription[];
Expand Down
Loading
Loading