Skip to content
Merged
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 @@ -21,6 +21,7 @@ Downstream code should now depend on the diagram services module for that bean.
=== Bug fixes

- https://github.com/eclipse-syson/syson/issues/2359[#2359] [diagrams] Fix `Comment` graphical nodes so changing their background color no longer changes their border color.
- https://github.com/eclipse-syson/syson/issues/2358[#2358] [details] Fix expression creation and editing on `SuccessionAsUsage` elements.

=== Improvements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
import org.eclipse.syson.sysml.Element;
import org.eclipse.syson.sysml.Expression;
import org.eclipse.syson.sysml.FeatureValue;
import org.eclipse.syson.sysml.SuccessionAsUsage;
import org.eclipse.syson.sysml.TransitionUsage;
import org.eclipse.syson.sysml.dto.CreateExpressionInput;
import org.eclipse.syson.sysml.dto.CreateExpressionSuccessPayload;
Expand Down Expand Up @@ -634,6 +635,45 @@ public void canEditGuardExpressionOnTransition() {
.verify(Duration.ofSeconds(10));
}

@DisplayName("GIVEN a SysML succession with a feature value expression, WHEN editing it from the succession THEN the expression is replaced")
@GivenSysONServer({ ExpressionSamplesProjectData.SCRIPT_PATH })
@Test
public void canEditFeatureValueExpressionOnSuccession() {
String editingContextId = ExpressionSamplesProjectData.EDITING_CONTEXT_ID;

List<String> defaultFilters = this.explorerDefaultFiltersSearchService.findTreeDefaultFilterIds(editingContextId, this.sysONExplorerTreeDescriptionId);
var explorerRepresentationId = this.representationIdBuilder.buildExplorerRepresentationId(this.sysONExplorerTreeDescriptionId, List.of(), defaultFilters);
var explorerInput = new ExplorerEventInput(UUID.randomUUID(), editingContextId, explorerRepresentationId);
var flux = this.explorerEventSubscriptionRunner.run(explorerInput).flux();

var treeId = new AtomicReference<String>();
Consumer<Object> initialTreeContentConsumer = assertRefreshedTreeThat(tree -> {
assertThat(tree).isNotNull();
treeId.set(tree.getId());
});

String successionId = ExpressionSamplesProjectData.SemanticIds.THERMAL_CONTROL_TO_HEATING_SUCCESSION_ID;
Runnable createExpression = this.createExpression(editingContextId, successionId, "true");
Consumer<Object> treeRefreshed = assertRefreshedTreeThat(tree -> {
assertThat(tree).isNotNull();
assertThat(tree.getId()).isEqualTo(treeId.get());
});
Runnable checkCreatedExpression = this.checkElementHasExpression(editingContextId, successionId, SuccessionAsUsage.class, new AtomicReference<>(), "true");
Runnable editExpression = this.editExpression(editingContextId, () -> successionId, "false");
Runnable checkEditedExpression = this.checkElementHasExpression(editingContextId, successionId, SuccessionAsUsage.class, new AtomicReference<>(), "false");

StepVerifier.create(flux)
.consumeNextWith(initialTreeContentConsumer)
.then(createExpression)
.consumeNextWith(treeRefreshed)
.then(checkCreatedExpression)
.then(editExpression)
.consumeNextWith(treeRefreshed)
.then(checkEditedExpression)
.thenCancel()
.verify(Duration.ofSeconds(10));
}

@DisplayName("GIVEN a SysML attribute that has an existing expression, WHEN editing the expression to an invalid new value THEN the attribute's expression is not modified")
@GivenSysONServer({ ExpressionSamplesProjectData.SCRIPT_PATH })
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext;
import org.eclipse.syson.sysml.Element;
import org.eclipse.syson.sysml.Expression;
import org.eclipse.syson.sysml.Feature;
import org.eclipse.syson.sysml.Relationship;
import org.eclipse.syson.sysml.dto.EditExpressionInput;
import org.eclipse.syson.sysml.dto.EditExpressionSuccessPayload;
Expand Down Expand Up @@ -89,7 +90,7 @@ public void handle(Sinks.One<IPayload> payloadSink, Sinks.Many<ChangeDescription

if (optionalParent.isPresent() && optionalExpression.isPresent()) {
var parentElement = optionalParent.get();
if (parentElement instanceof Relationship relationship) {
if (parentElement instanceof Relationship relationship && !(parentElement instanceof Feature)) {
parentElement = relationship.getOwningRelatedElement();
}
var result = this.expressionEditor.editExpression(emfEditingContext, parentElement, optionalExpression.get(), editExpressionInput.newExpressionText(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,21 @@
package org.eclipse.syson.sysml.metamodel.services;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;

import org.eclipse.emf.common.util.EList;
import org.eclipse.syson.sysml.ActorMembership;
import org.eclipse.syson.sysml.AllocationUsage;
import org.eclipse.syson.sysml.BooleanExpression;
import org.eclipse.syson.sysml.ConcernUsage;
import org.eclipse.syson.sysml.AllocationUsage;
import org.eclipse.syson.sysml.Connector;
import org.eclipse.syson.sysml.EndFeatureMembership;
import org.eclipse.syson.sysml.ConstraintUsage;
import org.eclipse.syson.sysml.Element;
import org.eclipse.syson.sysml.EndFeatureMembership;
import org.eclipse.syson.sysml.Expression;
import org.eclipse.syson.sysml.Feature;
import org.eclipse.syson.sysml.FeatureChainExpression;
Expand All @@ -37,13 +38,13 @@
import org.eclipse.syson.sysml.Namespace;
import org.eclipse.syson.sysml.OwningMembership;
import org.eclipse.syson.sysml.PartUsage;
import org.eclipse.syson.sysml.ReferenceUsage;
import org.eclipse.syson.sysml.Redefinition;
import org.eclipse.syson.sysml.ReferenceUsage;
import org.eclipse.syson.sysml.Relationship;
import org.eclipse.syson.sysml.RequirementConstraintMembership;
import org.eclipse.syson.sysml.ResultExpressionMembership;
import org.eclipse.syson.sysml.StateUsage;
import org.eclipse.syson.sysml.StakeholderMembership;
import org.eclipse.syson.sysml.StateUsage;
import org.eclipse.syson.sysml.SubjectMembership;
import org.eclipse.syson.sysml.SysmlFactory;
import org.eclipse.syson.sysml.TransitionUsage;
Expand Down Expand Up @@ -140,7 +141,8 @@ public boolean isTopLevelExpression(Element element) {
}

/**
* Check is a given {@code element} has a single/non-ambiguous existing {@link Expression} definition associated.
* Check if a given {@code element} has a single/non-ambiguous existing {@link Expression} definition associated,
* either directly or through an owned {@link FeatureValue}.
*
* @param element
* the element to test
Expand Down Expand Up @@ -182,11 +184,12 @@ public List<OwningMembership> getCompatibleExpressionOwnerships(Element element)
}

/**
* Check is a given {@code element} has a single/non-ambiguous existing {@link Expression} definition associated.
* Finds a single/non-ambiguous existing {@link Expression} definition associated with a given {@code element},
* either directly or through an owned {@link FeatureValue}.
*
* @param element
* the element to test
* @return true if the element has a single existing associated expression definition.
* @return the single associated expression definition, or an empty optional if none or more than one exists.
*/
public Optional<Expression> findSingleExpressionDefinition(Element element) {
Optional<Expression> result = Optional.empty();
Expand All @@ -199,9 +202,22 @@ public Optional<Expression> findSingleExpressionDefinition(Element element) {
} else {
ownedElements = element.getOwnedElement();
}
var ownedExpressions = ownedElements.stream().filter(child -> this.isExpressionDefinition(child)).toList();
if (ownedExpressions.size() == 1) {
result = Optional.of((Expression) ownedExpressions.get(0));
var expressionCandidates = new LinkedHashSet<Expression>();
ownedElements.stream()
.filter(this::isExpressionDefinition)
.map(Expression.class::cast)
.forEach(expressionCandidates::add);
if (element instanceof Feature feature) {
feature.getOwnedRelationship().stream()
.filter(FeatureValue.class::isInstance)
.map(FeatureValue.class::cast)
.map(FeatureValue::getValue)
.filter(Objects::nonNull)
.filter(this::isExpressionDefinition)
.forEach(expressionCandidates::add);
}
if (expressionCandidates.size() == 1) {
result = Optional.of(expressionCandidates.getFirst());
}
}
return result;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*******************************************************************************
* 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.sysml.metamodel.services;

import static org.assertj.core.api.Assertions.assertThat;

import org.eclipse.syson.sysml.FeatureValue;
import org.eclipse.syson.sysml.LiteralBoolean;
import org.eclipse.syson.sysml.PartUsage;
import org.eclipse.syson.sysml.SuccessionAsUsage;
import org.eclipse.syson.sysml.SysmlFactory;
import org.junit.jupiter.api.Test;

/**
* Tests for {@link MetamodelQueryElementService}.
*
* @author arichard
*/
public class MetamodelQueryElementServiceTest {

/**
* Verifies that a feature value expression is found from its owning feature.
*/
@Test
public void findSingleExpressionDefinitionInFeatureValueOfSuccessionAsUsage() {
var service = new MetamodelQueryElementService();
SuccessionAsUsage successionAsUsage = SysmlFactory.eINSTANCE.createSuccessionAsUsage();
FeatureValue featureValue = SysmlFactory.eINSTANCE.createFeatureValue();
LiteralBoolean expression = SysmlFactory.eINSTANCE.createLiteralBoolean();

successionAsUsage.getOwnedRelationship().add(featureValue);
featureValue.getOwnedRelatedElement().add(expression);

assertThat(service.findSingleExpressionDefinition(successionAsUsage)).containsSame(expression);
assertThat(service.hasSingleExpressionDefinition(successionAsUsage)).isTrue();
}

/**
* Verifies that an expression reached both directly and through a feature value is counted only once.
*/
@Test
public void findSingleExpressionDefinitionInFeatureValueOfPartUsage() {
var service = new MetamodelQueryElementService();
PartUsage partUsage = SysmlFactory.eINSTANCE.createPartUsage();
FeatureValue featureValue = SysmlFactory.eINSTANCE.createFeatureValue();
LiteralBoolean expression = SysmlFactory.eINSTANCE.createLiteralBoolean();

partUsage.getOwnedRelationship().add(featureValue);
featureValue.getOwnedRelatedElement().add(expression);

assertThat(service.findSingleExpressionDefinition(partUsage)).containsSame(expression);
assertThat(service.hasSingleExpressionDefinition(partUsage)).isTrue();
}

/**
* Verifies that a directly owned expression is found.
*/
@Test
public void findSingleExpressionDefinitionDirectlyOwnedByFeatureValue() {
var service = new MetamodelQueryElementService();
FeatureValue featureValue = SysmlFactory.eINSTANCE.createFeatureValue();
LiteralBoolean expression = SysmlFactory.eINSTANCE.createLiteralBoolean();

featureValue.getOwnedRelatedElement().add(expression);

assertThat(service.findSingleExpressionDefinition(featureValue)).containsSame(expression);
}

/**
* Verifies that distinct feature value expressions remain ambiguous.
*/
@Test
public void findSingleExpressionDefinitionWithMultipleFeatureValueExpressions() {
var service = new MetamodelQueryElementService();
SuccessionAsUsage successionAsUsage = SysmlFactory.eINSTANCE.createSuccessionAsUsage();
FeatureValue firstFeatureValue = SysmlFactory.eINSTANCE.createFeatureValue();
FeatureValue secondFeatureValue = SysmlFactory.eINSTANCE.createFeatureValue();
LiteralBoolean firstExpression = SysmlFactory.eINSTANCE.createLiteralBoolean();
LiteralBoolean secondExpression = SysmlFactory.eINSTANCE.createLiteralBoolean();

successionAsUsage.getOwnedRelationship().add(firstFeatureValue);
successionAsUsage.getOwnedRelationship().add(secondFeatureValue);
firstFeatureValue.getOwnedRelatedElement().add(firstExpression);
secondFeatureValue.getOwnedRelatedElement().add(secondExpression);

assertThat(service.findSingleExpressionDefinition(successionAsUsage)).isEmpty();
assertThat(service.hasSingleExpressionDefinition(successionAsUsage)).isFalse();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

** Fix `Comment` graphical nodes so changing their background color no longer changes their border color.

* In all views:

** Fix expression creation and editing on `SuccessionAsUsage` elements.

* In textual import/export:


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,26 @@ describe('Details View Tests', () => {
cy.getByTestId('details-expression-value').should('have.text', '<none>');
cy.getByTestId('Create-expression-button').should('exist').should('be.enabled');
});

it('Then creating an expression leaves a single Expression value widget', () => {
cy.getByTestId('Create-expression-button').click();
cy.getByTestId('edit-sysml-expression-modal-textarea').clear().type('true');
cy.getByTestId('edit-expression-submit').click();

cy.getByTestId('edit-sysml-expression-modal').should('not.exist');
cy.getByTestId('details-expression-value').should('have.length', 1).and('have.text', 'true');
cy.getByTestId('Edit-expression-button').should('have.length', 1).and('be.enabled');

explorer.getSelectedTreeItems().find('button').click();
cy.getByTestId('treeitem-contextmenu').findByTestId('edit-sysml-expression-menu').should('exist');
cy.getByTestId('treeitem-contextmenu').findByTestId('new-sysml-expression-menu').should('not.exist');
cy.getByTestId('treeitem-contextmenu').findByTestId('edit-sysml-expression-menu').click();
cy.getByTestId('edit-sysml-expression-modal-textarea').clear().type('false');
cy.getByTestId('edit-expression-submit').click();

cy.getByTestId('edit-sysml-expression-modal').should('not.exist');
cy.getByTestId('details-expression-value').should('have.length', 1).and('have.text', 'false');
});
});
});
});
Loading