Skip to content

Commit 815d3ae

Browse files
committed
[1368] Add an edge representation for FeatureValue linking two Features
Bug: #1368 Signed-off-by: Arthur Daussy <arthur.daussy@obeo.fr>
1 parent 5d1579d commit 815d3ae

File tree

20 files changed

+709
-47
lines changed

20 files changed

+709
-47
lines changed

CHANGELOG.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ All existing SysON _DiagramDescriptions_ (i.g. _General View_, _Interconnection
100100
- https://github.com/eclipse-syson/syson/issues/1350[#1350] [general-view] Improve _direct edit_ tool on `Feature` to be able to edit `FeatureValue` with basic expressions.
101101
- https://github.com/eclipse-syson/syson/issues/1363[#1363] [general-view] Add a reveal only valued content action on the manage visibility node action that will hide empty graphical compartments and will reveal the others.
102102
- https://github.com/eclipse-syson/syson/issues/1357[#1357] [syson] Add support for `Resource` and `EAnnotation` in `SysONReadOnlyObjectPredicateDelegate`.
103+
- https://github.com/eclipse-syson/syson/issues/1368[#1368] [general-view] Add an edge representation for `FeatureValue` linking two `Features`.
103104

104105
=== New features
105106

backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/configuration/SysMLv2PropertiesConfigurer.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ private FormDescription createDetailsView() {
172172
pageCore.getGroups().add(this.createExtraRequirementConstraintMembershipPropertiesGroup());
173173
pageCore.getGroups().add(this.createExtraAcceptActionUsagePropertiesGroup());
174174
pageCore.getGroups().add(this.createExtraTransitionSourceTargetPropertiesGroup());
175+
pageCore.getGroups().add(this.createFeatureValuePropertiesGroup());
175176

176177
PageDescription pageAdvanced = FormFactory.eINSTANCE.createPageDescription();
177178
pageAdvanced.setName("SysON-DetailsView-Advanced");
@@ -186,6 +187,29 @@ private FormDescription createDetailsView() {
186187
return form;
187188
}
188189

190+
/**
191+
* Creates a group to display the value of a Feature or FeatureValue.
192+
*
193+
* @return a {@link GroupDescription}
194+
*/
195+
private GroupDescription createFeatureValuePropertiesGroup() {
196+
GroupDescription group = FormFactory.eINSTANCE.createGroupDescription();
197+
group.setDisplayMode(GroupDisplayMode.LIST);
198+
group.setName("Value");
199+
group.setLabelExpression("");
200+
group.setSemanticCandidatesExpression(AQLUtils.getSelfServiceCallExpression("getFeatureValue"));
201+
202+
TextAreaDescription expressionWidget = FormFactory.eINSTANCE.createTextAreaDescription();
203+
expressionWidget.setName("ValueExpression");
204+
expressionWidget.setLabelExpression("Value");
205+
expressionWidget.setValueExpression(AQLUtils.getSelfServiceCallExpression("getValueExpression"));
206+
expressionWidget.setIsEnabledExpression("aql:false");
207+
208+
group.getChildren().add(expressionWidget);
209+
210+
return group;
211+
}
212+
189213
private GroupDescription createCorePropertiesGroup() {
190214
GroupDescription group = FormFactory.eINSTANCE.createGroupDescription();
191215
group.setDisplayMode(GroupDisplayMode.LIST);

backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/services/DetailsViewService.java

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969
import org.eclipse.syson.sysml.TransitionUsage;
7070
import org.eclipse.syson.sysml.Type;
7171
import org.eclipse.syson.sysml.ViewUsage;
72+
import org.eclipse.syson.sysml.textual.SysMLElementSerializer;
73+
import org.eclipse.syson.sysml.textual.utils.FileNameDeresolver;
7274

7375
/**
7476
* Java services needed to execute the AQL expressions used in the {@link SysMLv2PropertiesConfigurer}.
@@ -566,11 +568,49 @@ public Element setNewDocumentationValue(Element self, String newValue) {
566568
return self;
567569
}
568570

571+
/**
572+
* Gets the {@link FeatureValue} from a {@link Feature} with a {@link FeatureValue} or a {@link FeatureValue}.
573+
*
574+
* @param self
575+
* a {@link FeatureValue} or {@link Feature}
576+
* @return a {@link FeatureValue} or <code>null</code>
577+
*/
578+
public Element getFeatureValue(Element self) {
579+
Element result = null;
580+
if (self instanceof FeatureValue featureValue && featureValue.getValue() != null) {
581+
result = featureValue;
582+
} else if (self instanceof Feature feature && feature.getValuation() != null && feature.getValuation().getValue() != null) {
583+
result = feature.getValuation();
584+
}
585+
return result;
586+
}
587+
588+
/**
589+
* Gets the textual representation of the value of a {@link FeatureValue}.
590+
*
591+
* @param featureValue
592+
* a {@link FeatureValue}
593+
* @return a textual representation of the value (or empty string if none)
594+
*/
595+
public String getValueExpressionTextualRepresentation(FeatureValue featureValue) {
596+
Expression value = featureValue.getValue();
597+
String result = "";
598+
if (value != null) {
599+
String textualFormat = new SysMLElementSerializer("\n", "\t", new FileNameDeresolver(), s -> {
600+
// Do nothing for now
601+
}).doSwitch(value);
602+
if (textualFormat != null) {
603+
result = textualFormat;
604+
}
605+
}
606+
return result;
607+
}
608+
569609
/**
570610
* Returns the element that owns the visibility feature of the given element.
571611
*
572612
* @param self
573-
* An element for which the visibility owner is being searched.
613+
* An element for which the visibility owner is being searched.
574614
* @return the element that owns the visibility feature of the given element
575615
*/
576616
public Element getVisibilityPropertyOwner(Element self) {
@@ -584,7 +624,7 @@ public Element getVisibilityPropertyOwner(Element self) {
584624
* Returns the enumeration literals for the visibility feature of the given element.
585625
*
586626
* @param self
587-
* An element for which the list of visibility literals are being searched.
627+
* An element for which the list of visibility literals are being searched.
588628
* @return the enumeration literals for the visibility feature of the given element.
589629
*/
590630
public List<EEnumLiteral> getVisibilityEnumLiterals(Element self) {
@@ -599,7 +639,7 @@ public List<EEnumLiteral> getVisibilityEnumLiterals(Element self) {
599639
* Returns the visibility value of the given element.
600640
*
601641
* @param self
602-
* An element for which the list of visibility literals are being searched.
642+
* An element for which the list of visibility literals are being searched.
603643
* @return the current value of the visibility feature of the given element.
604644
*/
605645
public EEnumLiteral getVisibilityValue(Element self) {
@@ -614,11 +654,11 @@ public EEnumLiteral getVisibilityValue(Element self) {
614654
* Sets the visibility value of the given element.
615655
*
616656
* @param self
617-
* An element for which the list of visibility literals are being searched.
657+
* An element for which the list of visibility literals are being searched.
618658
* @param newValue
619-
* the value to set.
659+
* the value to set.
620660
* @return <code>true</code> if the visibility feature of the given element has been properly set and
621-
* <code>false</code> otherwise.
661+
* <code>false</code> otherwise.
622662
*/
623663
public boolean setVisibilityValue(Element self, Object newValue) {
624664
boolean result = false;
@@ -655,7 +695,7 @@ private void handleImplied(Element element, EStructuralFeature eStructuralFeatur
655695
* guarantee that it is well formed after its call.
656696
*
657697
* @param aau
658-
* an {@link AcceptActionUsage}
698+
* an {@link AcceptActionUsage}
659699
*/
660700
private void checkAndRepairAcceptActionUsageStructure(AcceptActionUsage aau) {
661701
this.checkAndRepairAcceptActionUsagePayload(aau);

backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVItemAndAttributeExpressionTests.java

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,32 @@
1212
*******************************************************************************/
1313
package org.eclipse.syson.application.controllers.diagrams.general.view;
1414

15+
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.junit.jupiter.api.Assertions.fail;
17+
1518
import java.time.Duration;
19+
import java.util.List;
20+
import java.util.Optional;
1621
import java.util.UUID;
1722
import java.util.concurrent.atomic.AtomicReference;
23+
import java.util.function.Consumer;
1824

1925
import org.eclipse.sirius.components.collaborative.diagrams.dto.DiagramEventInput;
2026
import org.eclipse.sirius.components.collaborative.diagrams.dto.DiagramRefreshedEventPayload;
2127
import org.eclipse.sirius.components.diagrams.Diagram;
28+
import org.eclipse.sirius.components.diagrams.Edge;
29+
import org.eclipse.sirius.components.diagrams.InsideLabel;
2230
import org.eclipse.sirius.components.diagrams.tests.graphql.EditLabelMutationRunner;
2331
import org.eclipse.sirius.components.diagrams.tests.graphql.InitialDirectEditElementLabelQueryRunner;
32+
import org.eclipse.sirius.components.diagrams.tests.navigation.DiagramNavigator;
2433
import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState;
2534
import org.eclipse.syson.AbstractIntegrationTests;
35+
import org.eclipse.syson.application.controllers.diagrams.testers.DeleteFromDiagramRunner;
36+
import org.eclipse.syson.application.controllers.diagrams.testers.DeleteFromDiagramTester;
2637
import org.eclipse.syson.application.controllers.diagrams.testers.DirectEditInitialLabelTester;
2738
import org.eclipse.syson.application.controllers.diagrams.testers.DirectEditTester;
2839
import org.eclipse.syson.application.data.GeneralViewItemAndAttributeProjectData;
40+
import org.eclipse.syson.services.diagrams.DiagramComparator;
2941
import org.eclipse.syson.services.diagrams.api.IGivenDiagramReference;
3042
import org.eclipse.syson.services.diagrams.api.IGivenDiagramSubscription;
3143
import org.junit.jupiter.api.AfterEach;
@@ -65,6 +77,12 @@ public class GVItemAndAttributeExpressionTests extends AbstractIntegrationTests
6577
@Autowired
6678
private EditLabelMutationRunner editLabelMutationRunner;
6779

80+
@Autowired
81+
private DiagramComparator diagramComparator;
82+
83+
@Autowired
84+
private DeleteFromDiagramRunner deleteFromDiagramRunner;
85+
6886
private Step<DiagramRefreshedEventPayload> verifier;
6987

7088
private AtomicReference<Diagram> diagram;
@@ -73,6 +91,8 @@ public class GVItemAndAttributeExpressionTests extends AbstractIntegrationTests
7391

7492
private DirectEditTester directEditTester;
7593

94+
private DeleteFromDiagramTester deleteFromDiagramTester;
95+
7696
@BeforeEach
7797
public void setUp() {
7898
this.givenInitialServerState.initialize();
@@ -84,6 +104,8 @@ public void setUp() {
84104
this.diagram = this.givenDiagram.getDiagram(this.verifier);
85105
this.directEditInitialLabelTester = new DirectEditInitialLabelTester(this.initialDirectEditElementLabelQueryRunner, GeneralViewItemAndAttributeProjectData.EDITING_CONTEXT_ID);
86106
this.directEditTester = new DirectEditTester(this.editLabelMutationRunner, GeneralViewItemAndAttributeProjectData.EDITING_CONTEXT_ID);
107+
this.deleteFromDiagramTester = new DeleteFromDiagramTester(this.deleteFromDiagramRunner, GeneralViewItemAndAttributeProjectData.EDITING_CONTEXT_ID,
108+
GeneralViewItemAndAttributeProjectData.GraphicalIds.DIAGRAM_ID);
87109
}
88110

89111
@AfterEach
@@ -215,4 +237,92 @@ public void attributeWithBranketExpression() {
215237
GeneralViewItemAndAttributeProjectData.GraphicalIds.P1_1_X1_ID,
216238
"a2_1 = 45 [kilogram]");
217239
}
240+
241+
@DisplayName("GIVEN an ItemUsage, WHEN with a value referencing another ItemUsage, THEN an edge should connect the ItemUsage")
242+
@Test
243+
@Sql(scripts = { GeneralViewItemAndAttributeProjectData.SCRIPT_PATH }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
244+
@Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED))
245+
public void itemFeatureChainBindingEdge() {
246+
247+
// Create an edge using direct edit
248+
this.directEditTester.checkDirectEditInsideLabel(this.verifier, this.diagram,
249+
GeneralViewItemAndAttributeProjectData.GraphicalIds.A2_1_ICON_AND_LABEL_ID,
250+
"in a2_1 = a1.a1_1",
251+
this.buildEdgeChecker(GeneralViewItemAndAttributeProjectData.GraphicalIds.A2_1_ICON_AND_LABEL_ID, "in a2_1 = a1.a1_1",
252+
GeneralViewItemAndAttributeProjectData.GraphicalIds.A2_1_BORDERED_NODE_ID, GeneralViewItemAndAttributeProjectData.GraphicalIds.A1_1_BORDERED_NODE_ID));
253+
254+
// Change the edge to a new target
255+
this.directEditTester.checkDirectEditInsideLabel(this.verifier, this.diagram,
256+
GeneralViewItemAndAttributeProjectData.GraphicalIds.A2_1_ICON_AND_LABEL_ID,
257+
"in a2_1 = a1.a1_2",
258+
this.buildEdgeChecker(GeneralViewItemAndAttributeProjectData.GraphicalIds.A2_1_ICON_AND_LABEL_ID, "in a2_1 = a1.a1_2",
259+
GeneralViewItemAndAttributeProjectData.GraphicalIds.A2_1_BORDERED_NODE_ID, GeneralViewItemAndAttributeProjectData.GraphicalIds.A1_2_BORDERED_NODE_ID));
260+
261+
// Remove edge
262+
this.directEditTester.checkDirectEditInsideLabel(this.verifier, this.diagram,
263+
GeneralViewItemAndAttributeProjectData.GraphicalIds.A2_1_ICON_AND_LABEL_ID,
264+
"in a2_1 =",
265+
this.buildNoEdgeStartingFromChecker(GeneralViewItemAndAttributeProjectData.GraphicalIds.A2_1_ICON_AND_LABEL_ID, "in a2_1",
266+
GeneralViewItemAndAttributeProjectData.GraphicalIds.A2_1_BORDERED_NODE_ID));
267+
268+
}
269+
270+
@DisplayName("GIVEN an ItemUsage, WHEN deleting an edge representing a FeatureValue, THEN the FeatureValue should be deleted and the label of the ItemUsage should be updated updated")
271+
@Test
272+
@Sql(scripts = { GeneralViewItemAndAttributeProjectData.SCRIPT_PATH }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
273+
@Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED))
274+
public void deleteFeatureValueEdge() {
275+
276+
this.deleteFromDiagramTester.checkRemoveFromDiagram(this.verifier, List.of(), List.of(GeneralViewItemAndAttributeProjectData.GraphicalIds.FEATURE_VALUE_A2_2_TO_A1_4_EDGE),
277+
payload -> Optional.of(payload)
278+
.map(DiagramRefreshedEventPayload::diagram)
279+
.ifPresentOrElse(newDiagram -> {
280+
// Check label no more FeatureValue (the = part is gone)
281+
DiagramNavigator diagramNavigator = new DiagramNavigator(newDiagram);
282+
InsideLabel newLabel = diagramNavigator.nodeWithId(GeneralViewItemAndAttributeProjectData.GraphicalIds.A2_2_ICON_AND_LABEL_ID).getNode().getInsideLabel();
283+
assertThat(newLabel.getText()).isEqualTo("out a2_2");
284+
285+
// No more edge starting from a2_2
286+
assertThat(newDiagram.getEdges()).noneMatch(s -> GeneralViewItemAndAttributeProjectData.GraphicalIds.A2_2_BORDERED_NODE_ID.equals(s.getSourceId()));
287+
}, () -> fail("Missing diagram")));
288+
289+
}
290+
291+
private Consumer<DiagramRefreshedEventPayload> buildEdgeChecker(String nodeToCheckForLabel, String expectedLabel, String sourceNodeId, String targetNodeId) {
292+
return payload -> Optional.of(payload)
293+
.map(DiagramRefreshedEventPayload::diagram)
294+
.ifPresentOrElse(newDiagram -> {
295+
// Check label
296+
DiagramNavigator diagramNavigator = new DiagramNavigator(newDiagram);
297+
InsideLabel newLabel = diagramNavigator.nodeWithId(nodeToCheckForLabel).getNode().getInsideLabel();
298+
assertThat(newLabel.getText()).isEqualTo(expectedLabel);
299+
300+
// Check new edge
301+
List<Edge> newEdges = this.diagramComparator.newEdges(this.diagram.get(), newDiagram);
302+
assertThat(newEdges).hasSize(1)
303+
.first()
304+
.satisfies(edge -> {
305+
assertThat(edge.getSourceId()).as("Should start from A2_1").isEqualTo(sourceNodeId);
306+
}, edge -> {
307+
assertThat(edge.getTargetId()).as("Should end to A1_1").isEqualTo(targetNodeId);
308+
});
309+
}, () -> fail("Missing diagram"));
310+
}
311+
312+
private Consumer<DiagramRefreshedEventPayload> buildNoEdgeStartingFromChecker(String nodeToCheckForLabel, String expectedLabel, String sourceNodeId) {
313+
return payload -> Optional.of(payload)
314+
.map(DiagramRefreshedEventPayload::diagram)
315+
.ifPresentOrElse(newDiagram -> {
316+
// Check label
317+
DiagramNavigator diagramNavigator = new DiagramNavigator(newDiagram);
318+
InsideLabel newLabel = diagramNavigator.nodeWithId(nodeToCheckForLabel).getNode().getInsideLabel();
319+
assertThat(newLabel.getText()).isEqualTo(expectedLabel);
320+
321+
// Check there is starting from the given source
322+
List<Edge> newEdges = this.diagramComparator.newEdges(this.diagram.get(), newDiagram);
323+
324+
assertThat(newDiagram.getEdges()).noneMatch(s -> sourceNodeId.equals(s.getSourceId()));
325+
assertThat(newEdges).hasSize(0);
326+
}, () -> fail("Missing diagram"));
327+
}
218328
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Obeo.
3+
* This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Obeo - initial API and implementation
12+
*******************************************************************************/
13+
package org.eclipse.syson.application.controllers.diagrams.testers;
14+
15+
import java.util.Objects;
16+
17+
import org.eclipse.sirius.components.collaborative.diagrams.dto.DeleteFromDiagramInput;
18+
import org.eclipse.sirius.components.graphql.tests.api.IGraphQLRequestor;
19+
import org.eclipse.sirius.components.graphql.tests.api.IMutationRunner;
20+
import org.springframework.stereotype.Service;
21+
22+
/**
23+
* Used to invoke a remove from diagram tool with the GraphQL API.
24+
*
25+
* <p>
26+
* This class should be provided by Sirius Web, remove this once
27+
* https://github.com/eclipse-sirius/sirius-web/issues/5041 is closed
28+
* </p>
29+
*
30+
* @author Arthur Daussy
31+
*/
32+
@Service
33+
public class DeleteFromDiagramRunner implements IMutationRunner<DeleteFromDiagramInput> {
34+
35+
private static final String REMOVE_FROM_DIAGRAM_MUTATION = """
36+
mutation deleteFromDiagram($input: DeleteFromDiagramInput!) {
37+
deleteFromDiagram(input: $input) {
38+
__typename
39+
... on ErrorPayload {
40+
messages {
41+
body
42+
level
43+
__typename
44+
}
45+
__typename
46+
}
47+
... on DeleteFromDiagramSuccessPayload {
48+
messages {
49+
body
50+
level
51+
__typename
52+
}
53+
__typename
54+
}
55+
}
56+
}
57+
""";
58+
59+
private final IGraphQLRequestor graphQLRequestor;
60+
61+
public DeleteFromDiagramRunner(IGraphQLRequestor graphQLRequestor) {
62+
this.graphQLRequestor = Objects.requireNonNull(graphQLRequestor);
63+
}
64+
65+
@Override
66+
public String run(DeleteFromDiagramInput input) {
67+
return this.graphQLRequestor.execute(REMOVE_FROM_DIAGRAM_MUTATION, input);
68+
}
69+
70+
}

0 commit comments

Comments
 (0)