From e295ff2fc1264d1d4f6c5eb2b2c497f242fcf55e Mon Sep 17 00:00:00 2001 From: Guillaume Coutable Date: Thu, 18 Jun 2026 11:30:21 +0200 Subject: [PATCH] [2239] Add the support for a frontend tool to rotate Fork and Join nodes Bug: https://github.com/eclipse-syson/syson/issues/2239 Signed-off-by: Guillaume Coutable --- CHANGELOG.adoc | 1 + .../ForkJoinNodePaletteToolProvider.java | 97 ++++++++++++++++++ .../ForkActionNodeDescriptionProvider.java | 2 +- ...notes-rotate-fork-join-graphical-nodes.png | Bin 0 -> 30382 bytes .../pages/release-notes/2026.7.0.adoc | 3 + .../registry/SysONExtensionRegistry.tsx | 7 ++ .../RotateNodeToolOverriddenContribution.tsx | 46 +++++++++ .../rotateNodeTool/useRotateNode.ts | 93 +++++++++++++++++ .../rotateNodeTool/useRotateNode.types.ts | 16 +++ .../playwright/e2e/rotate-node.spec.ts | 82 +++++++++++++++ 10 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/nodes/services/ForkJoinNodePaletteToolProvider.java create mode 100644 doc/content/modules/user-manual/assets/images/release-notes-rotate-fork-join-graphical-nodes.png create mode 100644 frontend/syson-components/src/extensions/rotateNodeTool/RotateNodeToolOverriddenContribution.tsx create mode 100644 frontend/syson-components/src/extensions/rotateNodeTool/useRotateNode.ts create mode 100644 frontend/syson-components/src/extensions/rotateNodeTool/useRotateNode.types.ts create mode 100644 integration-tests-playwright/playwright/e2e/rotate-node.spec.ts diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 0e9033290..8d29c4039 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -74,6 +74,7 @@ It leverages the selection dialog to either create an _occurrence timeslice/snap - https://github.com/eclipse-syson/syson/issues/2260[#2260] [diagrams] Add the _New Assume Constraint_ or _New Require Constraint_ edge tools to create _assume_ and _require_ graphical edges. - https://github.com/eclipse-syson/syson/issues/2113[#2113] [diagrams] Handle start/end/merge/decision... graphical nodes on Action Flow View diagram background - https://github.com/eclipse-syson/syson/issues/2303[#2303] [diagrams] Add support for list item inheritance in _states_ and _exhibit states_ compartments +- https://github.com/eclipse-syson/syson/issues/2239[#2239] [diagrams] Add the support for a frontend tool to rotate `ForkNode` and `JoinNode` graphical nodes == v2026.5.0 diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/nodes/services/ForkJoinNodePaletteToolProvider.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/nodes/services/ForkJoinNodePaletteToolProvider.java new file mode 100644 index 000000000..af485b6c4 --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/nodes/services/ForkJoinNodePaletteToolProvider.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.syson.application.nodes.services; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.sirius.components.collaborative.diagrams.DiagramContext; +import org.eclipse.sirius.components.collaborative.diagrams.dto.ITool; +import org.eclipse.sirius.components.collaborative.diagrams.dto.SingleClickOnDiagramElementTool; +import org.eclipse.sirius.components.collaborative.diagrams.dto.ToolSection; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IObjectSearchService; +import org.eclipse.sirius.components.diagrams.Node; +import org.eclipse.sirius.components.diagrams.description.IDiagramElementDescription; +import org.eclipse.sirius.components.view.emf.diagram.api.IPaletteToolsProvider; +import org.eclipse.syson.sysml.ForkNode; +import org.eclipse.syson.sysml.JoinNode; +import org.springframework.stereotype.Service; + +/** + * Used to contribute the Rotate tool for {@link ForkNode} and {@link JoinNode} graphical nodes. + *

+ * This {@link IPaletteToolsProvider} only declare the tool. + * The behavior is contributed in the frontend. + *

+ * + * @author gcoutable + */ +@Service +public class ForkJoinNodePaletteToolProvider implements IPaletteToolsProvider { + + private static final String FORK_JOIN_NODE_ROTATE_TOOL_ID = "fork_join_node_rotate_tool"; + + private static final String FORK_JOIN_NODE_ROTATE_TOOL_LABEL = "Rotate"; + + private static final String FORK_JOIN_NODE_ROTATE_TOOL_SECTION_ID = "edit-section"; + + private static final String FORK_JOIN_NODE_ROTATE_TOOL_SECTION_LABEL = "Edit"; + + private final IObjectSearchService objectSearchService; + + public ForkJoinNodePaletteToolProvider(IObjectSearchService objectSearchService) { + this.objectSearchService = Objects.requireNonNull(objectSearchService); + } + + @Override + public List createExtraToolSections(IEditingContext editingContext, DiagramContext diagramContext, Object diagramElementDescription, Object diagramElement) { + List extraTool = new ArrayList<>(); + + var optionalTargetObjectId = this.getTargetObjectId(diagramElement); + + if (optionalTargetObjectId.isPresent() && diagramElementDescription instanceof IDiagramElementDescription nodeDescription) { + var semanticObject = this.objectSearchService.getObject(editingContext, optionalTargetObjectId.get()); + if (semanticObject.isPresent() && (semanticObject.get() instanceof ForkNode || semanticObject.get() instanceof JoinNode)) { + ITool tool = SingleClickOnDiagramElementTool.newSingleClickOnDiagramElementTool(FORK_JOIN_NODE_ROTATE_TOOL_ID) + .label(FORK_JOIN_NODE_ROTATE_TOOL_LABEL) + .targetDescriptions(List.of(nodeDescription)) + .iconURL(List.of()) + .keyBindings(List.of()) + .build(); + extraTool.add(tool); + } + } + + if (!extraTool.isEmpty()) { + return List.of(new ToolSection(FORK_JOIN_NODE_ROTATE_TOOL_SECTION_ID, FORK_JOIN_NODE_ROTATE_TOOL_SECTION_LABEL, List.of(), extraTool)); + } else { + return List.of(); + } + } + + private Optional getTargetObjectId(Object diagramElement) { + Optional result = Optional.empty(); + if (diagramElement instanceof Node node) { + result = Optional.of(node.getTargetObjectId()); + } + return result; + } + + @Override + public List createQuickAccessTools(IEditingContext editingContext, DiagramContext diagramContext, Object diagramElementDescription, Object diagramElement) { + return List.of(); + } +} diff --git a/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/nodes/ForkActionNodeDescriptionProvider.java b/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/nodes/ForkActionNodeDescriptionProvider.java index 96c93efab..c6abd2cd7 100644 --- a/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/nodes/ForkActionNodeDescriptionProvider.java +++ b/backend/views/syson-diagram-common-view/src/main/java/org/eclipse/syson/diagram/common/view/nodes/ForkActionNodeDescriptionProvider.java @@ -49,7 +49,7 @@ protected String getRemoveToolLabel() { @Override protected UserResizableDirection isNodeResizable() { - return UserResizableDirection.HORIZONTAL; + return UserResizableDirection.BOTH; } @Override diff --git a/doc/content/modules/user-manual/assets/images/release-notes-rotate-fork-join-graphical-nodes.png b/doc/content/modules/user-manual/assets/images/release-notes-rotate-fork-join-graphical-nodes.png new file mode 100644 index 0000000000000000000000000000000000000000..12bd15f470c0a741712c00ad78d6c93578f48950 GIT binary patch literal 30382 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%0A0$*Cg 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 { + return tool.id === 'fork_join_node_rotate_tool'; + }, + component: RotateNodeToolOverriddenContribution, + }, ]; sysONExtensionRegistry.putData(paletteToolOverrideExtensionPoint, { diff --git a/frontend/syson-components/src/extensions/rotateNodeTool/RotateNodeToolOverriddenContribution.tsx b/frontend/syson-components/src/extensions/rotateNodeTool/RotateNodeToolOverriddenContribution.tsx new file mode 100644 index 000000000..7dead179d --- /dev/null +++ b/frontend/syson-components/src/extensions/rotateNodeTool/RotateNodeToolOverriddenContribution.tsx @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +import { DiagramContext, DiagramContextValue } from '@eclipse-sirius/sirius-components-diagrams'; +import { PaletteToolContributionComponentProps } from '@eclipse-sirius/sirius-components-palette'; +import Rotate90DegreesCwIcon from '@mui/icons-material/Rotate90DegreesCw'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import { Fragment, useContext } from 'react'; +import { useRotateNode } from './useRotateNode'; + +export const RotateNodeToolOverriddenContribution = ({ + representationElementIds, +}: PaletteToolContributionComponentProps) => { + const { readOnly } = useContext(DiagramContext); + const { rotate } = useRotateNode(); + + return ( + + rotate(representationElementIds)} + data-testid="overridden_tool_node-rotate" + disabled={readOnly} + sx={{ paddingTop: 0, paddingBottom: 0 }}> + theme.spacing(2), color: (theme) => theme.palette.text.primary }}> + + + + + + ); +}; diff --git a/frontend/syson-components/src/extensions/rotateNodeTool/useRotateNode.ts b/frontend/syson-components/src/extensions/rotateNodeTool/useRotateNode.ts new file mode 100644 index 000000000..053165f99 --- /dev/null +++ b/frontend/syson-components/src/extensions/rotateNodeTool/useRotateNode.ts @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +import { EdgeData, NodeData, useLayout, useSynchronizeLayoutData } from '@eclipse-sirius/sirius-components-diagrams'; +import { Edge, Node, useReactFlow } from '@xyflow/react'; +import { UseRotateNodeValue } from './useRotateNode.types'; + +export const useRotateNode = (): UseRotateNodeValue => { + const { getNodes, getEdges, setNodes, setEdges } = useReactFlow, Edge>(); + const { synchronizeLayoutData } = useSynchronizeLayoutData(); + const { layout } = useLayout(); + + const rotate = (representationElementIds: string[]) => { + const updatedNodes: Node[] = getNodes().map((node) => { + if (representationElementIds.length > 0 && node.id === representationElementIds[0]) { + const cx = node.position.x + (node.width ?? 0) / 2; + const cy = node.position.y + (node.height ?? 0) / 2; + + const newWidth = node.height ?? 0; + const newHeight = node.width ?? 0; + + const newX = cx - newWidth / 2; + const newY = cy - newHeight / 2; + return { + ...node, + position: { x: newX, y: newY }, + width: newWidth, + height: newHeight, + data: { ...node.data, resizedByUser: true, connectionHandles: [] }, + }; + } + return node; + }); + + const updatedEdges: Edge[] = getEdges().map((edge) => { + if ( + ((representationElementIds.length > 0 && edge.source === representationElementIds[0]) || + edge.target === representationElementIds[0]) && + edge.data + ) { + return { + ...edge, + data: { + ...edge.data, + bendingPoints: null, + }, + }; + } + return edge; + }); + + const diagramToLayout = { + nodes: updatedNodes, + edges: updatedEdges, + }; + + layout(diagramToLayout, diagramToLayout, null, 'UNDEFINED', (laidOutDiagram) => { + const updatedNodesAfterLayout = updatedNodes.map((node) => { + if (representationElementIds.find((nodeId) => nodeId === node.id)) { + return laidOutDiagram.nodes.find((laidOutNode) => laidOutNode.id === node.id) ?? node; + } + return node; + }); + + const updatedEdgesAfterLayout = updatedEdges.map((edge) => { + if (representationElementIds.find((nodeId) => nodeId === edge.source || nodeId === edge.target)) { + return laidOutDiagram.edges.find((laidOutEdge) => laidOutEdge.id === edge.id) ?? edge; + } + return edge; + }); + + setNodes(updatedNodesAfterLayout); + setEdges(updatedEdgesAfterLayout); + const finalDiagram = { + nodes: updatedNodesAfterLayout, + edges: updatedEdgesAfterLayout, + }; + synchronizeLayoutData(crypto.randomUUID(), 'layout', finalDiagram, 'UNCHANGED'); + }); + }; + + return { rotate }; +}; diff --git a/frontend/syson-components/src/extensions/rotateNodeTool/useRotateNode.types.ts b/frontend/syson-components/src/extensions/rotateNodeTool/useRotateNode.types.ts new file mode 100644 index 000000000..e3e9cd519 --- /dev/null +++ b/frontend/syson-components/src/extensions/rotateNodeTool/useRotateNode.types.ts @@ -0,0 +1,16 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +export interface UseRotateNodeValue { + rotate: (representationElementIds: string[]) => void; +} diff --git a/integration-tests-playwright/playwright/e2e/rotate-node.spec.ts b/integration-tests-playwright/playwright/e2e/rotate-node.spec.ts new file mode 100644 index 000000000..295914ba5 --- /dev/null +++ b/integration-tests-playwright/playwright/e2e/rotate-node.spec.ts @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) 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 + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +import { expect, test } from '@playwright/test'; +import { PlaywrightExplorer } from '../helpers/PlaywrightExplorer'; +import { PlaywrightNode } from '../helpers/PlaywrightNode'; +import { PlaywrightProject } from '../helpers/PlaywrightProject'; + +test.describe('diagram - general view', () => { + let projectId; + test.beforeEach(async ({ page, request }) => { + await page.addInitScript(() => { + // @ts-expect-error: we use a variable in the DOM to disable `fitView` functionality for Cypress tests. + window.document.DEACTIVATE_FIT_VIEW_FOR_CYPRESS_TESTS = true; + }); + const project = await new PlaywrightProject(request).createSysMLV2Project('general'); + projectId = project.projectId; + + await page.goto(`/projects/${projectId}/edit`); + const playwrightExplorer = new PlaywrightExplorer(page); + await playwrightExplorer.uploadDocument('SysMLv2WithGeneralView.sysml'); + await playwrightExplorer.expand('SysMLv2WithGeneralView.sysml'); + await playwrightExplorer.expand('Package1'); + await playwrightExplorer.createRepresentation('view1 [GeneralView]', 'General View', 'view1'); + + await page.getByTestId('arrange-all-main-button').click(); + + const partNode = new PlaywrightNode(page, 'part1', 'List'); + await expect(partNode.nodeLocator).toBeAttached(); + + // Keep the diagram content visible without depending on Sirius Web's arrange-all toolbar controls. + await page.getByTestId('zoom-out').click(); + }); + + test.afterEach(async ({ request }) => { + await new PlaywrightProject(request).deleteProject(projectId); + }); + + test('WHEN applying the rotate tool on a fork node, THEN the node is rotated', async ({ page, browserName }) => { + if (browserName === 'firefox') { + test.skip(); //The test fails inexplicably in Firefox. + } + + // Create a new Action node + await page.getByTestId('rf__wrapper').click({ button: 'right', position: { x: 250, y: 250 } }); + await expect(page.getByTestId('Palette')).toBeAttached(); + await page.getByTestId('toolSection-Behavior').click(); + await page.getByTestId('tool-New Action').click(); + const playwrightActionNode = new PlaywrightNode(page, 'action1', 'List'); + await expect(playwrightActionNode.nodeLocator).toBeAttached(); + + // Create a new Fork node in the Action node + await playwrightActionNode.openPalette(); + await page.getByTestId('toolSection-Behavior').click(); + await page.getByTestId('tool-New Fork').click(); + const playwrightForkNode = new PlaywrightNode(page, 'forkNode1'); + await expect(playwrightForkNode.nodeLocator).toBeAttached(); + const {width, height} = await playwrightForkNode.getReactFlowSize(); + const expectedRotatedWidth = height; + const expectedRotatedHeight = width; + + // Apply the rotate tool on the Fork node + await playwrightForkNode.openPalette(); + await page.getByTestId('toolSection-Edit').click(); + await page.getByTestId('overridden_tool_node-rotate').click(); + await playwrightForkNode.closePalette(); + const rotatedSize = await playwrightForkNode.getReactFlowSize(); + + expect(rotatedSize.width).toBeCloseTo(expectedRotatedWidth, 0); + expect(rotatedSize.height).toBeCloseTo(expectedRotatedHeight, 0); + }); + +}); \ No newline at end of file