From c58cd4cd83a9cf48228811199ca49a1f1d3070d3 Mon Sep 17 00:00:00 2001 From: Pierre-Charles David Date: Wed, 24 Jun 2026 15:05:29 +0200 Subject: [PATCH] [2301] Constraint nodes should display their corresponding expression if it is set Bug: https://github.com/eclipse-syson/syson/issues/2301 Signed-off-by: Pierre-Charles David --- CHANGELOG.adoc | 1 + ...xpressionsControllersIntegrationTests.java | 64 +++++++++++++++++++ .../services/DiagramQueryLabelService.java | 46 ++++++++++--- 3 files changed, 101 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index ff47b7b43..4ecb38183 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -49,6 +49,7 @@ Now, the choice is restricted to the _timeslice/snapshot_ type that match the gr Users should use the dedicated tools to edit expressions instead, as they ensure only valid expressions (with all names resolving) are accepted. - https://github.com/eclipse-syson/syson/issues/2287[#2287] [details] Use a label instead of a text field to display Expressions in the Details view. - https://github.com/eclipse-syson/syson/issues/2290[#2290] [details] When the selected element can have an expression (but does not), display a widget to indicate it and to allow to create one. +- https://github.com/eclipse-syson/syson/issues/2301[#2301] [diagram] Constraint nodes now display their corresponding expression directly in their label if it is set. === New features diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/expressions/ExpressionsControllersIntegrationTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/expressions/ExpressionsControllersIntegrationTests.java index c1441dbd6..e83775f77 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/expressions/ExpressionsControllersIntegrationTests.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/expressions/ExpressionsControllersIntegrationTests.java @@ -13,6 +13,7 @@ package org.eclipse.syson.application.controllers.expressions; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.sirius.components.diagrams.tests.DiagramEventPayloadConsumer.assertRefreshedDiagramThat; import static org.eclipse.sirius.components.trees.tests.TreeEventPayloadConsumer.assertRefreshedTreeThat; import com.jayway.jsonpath.JsonPath; @@ -27,6 +28,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import org.eclipse.sirius.components.collaborative.dto.CreateRepresentationInput; import org.eclipse.sirius.components.core.api.ErrorPayload; import org.eclipse.sirius.components.core.api.IEditingContext; import org.eclipse.sirius.components.core.api.IIdentityService; @@ -34,21 +36,26 @@ import org.eclipse.sirius.components.core.api.IObjectSearchService; import org.eclipse.sirius.components.core.api.IPayload; import org.eclipse.sirius.components.core.api.SuccessPayload; +import org.eclipse.sirius.components.diagrams.Diagram; import org.eclipse.sirius.components.graphql.tests.ExecuteEditingContextFunctionInput; import org.eclipse.sirius.components.graphql.tests.ExecuteEditingContextFunctionSuccessPayload; import org.eclipse.sirius.components.graphql.tests.api.IExecuteEditingContextFunctionRunner; import org.eclipse.sirius.web.application.views.explorer.ExplorerEventInput; +import org.eclipse.sirius.web.tests.services.api.IGivenCreatedDiagramSubscription; import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState; import org.eclipse.sirius.web.tests.services.explorer.ExplorerEventSubscriptionRunner; import org.eclipse.sirius.web.tests.services.representation.RepresentationIdBuilder; import org.eclipse.syson.AbstractIntegrationTests; import org.eclipse.syson.GivenSysONServer; +import org.eclipse.syson.application.controllers.diagrams.checkers.CheckDiagramElementCount; +import org.eclipse.syson.application.controllers.diagrams.testers.DropFromExplorerTester; import org.eclipse.syson.application.controllers.expressions.graphql.CreateExpressionMutationRunner; import org.eclipse.syson.application.controllers.expressions.graphql.DeleteExpressionMutationRunner; import org.eclipse.syson.application.controllers.expressions.graphql.EditExpressionMutationRunner; import org.eclipse.syson.application.controllers.expressions.graphql.ExpressionTextualRepresentationQueryRunner; import org.eclipse.syson.application.data.ExpressionSamplesProjectData; import org.eclipse.syson.application.expressions.dto.DeleteExpressionInput; +import org.eclipse.syson.services.diagrams.DiagramComparator; import org.eclipse.syson.services.explorer.api.IExplorerDefaultFiltersSearchService; import org.eclipse.syson.sysml.AttributeUsage; import org.eclipse.syson.sysml.ConstraintUsage; @@ -62,6 +69,7 @@ import org.eclipse.syson.sysml.dto.EditExpressionSuccessPayload; import org.eclipse.syson.sysml.metamodel.services.MetamodelQueryElementService; import org.eclipse.syson.tree.explorer.view.SysONTreeViewDescriptionProvider; +import org.eclipse.syson.util.SysONRepresentationDescriptionIdentifiers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -70,6 +78,7 @@ import org.springframework.test.context.transaction.TestTransaction; import org.springframework.transaction.annotation.Transactional; +import reactor.core.publisher.Flux; import reactor.test.StepVerifier; /** @@ -84,6 +93,9 @@ public class ExpressionsControllersIntegrationTests extends AbstractIntegrationT @Autowired private IGivenInitialServerState givenInitialServerState; + @Autowired + private DropFromExplorerTester dropFromExplorerTester; + @Autowired private ExplorerEventSubscriptionRunner explorerEventSubscriptionRunner; @@ -117,6 +129,12 @@ public class ExpressionsControllersIntegrationTests extends AbstractIntegrationT @Autowired private DeleteExpressionMutationRunner deleteExpressionMutationRunner; + @Autowired + private IGivenCreatedDiagramSubscription givenCreatedDiagramSubscription; + + @Autowired + private DiagramComparator diagramComparator; + private String sysONExplorerTreeDescriptionId; private MetamodelQueryElementService metamodelQueryElementService; @@ -585,6 +603,52 @@ public void topLevelExpressionTextualRepresentation() { .verify(Duration.ofSeconds(10)); } + private Flux givenSubscriptionToNewDiagram() { + var input = new CreateRepresentationInput( + UUID.randomUUID(), + ExpressionSamplesProjectData.EDITING_CONTEXT_ID, + SysONRepresentationDescriptionIdentifiers.GENERAL_VIEW_DIAGRAM_DESCRIPTION_ID, + ExpressionSamplesProjectData.SemanticIds.EXPRESSIONS_PACKAGE_ID, + "Diagram"); + return this.givenCreatedDiagramSubscription.createAndSubscribe(input).flux(); + } + + + @DisplayName("GIVEN a SysML model with expressions, WHEN dropping a contraint with an associated expression on an empty diagram, THEN the node showing the constraint includes the text of the exression in its label") + @GivenSysONServer({ ExpressionSamplesProjectData.SCRIPT_PATH }) + @Test + public void topLevelConstraintNodeLabelIncludesExpressionInLabel() { + var flux = this.givenSubscriptionToNewDiagram(); + + AtomicReference diagram = new AtomicReference<>(); + Consumer initialDiagramContentConsumer = assertRefreshedDiagramThat(diagram::set); + + Runnable dropFromExplorerRunnable = () -> { + assertThat(diagram.get().getNodes()).hasSize(0); + var background = diagram.get().getStyle().getBackground(); + assertThat(background).isNotEqualTo("white"); + this.dropFromExplorerTester.dropFromExplorerOnDiagram(ExpressionSamplesProjectData.EDITING_CONTEXT_ID, diagram, ExpressionSamplesProjectData.SemanticIds.TANK_PRESSURE_LIMIT_CONSTRAINT_ID); + }; + + Consumer updatedDiagramConsumer = assertRefreshedDiagramThat(newDiagram -> { + new CheckDiagramElementCount(this.diagramComparator) + .hasNewEdgeCount(0) + // 1 node for the ConstraintUsage, 4 for its compartments (doc, attributes, constraints, ports) + .hasNewNodeCount(5) + .check(diagram.get(), newDiagram); + + String nodeLabel = newDiagram.getNodes().get(0).getInsideLabel().getText(); + assertThat(nodeLabel).isEqualTo("«constraint»\npressureLimit\n{ pressure <= maxPressure }"); + }); + + StepVerifier.create(flux) + .consumeNextWith(initialDiagramContentConsumer) + .then(dropFromExplorerRunnable) + .consumeNextWith(updatedDiagramConsumer) + .thenCancel() + .verify(Duration.ofSeconds(10)); + + } /** * Executes a function in the editing context with the specified id (which is assumed to be loaded). The function diff --git a/backend/services/syson-diagram-services/src/main/java/org/eclipse/syson/diagram/services/DiagramQueryLabelService.java b/backend/services/syson-diagram-services/src/main/java/org/eclipse/syson/diagram/services/DiagramQueryLabelService.java index 674fadabc..45f6a9782 100644 --- a/backend/services/syson-diagram-services/src/main/java/org/eclipse/syson/diagram/services/DiagramQueryLabelService.java +++ b/backend/services/syson-diagram-services/src/main/java/org/eclipse/syson/diagram/services/DiagramQueryLabelService.java @@ -48,6 +48,7 @@ import org.eclipse.syson.sysml.Redefinition; import org.eclipse.syson.sysml.ReferenceSubsetting; import org.eclipse.syson.sysml.RequirementConstraintMembership; +import org.eclipse.syson.sysml.ResultExpressionMembership; import org.eclipse.syson.sysml.SatisfyRequirementUsage; import org.eclipse.syson.sysml.StateSubactionMembership; import org.eclipse.syson.sysml.Subclassification; @@ -355,12 +356,15 @@ public String getDefaultInitialDirectEditLabel(Element element) { * @return the label of the value part of the given {@link Usage} if there is one, an empty string otherwise. */ private String getValueStringRepresentation(Usage usage, boolean directEditInput) { + if (directEditInput) { + return ""; + } StringBuilder label = new StringBuilder(); var featureValue = usage.getOwnedRelationship().stream() .filter(FeatureValue.class::isInstance) .map(FeatureValue.class::cast) .findFirst(); - if (featureValue.isPresent() && !directEditInput) { + if (featureValue.isPresent()) { var expression = featureValue.get().getValue(); String valueAsString = null; if (expression != null) { @@ -377,6 +381,12 @@ private String getValueStringRepresentation(Usage usage, boolean directEditInput .append(this.getFeatureValueRelationshipSymbol(featureValue.get())) .append(LabelConstants.SPACE) .append(valueAsString); + } else if (usage instanceof ConstraintUsage constraintUsage) { + String expressionPart = this.getConstraintExpression(constraintUsage); + if (!expressionPart.isEmpty()) { + label.append(LabelConstants.CR); + label.append(expressionPart); + } } return label.toString(); } @@ -574,7 +584,7 @@ public String getDependencyLabel(Dependency dependency) { * Returns the label for the given {@link SatisfyRequirementUsage}. * * @param satisfyRequirementUsage - * The given {@link SatisfyRequirementUsage} + * The given {@link SatisfyRequirementUsage} * @return the label for the given {@link SatisfyRequirementUsage} */ public String getSatisfyLabel(SatisfyRequirementUsage satisfyRequirementUsage) { @@ -607,19 +617,35 @@ private String getCompartmentItemLabel(ConstraintUsage constraintUsage, boolean label.append(this.getIdentificationLabel(constraintUsage)); label.append(this.getReferenceSubsettingLabel(constraintUsage)); } - - if (!directEditInput && !constraintUsage.getOwnedMember().isEmpty() && constraintUsage.getOwnedMember().get(0) instanceof Expression expression) { - if (!label.isEmpty()) { - label.append(LabelConstants.SPACE); - } - label.append(LabelConstants.OPEN_BRACE).append(LabelConstants.SPACE); - label.append(this.getSysmlTextualRepresentation(expression, directEditInput)); - label.append(LabelConstants.SPACE).append(LabelConstants.CLOSE_BRACE); + String expressionPart = this.getConstraintExpression(constraintUsage); + if (!expressionPart.isEmpty()) { + label.append(LabelConstants.SPACE); + label.append(expressionPart); } } return label.toString(); } + /** + * Returns the string representation of the {@link Expression} associated with a {@link ConstraintUsage} using the + * proper syntax (between braces=. + * + * @param constraintUsage + * a {@link ConstraintUsage} + * @param label + * the string representation of the corresponding expression if it is set, or an empty string if there is + * no associated expression. + */ + private String getConstraintExpression(ConstraintUsage constraintUsage) { + StringBuilder label = new StringBuilder(); + if (this.metamodelQueryElementService.getResultExpressionMembership(constraintUsage) instanceof ResultExpressionMembership rem && rem.getOwnedResultExpression() != null) { + label.append(LabelConstants.OPEN_BRACE).append(LabelConstants.SPACE); + label.append(this.metamodelQueryElementService.getExpressionTextualRepresentation(rem.getOwnedResultExpression())); + label.append(LabelConstants.SPACE).append(LabelConstants.CLOSE_BRACE); + } + return label.toString(); + } + /** * Return the label for the given {@link Documentation} when displayed as a compartment item. *