diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 9eb07b831e..f68003f5fb 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -69,6 +69,7 @@ Prevent the feature chain computer from traversing the whole standard library to - https://github.com/eclipse-syson/syson/issues/2486[#2486] [import/export] Resolve cross-document references for nested SysML files. - https://github.com/eclipse-syson/syson/issues/2466[#2466] [diagrams] Fix subclassification edge tool in _General View_ diagrams. - https://github.com/eclipse-syson/syson/issues/2485[#2485] [diagrams] Fix the _New Derived Requirement_ edge tool creating a derivation between two `RequirementUsage`. +- https://github.com/eclipse-syson/syson/issues/2474[#2474] [diagrams] Do not offer expression tools on inherited border nodes or inherited compartment items. === Improvements diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/services/ExpressionsPaletteToolsProvider.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/services/ExpressionsPaletteToolsProvider.java index f008ca776d..6868d371be 100644 --- a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/services/ExpressionsPaletteToolsProvider.java +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/services/ExpressionsPaletteToolsProvider.java @@ -25,7 +25,9 @@ import org.eclipse.sirius.components.diagrams.Edge; import org.eclipse.sirius.components.diagrams.Node; import org.eclipse.sirius.components.diagrams.description.IDiagramElementDescription; +import org.eclipse.sirius.components.diagrams.description.NodeDescription; import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.eclipse.sirius.components.view.emf.diagram.api.IViewDiagramDescriptionSearchService; import org.eclipse.sirius.components.palette.dto.ITool; import org.eclipse.sirius.components.palette.dto.ToolSection; import org.eclipse.sirius.components.view.emf.diagram.api.IPaletteToolsProvider; @@ -50,6 +52,10 @@ @Service public class ExpressionsPaletteToolsProvider implements IPaletteToolsProvider { + private static final String INHERITED_BORDER_NODE_DESCRIPTION_NAME_FRAGMENT = "InheritedBorderNode"; + + private static final String INHERITED_COMPARTMENT_ITEM_DESCRIPTION_NAME_FRAGMENT = "InheritedCompartmentItem"; + private static final String NEW_EXPRESSION_TOOL_ID = "tool_new_expression"; private static final String NEW_EXPRESSION_TOOL_LABEL = "New expression"; @@ -70,21 +76,49 @@ public class ExpressionsPaletteToolsProvider implements IPaletteToolsProvider { private final IReadOnlyObjectPredicate readOnlyObjectPredicate; + private final IViewDiagramDescriptionSearchService viewDiagramDescriptionSearchService; + private final MetamodelQueryElementService metamodelQueryElementService; - public ExpressionsPaletteToolsProvider(IObjectSearchService objectSearchService, IReadOnlyObjectPredicate readOnlyObjectPredicate) { + /** + * Creates the provider. + * + * @param objectSearchService + * the service used to retrieve diagram semantic targets + * @param readOnlyObjectPredicate + * the predicate used to identify read-only semantic targets + * @param viewDiagramDescriptionSearchService + * the service used to resolve runtime descriptions to their View descriptions + */ + public ExpressionsPaletteToolsProvider(IObjectSearchService objectSearchService, IReadOnlyObjectPredicate readOnlyObjectPredicate, + IViewDiagramDescriptionSearchService viewDiagramDescriptionSearchService) { this.objectSearchService = Objects.requireNonNull(objectSearchService); this.readOnlyObjectPredicate = Objects.requireNonNull(readOnlyObjectPredicate); + this.viewDiagramDescriptionSearchService = Objects.requireNonNull(viewDiagramDescriptionSearchService); this.metamodelQueryElementService = new MetamodelQueryElementService(); } + /** + * Contributes expression tools only when the selected diagram element is not inherited content. + * + * @param editingContext + * the current editing context + * @param diagramContext + * the current diagram context + * @param diagramElementDescription + * the selected diagram element description + * @param diagramElement + * the selected diagram element + * @return the expression tool section, or no section when no expression action applies + */ @Override public List createExtraToolSections(IEditingContext editingContext, DiagramContext diagramContext, Object diagramElementDescription, Object diagramElement) { var tools = new ArrayList(); var optionalTargetObjectId = this.getTargetObjectId(diagramElement); - if (optionalTargetObjectId.isPresent() && diagramElementDescription instanceof IDiagramElementDescription nodeDescription) { + if (optionalTargetObjectId.isPresent() && diagramElementDescription instanceof IDiagramElementDescription nodeDescription + && !this.isInheritedNodeDescription(editingContext, nodeDescription)) { var semanticObject = this.objectSearchService.getObject(editingContext, optionalTargetObjectId.get()); if (semanticObject.isPresent() && semanticObject.get() instanceof Element element) { if (editingContext instanceof IEMFEditingContext emfEditingContext && this.canHaveNewExpression(emfEditingContext, element)) { @@ -116,6 +150,27 @@ private Optional getTargetObjectId(Object diagramElement) { return result; } + /** + * Indicates whether the description represents inherited content, which must not expose expression mutation + * tools. + * + * @param editingContext + * the current editing context + * @param diagramElementDescription + * the description of the selected diagram element + * @return {@code true} when the description represents an inherited border node or compartment item + */ + private boolean isInheritedNodeDescription(IEditingContext editingContext, IDiagramElementDescription diagramElementDescription) { + if (diagramElementDescription instanceof NodeDescription nodeDescription) { + return this.viewDiagramDescriptionSearchService.findViewNodeDescriptionById(editingContext, nodeDescription.getId()) + .map(org.eclipse.sirius.components.view.diagram.NodeDescription::getName) + .map(descriptionName -> descriptionName.contains(INHERITED_BORDER_NODE_DESCRIPTION_NAME_FRAGMENT) + || descriptionName.contains(INHERITED_COMPARTMENT_ITEM_DESCRIPTION_NAME_FRAGMENT)) + .orElse(false); + } + return false; + } + private boolean canHaveNewExpression(IEMFEditingContext editingContext, Element element) { return !this.readOnlyObjectPredicate.test(element) && this.metamodelQueryElementService.canContainExpressionDefinition(element) && !this.metamodelQueryElementService.hasSingleExpressionDefinition(element); diff --git a/backend/application/syson-application-configuration/src/test/java/org/eclipse/syson/application/expressions/services/ExpressionsPaletteToolsProviderTest.java b/backend/application/syson-application-configuration/src/test/java/org/eclipse/syson/application/expressions/services/ExpressionsPaletteToolsProviderTest.java new file mode 100644 index 0000000000..ee896d03ff --- /dev/null +++ b/backend/application/syson-application-configuration/src/test/java/org/eclipse/syson/application/expressions/services/ExpressionsPaletteToolsProviderTest.java @@ -0,0 +1,142 @@ +/******************************************************************************* + * 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.expressions.services; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Optional; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.eclipse.sirius.components.collaborative.diagrams.DiagramContext; +import org.eclipse.sirius.components.core.api.IObjectSearchService; +import org.eclipse.sirius.components.core.api.IReadOnlyObjectPredicate; +import org.eclipse.sirius.components.diagrams.Node; +import org.eclipse.sirius.components.diagrams.description.NodeDescription; +import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.eclipse.sirius.components.palette.dto.ToolSection; +import org.eclipse.sirius.components.view.diagram.DiagramFactory; +import org.eclipse.sirius.components.view.emf.diagram.api.IViewDiagramDescriptionSearchService; +import org.eclipse.syson.sysml.AttributeUsage; +import org.eclipse.syson.sysml.SysmlFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests the expression tools contributed to diagram palettes. + * + * @author arichard + */ +class ExpressionsPaletteToolsProviderTest { + + private static final String ELEMENT_ID = "attribute-id"; + + private IEMFEditingContext editingContext; + + private IObjectSearchService objectSearchService; + + private Node node; + + private IViewDiagramDescriptionSearchService viewDiagramDescriptionSearchService; + + private ExpressionsPaletteToolsProvider provider; + + /** + * Initializes the provider with an expression-capable attribute target. + */ + @BeforeEach + void setUp() { + this.editingContext = mock(IEMFEditingContext.class); + this.objectSearchService = mock(IObjectSearchService.class); + IReadOnlyObjectPredicate readOnlyObjectPredicate = mock(IReadOnlyObjectPredicate.class); + this.viewDiagramDescriptionSearchService = mock(IViewDiagramDescriptionSearchService.class); + this.node = mock(Node.class); + + AttributeUsage attributeUsage = SysmlFactory.eINSTANCE.createAttributeUsage(); + when(this.node.getTargetObjectId()).thenReturn(ELEMENT_ID); + when(this.objectSearchService.getObject(this.editingContext, ELEMENT_ID)).thenReturn(Optional.of(attributeUsage)); + when(readOnlyObjectPredicate.test(attributeUsage)).thenReturn(false); + + this.provider = new ExpressionsPaletteToolsProvider(this.objectSearchService, readOnlyObjectPredicate, this.viewDiagramDescriptionSearchService); + } + + /** + * Verifies that expression tools remain available on an owned diagram node. + */ + @Test + void expressionToolsAreAvailableOnOwnedNode() { + NodeDescription nodeDescription = this.nodeDescription("description-id"); + + List toolSections = this.provider.createExtraToolSections(this.editingContext, mock(DiagramContext.class), nodeDescription, this.node); + + assertThat(toolSections).singleElement().extracting(ToolSection::tools).asInstanceOf(InstanceOfAssertFactories.LIST) + .extracting("id") + .containsExactly("tool_new_expression"); + } + + /** + * Verifies that inherited border nodes do not expose expression mutation tools. + */ + @Test + void expressionToolsAreUnavailableOnInheritedBorderNode() { + NodeDescription nodeDescription = this.nodeDescription("inherited-border-node-description-id"); + this.mockViewNodeDescription(nodeDescription, "GV InheritedBorderNode AttributeUsage"); + + List toolSections = this.provider.createExtraToolSections(this.editingContext, mock(DiagramContext.class), nodeDescription, this.node); + + assertThat(toolSections).isEmpty(); + } + + /** + * Verifies that inherited compartment items do not expose expression mutation tools. + */ + @Test + void expressionToolsAreUnavailableOnInheritedCompartmentItem() { + NodeDescription nodeDescription = this.nodeDescription("inherited-compartment-item-description-id"); + this.mockViewNodeDescription(nodeDescription, "GV InheritedCompartmentItem AttributeUsage"); + + List toolSections = this.provider.createExtraToolSections(this.editingContext, mock(DiagramContext.class), nodeDescription, this.node); + + assertThat(toolSections).isEmpty(); + } + + /** + * Creates a diagram node description with the given name. + * + * @param name + * the description name + * @return the mocked node description + */ + private NodeDescription nodeDescription(String id) { + NodeDescription nodeDescription = mock(NodeDescription.class); + when(nodeDescription.getId()).thenReturn(id); + return nodeDescription; + } + + /** + * Associates a runtime node description with its View description. + * + * @param nodeDescription + * the runtime node description + * @param name + * the View node description name + */ + private void mockViewNodeDescription(NodeDescription nodeDescription, String name) { + org.eclipse.sirius.components.view.diagram.NodeDescription viewNodeDescription = DiagramFactory.eINSTANCE.createNodeDescription(); + viewNodeDescription.setName(name); + when(this.viewDiagramDescriptionSearchService.findViewNodeDescriptionById(this.editingContext, nodeDescription.getId())) + .thenReturn(Optional.of(viewNodeDescription)); + } +} diff --git a/doc/content/modules/user-manual/pages/release-notes/2026.9.0.adoc b/doc/content/modules/user-manual/pages/release-notes/2026.9.0.adoc index 48b5ba5d62..3e71887834 100644 --- a/doc/content/modules/user-manual/pages/release-notes/2026.9.0.adoc +++ b/doc/content/modules/user-manual/pages/release-notes/2026.9.0.adoc @@ -99,6 +99,7 @@ image::release-notes-port-inherited-item-border-nodes.png[ItemUsage border node The creation of a new `Element` remains available. ** Add the capability of selecting an existing `PortUsage` in _New Port as Receiver_ tool on `AcceptAction` graphical nodes. The creation of a new `PortUsage` remains available. + == Bug fixes * In diagrams: @@ -118,6 +119,7 @@ The affected connections were missing their source and target and were therefore ** Do not propose the _New Subclassification_ edge tool from `EnumerationDefinition` graphical nodes' palette, which cannot subclassify another `EnumerationDefinition`. ** Do not propose the _New Subclassification_ edge tool in the palette when targeting a `Usage` graphical node from a `Definition` graphical node. ** Fix the edge tool creating a `ConnectionUsage` between an `ActionUsage` graphical node and the _done_ `ActionUsage` graphical node. +** Inherited border nodes and inherited compartment items no longer offer the _New Expression_, _Edit Expression_, or _Delete Expression_ actions. * In all views: