diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 35c209d95..86e85fc19 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -58,6 +58,7 @@ Users should use the dedicated tools to edit expressions instead, as they ensure It concerns, at least, _actions_ and _items_ compartments list items. - https://github.com/eclipse-syson/syson/issues/2284[#2284] [diagrams] The expression-related tools in diagram palette are now available in the default _Edit_ section. - https://github.com/eclipse-syson/syson/issues/2319[#2319] [diagrams] Remove the margins around the `Actor` image. +- https://github.com/eclipse-syson/syson/issues/2325[#2325] [syson] Add checkboxes in the new Expressions popup allowing to set the _isDefault_ and _isInitial_ properties of `FeatureValue` elements when the expression is `FeatureValue` related. === New features @@ -70,10 +71,10 @@ Disabling the _Hide expression internals_ filter in the _Explorer_ view allows t - https://github.com/eclipse-syson/syson/issues/2235[#2235] [diagrams] Leverage the selection dialog to improve the graphical node tool creating a _frame_ `ConcernUsage` from `RequirementUsage` and `RequirementDefinition` graphical nodes. - https://github.com/eclipse-syson/syson/issues/2242[#2242] [explorer] Add two options to the dialog creating a child element of `RequirementUsage` or `RequirementDefinition` tree items. One creates a `ConcernUsage` and another one creates `FramedConcernMembership`. -- https://github.com/eclipse-syson/syson/issues/2097[#2097] [explorer] Add support for creating and editing exressions through their textual representation. +- https://github.com/eclipse-syson/syson/issues/2097[#2097] [explorer] Add support for creating and editing expressions through their textual representation. This is currently supported on `Features` (e.g. `Attribute`), `Constraints` and `Transitions` (guard conditions) view new context menu actions (_Create expression_ and _Edit expression_) on the corresponding elements in the _Explorer_. - https://github.com/eclipse-syson/syson/issues/2270[#2270] [diagram] Add dedicated tools to diagram elements palette to create/edit/delete expressions. -- https://github.com/eclipse-syson/syson/issues/2247[#2247] [diagrams] Add the support for creating _timeslices/snapshots_ from the different kind of `OccurrenceDefiniton` graphical nodes. +- https://github.com/eclipse-syson/syson/issues/2247[#2247] [diagrams] Add the support for creating _timeslices/snapshots_ from the different kind of `OccurrenceDefinition` graphical nodes. It leverages the selection dialog to either create an _occurrence timeslice/snapshot_, or the _usage timeslice/snapshot_ matching the `OccurrenceDefinition` on which the tool is applied. - https://github.com/eclipse-syson/syson/issues/2250[#2250] [diagrams] Leverage the selection dialog to improve the graphical node tools creating a _require_ `ConstraintUsage`, or an _assume_ `ConstraintUsage`, from `RequirementUsage` and `RequirementDefinition` graphical nodes. - https://github.com/eclipse-syson/syson/issues/2254[#2254] [diagrams] Add the support for _assume_ and _require_ graphical edges. diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/dto/ExpressionTextualRepresentationPayload.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/dto/ExpressionTextualRepresentationPayload.java index 3afd3f2b7..ebe5e334d 100644 --- a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/dto/ExpressionTextualRepresentationPayload.java +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/dto/ExpressionTextualRepresentationPayload.java @@ -20,9 +20,16 @@ /** * Payload for the {@code expressionTextualRepresentation} query field on EditingContext. * + * @param id + * the query identifier. + * @param textualRepresentation + * the textual representation of the effective expression, or an empty string if none is available. + * @param featureValueProperties + * the current feature value properties when supported. * @author pcdavid */ -public record ExpressionTextualRepresentationPayload(UUID id, String textualRepresentation) implements IPayload { +public record ExpressionTextualRepresentationPayload(UUID id, String textualRepresentation, + FeatureValueExpressionPropertiesPayload featureValueProperties) implements IPayload { public ExpressionTextualRepresentationPayload { Objects.requireNonNull(id); } diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/dto/FeatureValueExpressionPropertiesPayload.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/dto/FeatureValueExpressionPropertiesPayload.java new file mode 100644 index 000000000..35a2e3e17 --- /dev/null +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/dto/FeatureValueExpressionPropertiesPayload.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * 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.dto; + +/** + * Properties exposed by the expression editor for expressions owned by a SysML {@code FeatureValue}. + * + * @param isDefault + * whether the feature value is marked as default. + * @param isInitial + * whether the feature value is marked as initial. + * @author arichard + */ +public record FeatureValueExpressionPropertiesPayload(boolean isDefault, boolean isInitial) { +} diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/graphql/EditingContextExpressionTextualRepresentationDataFetcher.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/graphql/EditingContextExpressionTextualRepresentationDataFetcher.java index e1a53069a..5bd5f4fc7 100644 --- a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/graphql/EditingContextExpressionTextualRepresentationDataFetcher.java +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/graphql/EditingContextExpressionTextualRepresentationDataFetcher.java @@ -31,7 +31,7 @@ * @author pcdavid */ @QueryDataFetcher(type = "EditingContext", field = "expressionTextualRepresentation") -public class EditingContextExpressionTextualRepresentationDataFetcher implements IDataFetcherWithFieldCoordinates> { +public class EditingContextExpressionTextualRepresentationDataFetcher implements IDataFetcherWithFieldCoordinates> { private static final String ELEMENT_ID_ARGUMENT = "elementId"; @@ -42,7 +42,7 @@ public EditingContextExpressionTextualRepresentationDataFetcher(IEditingContextD } @Override - public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { String editingContextId = environment.getSource(); String elementId = environment.getArgument(ELEMENT_ID_ARGUMENT); @@ -50,7 +50,6 @@ public CompletableFuture get(DataFetchingEnvironment environment) throws return this.editingContextDispatcher.dispatchQuery(input.editingContextId(), input) .filter(ExpressionTextualRepresentationPayload.class::isInstance) .map(ExpressionTextualRepresentationPayload.class::cast) - .map(ExpressionTextualRepresentationPayload::textualRepresentation) .toFuture(); } } diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/services/ExpressionTextualRepresentationEventHandler.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/services/ExpressionTextualRepresentationEventHandler.java index ec8fe45fe..617962129 100644 --- a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/services/ExpressionTextualRepresentationEventHandler.java +++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/expressions/services/ExpressionTextualRepresentationEventHandler.java @@ -23,8 +23,14 @@ import org.eclipse.sirius.components.core.api.IPayload; import org.eclipse.syson.application.expressions.dto.ExpressionTextualRepresentationInput; import org.eclipse.syson.application.expressions.dto.ExpressionTextualRepresentationPayload; +import org.eclipse.syson.application.expressions.dto.FeatureValueExpressionPropertiesPayload; import org.eclipse.syson.sysml.Element; import org.eclipse.syson.sysml.Expression; +import org.eclipse.syson.sysml.Feature; +import org.eclipse.syson.sysml.FeatureValue; +import org.eclipse.syson.sysml.Function; +import org.eclipse.syson.sysml.Succession; +import org.eclipse.syson.sysml.TransitionUsage; import org.eclipse.syson.sysml.metamodel.services.MetamodelQueryElementService; import org.springframework.stereotype.Service; @@ -55,39 +61,133 @@ public boolean canHandle(IEditingContext editingContext, IInput input) { @Override public void handle(Sinks.One payloadSink, Sinks.Many changeDescriptionSink, IEditingContext editingContext, IInput input) { String textualRepresentation = ""; + FeatureValueExpressionPropertiesPayload featureValueProperties = null; if (input instanceof ExpressionTextualRepresentationInput expressionTextualRepresentationInput) { String elementId = expressionTextualRepresentationInput.elementId(); - Optional optionalExpression = this.getExpression(editingContext, elementId); + Optional optionalElement = this.getElement(editingContext, elementId); + Optional optionalExpression = optionalElement.flatMap(this::getExpression); if (optionalExpression.isPresent()) { textualRepresentation = this.metamodelQueryElementService.getExpressionTextualRepresentation(optionalExpression.get()); } + featureValueProperties = this.getFeatureValueProperties(optionalElement, optionalExpression); } - payloadSink.tryEmitValue(new ExpressionTextualRepresentationPayload(input.id(), textualRepresentation)); + payloadSink.tryEmitValue(new ExpressionTextualRepresentationPayload(input.id(), textualRepresentation, featureValueProperties)); } /** - * Finds the {@link Expression} element to consider given the provided {@code elementId}. + * Gets the target {@link Element} designated by the provided identifier. * * @param editingContext * the editing context. * @param elementId - * either to id of an actual {@link Expression} element, or of the parent {@link Element} of a single + * the identifier of the target element. + * @return the target {@link Element}, if it exists. + */ + private Optional getElement(IEditingContext editingContext, String elementId) { + return this.objectSearchService.getObject(editingContext, elementId) + .filter(Element.class::isInstance) + .map(Element.class::cast); + } + + /** + * Finds the {@link Expression} element to consider given the provided {@code elementId}. + * + * @param element + * either an actual {@link Expression} element, or the parent {@link Element} of a single * {@code Expression}. * @return the directly of indirectly designated {@link Expression}. */ - private Optional getExpression(IEditingContext editingContext, String elementId) { + private Optional getExpression(Element element) { Optional result = Optional.empty(); - Optional optionalElement = this.objectSearchService.getObject(editingContext, elementId) - .filter(Element.class::isInstance) - .map(Element.class::cast); + if (element instanceof Expression expression && this.metamodelQueryElementService.isTopLevelExpression(expression)) { + result = Optional.of(expression); + } else { + result = this.metamodelQueryElementService.findSingleExpressionDefinition(element); + } + return result; + } + + /** + * Gets the {@link FeatureValueExpressionPropertiesPayload} relevant for the expression editor context. + * + * @param optionalElement + * the selected element, if any. + * @param optionalExpression + * the effective expression, if any. + * @return the relevant {@link FeatureValueExpressionPropertiesPayload}, if any. + */ + private FeatureValueExpressionPropertiesPayload getFeatureValueProperties(Optional optionalElement, Optional optionalExpression) { + FeatureValueExpressionPropertiesPayload result = null; if (optionalElement.isPresent()) { Element element = optionalElement.get(); - if (element instanceof Expression expression && this.metamodelQueryElementService.isTopLevelExpression(expression)) { - result = optionalElement.map(Expression.class::cast); - } else { - result = this.metamodelQueryElementService.findSingleExpressionDefinition(element); + Optional optionalFeatureValue = Optional.empty(); + if (element instanceof Expression expression) { + optionalFeatureValue = this.getOwningFeatureValue(expression); + } else if (element instanceof FeatureValue featureValue) { + optionalFeatureValue = Optional.of(featureValue); + } else if (this.supportsFeatureValueProperties(element)) { + optionalFeatureValue = this.getOwnedFeatureValue(element, optionalExpression); + } + if (optionalFeatureValue.isPresent()) { + FeatureValue featureValue = optionalFeatureValue.get(); + result = new FeatureValueExpressionPropertiesPayload(featureValue.isIsDefault(), featureValue.isIsInitial()); + } else if (optionalElement.filter(this::supportsFeatureValueProperties).isPresent()) { + result = new FeatureValueExpressionPropertiesPayload(false, false); } } return result; } + + /** + * Gets the {@link FeatureValue} directly owned by the selected feature-like element or by the resolved expression. + * + * @param element + * the selected element. + * @param optionalExpression + * the effective expression, if any. + * @return the relevant {@link FeatureValue}, if any. + */ + private Optional getOwnedFeatureValue(Element element, Optional optionalExpression) { + Optional result = optionalExpression.flatMap(this::getOwningFeatureValue); + if (result.isEmpty() && element instanceof Feature feature) { + result = feature.getOwnedRelationship().stream() + .filter(FeatureValue.class::isInstance) + .map(FeatureValue.class::cast) + .findFirst(); + } + return result; + } + + /** + * Gets the owning {@link FeatureValue} of the provided expression, if it is one. + * + * @param expression + * the expression whose owner should be inspected. + * @return the owning {@link FeatureValue}, if any. + */ + private Optional getOwningFeatureValue(Expression expression) { + return Optional.ofNullable(expression.getOwningRelationship()) + .filter(FeatureValue.class::isInstance) + .map(FeatureValue.class::cast); + } + + /** + * Checks whether the selected element would own its expression through a {@link FeatureValue}, which is the only + * case where the expression editor should expose feature value properties in create mode. + * + * @param element + * the selected element. + * @return {@code true} if feature value properties should be exposed for a new expression on this element. + */ + private boolean supportsFeatureValueProperties(Element element) { + boolean result = false; + if (element instanceof TransitionUsage || element instanceof Succession) { + result = false; + } else if (element instanceof Function || element instanceof Expression) { + result = false; + } else { + result = element instanceof Feature; + } + return result; + } } diff --git a/backend/application/syson-application-configuration/src/main/resources/schema/expressions.graphqls b/backend/application/syson-application-configuration/src/main/resources/schema/expressions.graphqls index e02595d6d..7398a28c6 100644 --- a/backend/application/syson-application-configuration/src/main/resources/schema/expressions.graphqls +++ b/backend/application/syson-application-configuration/src/main/resources/schema/expressions.graphqls @@ -1,5 +1,15 @@ extend type EditingContext { - expressionTextualRepresentation(elementId: ID!): String + expressionTextualRepresentation(elementId: ID!): ExpressionEditorState +} + +type ExpressionEditorState { + textualRepresentation: String! + featureValueProperties: FeatureValueExpressionProperties +} + +type FeatureValueExpressionProperties { + isDefault: Boolean! + isInitial: Boolean! } extend type Mutation { 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 a46a12f49..f511fb788 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 @@ -73,6 +73,8 @@ import org.eclipse.syson.sysml.dto.CreateExpressionSuccessPayload; import org.eclipse.syson.sysml.dto.EditExpressionInput; import org.eclipse.syson.sysml.dto.EditExpressionSuccessPayload; +import org.eclipse.syson.sysml.dto.ExpressionPropertiesInput; +import org.eclipse.syson.sysml.dto.FeatureValueExpressionPropertiesInput; import org.eclipse.syson.sysml.metamodel.helper.LabelConstants; import org.eclipse.syson.sysml.metamodel.services.MetamodelQueryElementService; import org.eclipse.syson.tree.explorer.view.SysONTreeViewDescriptionProvider; @@ -203,6 +205,43 @@ public void canCreateExpressionInEmptyAttribute() { .verify(Duration.ofSeconds(10)); } + @DisplayName("GIVEN an empty SysML attribute, WHEN creating an expression with feature value properties THEN the created feature value stores them") + @GivenSysONServer({ ExpressionSamplesProjectData.SCRIPT_PATH }) + @Test + public void canCreateExpressionInEmptyAttributeWithFeatureValueProperties() { + String editingContextId = ExpressionSamplesProjectData.EDITING_CONTEXT_ID; + + List 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(); + Consumer initialTreeContentConsumer = assertRefreshedTreeThat(tree -> { + assertThat(tree).isNotNull(); + treeId.set(tree.getId()); + }); + + Runnable createExpressionOnPressureAttribute = this.createExpression(editingContextId, ExpressionSamplesProjectData.SemanticIds.TANK_PRESSURE_ATTRIBUTE_ID, "maxPressure / 2", + this.featureValueProperties(true, false)); + + Consumer treeRefreshed = assertRefreshedTreeThat(tree -> { + assertThat(tree).isNotNull(); + assertThat(tree.getId()).isEqualTo(treeId.get()); + }); + + Runnable checkPressureAttributeFeatureValueProperties = this.checkAttributeFeatureValue(editingContextId, ExpressionSamplesProjectData.SemanticIds.TANK_PRESSURE_ATTRIBUTE_ID, + "maxPressure / 2", true, false); + + StepVerifier.create(flux) + .consumeNextWith(initialTreeContentConsumer) + .then(createExpressionOnPressureAttribute) + .consumeNextWith(treeRefreshed) + .then(checkPressureAttributeFeatureValueProperties) + .thenCancel() + .verify(Duration.ofSeconds(10)); + } + @DisplayName("GIVEN a SysML attribute which does have an existing expression, WHEN trying to create a new expression on it THEN the new expression is created with proper name resolution") @GivenSysONServer({ ExpressionSamplesProjectData.SCRIPT_PATH }) @Test @@ -285,6 +324,110 @@ public void textEditAttributeExpression() { .verify(Duration.ofSeconds(10)); } + @DisplayName("GIVEN a SysML attribute with a value expression, WHEN editing it with feature value properties THEN the flags are updated together with the expression") + @GivenSysONServer({ ExpressionSamplesProjectData.SCRIPT_PATH }) + @Test + public void textEditAttributeExpressionAndFeatureValueProperties() { + String editingContextId = ExpressionSamplesProjectData.EDITING_CONTEXT_ID; + + List 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(); + Consumer initialTreeContentConsumer = assertRefreshedTreeThat(tree -> { + assertThat(tree).isNotNull(); + treeId.set(tree.getId()); + }); + + var existingExpressionId = new AtomicReference(); + + Runnable checkInitialExpression = this.checkElementHasExpression(editingContextId, + ExpressionSamplesProjectData.SemanticIds.TANK_MAX_VOLUME_ATTRIBUTE_ID, AttributeUsage.class, existingExpressionId, + "100.0 * minVolume"); + + Runnable editExpressionOnMaxVolumeAttribute = this.editExpression(editingContextId, existingExpressionId::get, "50 * minVolume", + this.featureValueProperties(true, true)); + + Consumer treeRefreshed = assertRefreshedTreeThat(tree -> { + assertThat(tree).isNotNull(); + assertThat(tree.getId()).isEqualTo(treeId.get()); + }); + + Runnable checkUpdatedFeatureValue = this.checkAttributeFeatureValue(editingContextId, ExpressionSamplesProjectData.SemanticIds.TANK_MAX_VOLUME_ATTRIBUTE_ID, + "50 * minVolume", true, true); + + StepVerifier.create(flux) + .consumeNextWith(initialTreeContentConsumer) + .then(checkInitialExpression) + .then(editExpressionOnMaxVolumeAttribute) + .consumeNextWith(treeRefreshed) + .then(checkUpdatedFeatureValue) + .thenCancel() + .verify(Duration.ofSeconds(10)); + } + + @DisplayName("GIVEN a SysML attribute with feature value flags, WHEN editing it with partial properties THEN omitted fields keep their previous value") + @GivenSysONServer({ ExpressionSamplesProjectData.SCRIPT_PATH }) + @Test + public void textEditAttributeExpressionWithPartialFeatureValueProperties() { + String editingContextId = ExpressionSamplesProjectData.EDITING_CONTEXT_ID; + + List 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(); + Consumer initialTreeContentConsumer = assertRefreshedTreeThat(tree -> { + assertThat(tree).isNotNull(); + treeId.set(tree.getId()); + }); + + var existingExpressionId = new AtomicReference(); + + Runnable checkInitialExpression = this.checkElementHasExpression(editingContextId, + ExpressionSamplesProjectData.SemanticIds.TANK_MAX_VOLUME_ATTRIBUTE_ID, AttributeUsage.class, existingExpressionId, + "100.0 * minVolume"); + + Runnable firstEdit = this.editExpression(editingContextId, existingExpressionId::get, "50 * minVolume", this.featureValueProperties(true, true)); + + Consumer firstTreeRefresh = assertRefreshedTreeThat(tree -> { + assertThat(tree).isNotNull(); + assertThat(tree.getId()).isEqualTo(treeId.get()); + }); + + Runnable checkIntermediateFeatureValue = this.checkAttributeFeatureValue(editingContextId, ExpressionSamplesProjectData.SemanticIds.TANK_MAX_VOLUME_ATTRIBUTE_ID, + "50 * minVolume", true, true); + + Runnable refreshExpressionId = this.checkElementHasExpression(editingContextId, ExpressionSamplesProjectData.SemanticIds.TANK_MAX_VOLUME_ATTRIBUTE_ID, + AttributeUsage.class, existingExpressionId, "50 * minVolume"); + + Runnable secondEdit = this.editExpression(editingContextId, existingExpressionId::get, "60 * minVolume", this.featureValueProperties(null, false)); + + Consumer secondTreeRefresh = assertRefreshedTreeThat(tree -> { + assertThat(tree).isNotNull(); + assertThat(tree.getId()).isEqualTo(treeId.get()); + }); + + Runnable checkFinalFeatureValue = this.checkAttributeFeatureValue(editingContextId, ExpressionSamplesProjectData.SemanticIds.TANK_MAX_VOLUME_ATTRIBUTE_ID, + "60 * minVolume", true, false); + + StepVerifier.create(flux) + .consumeNextWith(initialTreeContentConsumer) + .then(checkInitialExpression) + .then(firstEdit) + .consumeNextWith(firstTreeRefresh) + .then(checkIntermediateFeatureValue) + .then(refreshExpressionId) + .then(secondEdit) + .consumeNextWith(secondTreeRefresh) + .then(checkFinalFeatureValue) + .thenCancel() + .verify(Duration.ofSeconds(10)); + } + @DisplayName("GIVEN a SysML constraint which does not have a predicate expression, WHEN creating a new expression on it THEN the new expression is created with proper name resolution") @GivenSysONServer({ ExpressionSamplesProjectData.SCRIPT_PATH }) @Test @@ -325,6 +468,43 @@ public void canCreateExpressionInEmptyConstraint() { .verify(Duration.ofSeconds(10)); } + @DisplayName("GIVEN a SysML constraint, WHEN creating an expression with feature value properties THEN the expression is created and the properties are ignored") + @GivenSysONServer({ ExpressionSamplesProjectData.SCRIPT_PATH }) + @Test + public void canCreateExpressionInConstraintIgnoringFeatureValueProperties() { + String editingContextId = ExpressionSamplesProjectData.EDITING_CONTEXT_ID; + + List 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(); + Consumer initialTreeContentConsumer = assertRefreshedTreeThat(tree -> { + assertThat(tree).isNotNull(); + treeId.set(tree.getId()); + }); + + Runnable createExpression = this.createExpression(editingContextId, ExpressionSamplesProjectData.SemanticIds.PERFORMANCE_CONCERN_ASSUME_ID, "s.enabled == true", + this.featureValueProperties(true, true)); + + Consumer treeRefreshed = assertRefreshedTreeThat(tree -> { + assertThat(tree).isNotNull(); + assertThat(tree.getId()).isEqualTo(treeId.get()); + }); + + Runnable checkCreatedExpression = this.checkElementHasExpression(editingContextId, ExpressionSamplesProjectData.SemanticIds.PERFORMANCE_CONCERN_ASSUME_ID, ConstraintUsage.class, + new AtomicReference<>(), "s.enabled == true"); + + StepVerifier.create(flux) + .consumeNextWith(initialTreeContentConsumer) + .then(createExpression) + .consumeNextWith(treeRefreshed) + .then(checkCreatedExpression) + .thenCancel() + .verify(Duration.ofSeconds(10)); + } + @DisplayName("GIVEN a SysML transition which does not have a guard expression, WHEN creating a new expression on it THEN the new expression is created with proper name resolution") @GivenSysONServer({ ExpressionSamplesProjectData.SCRIPT_PATH }) @Test @@ -440,7 +620,8 @@ public void editAttributeExpressionWithInvalidNewValue() { }); Runnable editExpressionOnMaxVolumeAttributeWithInvalidValue = () -> { - var input = new EditExpressionInput(UUID.randomUUID(), editingContextId, existingExpressionId.get(), "50 * minVolumeTypo"); + var input = new EditExpressionInput(UUID.randomUUID(), editingContextId, existingExpressionId.get(), "50 * minVolumeTypo", + this.featureValueProperties(true, true)); var result = this.editExpressionMutationRunner.run(input); String typename = JsonPath.read(result.data(), "$.data.editExpression.__typename"); assertThat(typename).isEqualTo(ErrorPayload.class.getSimpleName()); @@ -463,6 +644,9 @@ public void editAttributeExpressionWithInvalidNewValue() { // Make sure we did not create an additional invalid FeatureValue and left it around. assertThat(maxVolumeAttribute.getOwnedRelationship().stream().filter(FeatureValue.class::isInstance)).hasSize(1); + FeatureValue featureValue = maxVolumeAttribute.getOwnedRelationship().stream().filter(FeatureValue.class::isInstance).map(FeatureValue.class::cast).findFirst().orElseThrow(); + assertThat(featureValue.isIsDefault()).isFalse(); + assertThat(featureValue.isIsInitial()).isFalse(); return new ExecuteEditingContextFunctionSuccessPayload(input.id(), optionalMaxVolumeAttribute.get()); }); @@ -587,34 +771,47 @@ public void topLevelExpressionTextualRepresentation() { treeId.set(tree.getId()); }); - // The Tank part is not an expression => "" - var checkTank = this.checkExpressiontTextualRepresentation(editingContextId, ExpressionSamplesProjectData.SemanticIds.TANK_ID, ""); + // The Tank part definition is not an expression and does not expose feature value properties. + var checkTank = this.checkExpressionEditorState(editingContextId, ExpressionSamplesProjectData.SemanticIds.TANK_ID, "", null, null); // The Tank's attribute is not an expression itself either, but it owns a single, non-ambiguous expression to // can act as a "shortcut" to interact with id. - var checkTankAttribute = this.checkExpressiontTextualRepresentation(editingContextId, ExpressionSamplesProjectData.SemanticIds.TANK_MAX_VOLUME_ATTRIBUTE_ID, "100.0 * minVolume"); + var checkTankAttribute = this.checkExpressionEditorState(editingContextId, ExpressionSamplesProjectData.SemanticIds.TANK_MAX_VOLUME_ATTRIBUTE_ID, "100.0 * minVolume", + false, false); // The actual attribute default value expression however should be correctly represented - var checkTankAttributeValueExpression = this.checkExpressiontTextualRepresentation(editingContextId, ExpressionSamplesProjectData.SemanticIds.TANK_MAX_VOLUME_ATTRIBUTE_VALUE_ID, - "100.0 * minVolume"); + var checkTankAttributeValueExpression = this.checkExpressionEditorState(editingContextId, ExpressionSamplesProjectData.SemanticIds.TANK_MAX_VOLUME_ATTRIBUTE_VALUE_ID, + "100.0 * minVolume", false, false); + // An empty attribute still supports feature value properties for expression creation. + var checkEmptyTankAttribute = this.checkExpressionEditorState(editingContextId, ExpressionSamplesProjectData.SemanticIds.TANK_PRESSURE_ATTRIBUTE_ID, "", false, false); - var checkPerformanceConcern = this.checkExpressiontTextualRepresentation(editingContextId, ExpressionSamplesProjectData.SemanticIds.PERFORMANCE_CONCERN_ID, ""); + var checkPerformanceConcern = this.checkExpressionEditorState(editingContextId, ExpressionSamplesProjectData.SemanticIds.PERFORMANCE_CONCERN_ID, "", null, null); // A ConstaintUsage *is* an Expression from the point of view of SysML's type hierarchy, but not a top-level // Expression. However if it contains a single expression, it an act as a "shortcut" to it. - var checkPerformanceConcernAssumeConstraint = this.checkExpressiontTextualRepresentation(editingContextId, ExpressionSamplesProjectData.SemanticIds.PERFORMANCE_CONCERN_ASSUME_ID, ""); - var checkPerformanceConcernRequireConstraint = this.checkExpressiontTextualRepresentation(editingContextId, ExpressionSamplesProjectData.SemanticIds.PERFORMANCE_CONCERN_REQUIRE_ID, - "s.samplingRate >= 50.0 & s.currentValue != 0.0 | s.errorCount == 0"); + var checkPerformanceConcernAssumeConstraint = this.checkExpressionEditorState(editingContextId, ExpressionSamplesProjectData.SemanticIds.PERFORMANCE_CONCERN_ASSUME_ID, "", null, + null); + var checkPerformanceConcernRequireConstraint = this.checkExpressionEditorState(editingContextId, ExpressionSamplesProjectData.SemanticIds.PERFORMANCE_CONCERN_REQUIRE_ID, + "s.samplingRate >= 50.0 & s.currentValue != 0.0 | s.errorCount == 0", null, null); // require s.samplingRate >= 50.0 & s.currentValue != 0.0 | s.errorCount == 0 - var checkPerformanceConcernRequireConstraintExpression = this.checkExpressiontTextualRepresentation(editingContextId, - ExpressionSamplesProjectData.SemanticIds.PERFORMANCE_CONCERN_REQUIRE_EXPRESSION_ID, "s.samplingRate >= 50.0 & s.currentValue != 0.0 | s.errorCount == 0"); + var checkPerformanceConcernRequireConstraintExpression = this.checkExpressionEditorState(editingContextId, + ExpressionSamplesProjectData.SemanticIds.PERFORMANCE_CONCERN_REQUIRE_EXPRESSION_ID, "s.samplingRate >= 50.0 & s.currentValue != 0.0 | s.errorCount == 0", null, null); + var checkHeatingTransition = this.checkExpressionEditorState(editingContextId, ExpressionSamplesProjectData.SemanticIds.THERMAL_CONTROL_TO_HEATING_TRANSITION_ID, "", null, null); + var checkHeatingSuccession = this.checkExpressionEditorState(editingContextId, ExpressionSamplesProjectData.SemanticIds.THERMAL_CONTROL_TO_HEATING_SUCCESSION_ID, "", null, + null); + var checkCoolingTransition = this.checkExpressionEditorState(editingContextId, ExpressionSamplesProjectData.SemanticIds.THERMAL_CONTROL_TO_COOLING_TRANSITION_ID, + "currentTemp > targetTemp + tolerance", null, null); StepVerifier.create(flux) .consumeNextWith(initialTreeContentConsumer) .then(checkTank) .then(checkTankAttribute) .then(checkTankAttributeValueExpression) + .then(checkEmptyTankAttribute) .then(checkPerformanceConcern) .then(checkPerformanceConcernAssumeConstraint) .then(checkPerformanceConcernRequireConstraint) .then(checkPerformanceConcernRequireConstraintExpression) + .then(checkHeatingTransition) + .then(checkHeatingSuccession) + .then(checkCoolingTransition) .thenCancel() .verify(Duration.ofSeconds(10)); } @@ -850,8 +1047,12 @@ private Runnable semanticCheck(String editingContextId, BiFunction { - var input = new CreateExpressionInput(UUID.randomUUID(), editingContextId, parentElementId, expressionContent); + var input = new CreateExpressionInput(UUID.randomUUID(), editingContextId, parentElementId, expressionContent, properties); var result = this.createExpressionMutationRunner.run(input); String typename = JsonPath.read(result.data(), "$.data.createExpression.__typename"); assertThat(typename).isEqualTo(CreateExpressionSuccessPayload.class.getSimpleName()); @@ -859,20 +1060,34 @@ private Runnable createExpression(String editingContextId, String parentElementI } private Runnable editExpression(String editingContextId, Supplier elementId, String expressionContent) { + return this.editExpression(editingContextId, elementId, expressionContent, null); + } + + private Runnable editExpression(String editingContextId, Supplier elementId, String expressionContent, ExpressionPropertiesInput properties) { return () -> { - var input = new EditExpressionInput(UUID.randomUUID(), editingContextId, elementId.get(), expressionContent); + var input = new EditExpressionInput(UUID.randomUUID(), editingContextId, elementId.get(), expressionContent, properties); var result = this.editExpressionMutationRunner.run(input); String typename = JsonPath.read(result.data(), "$.data.editExpression.__typename"); assertThat(typename).isEqualTo(EditExpressionSuccessPayload.class.getSimpleName()); }; } - private Runnable checkExpressiontTextualRepresentation(String editingContextId, String elementId, String expectedTextualRepresentation) { + private Runnable checkExpressionEditorState(String editingContextId, String elementId, String expectedTextualRepresentation, Boolean expectedIsDefault, + Boolean expectedIsInitial) { return () -> { Map variables = Map.of("editingContextId", editingContextId, "elementId", elementId); var result = this.expressionTextualRepresentationQueryRunner.run(variables); - String textualRepresentation = JsonPath.read(result.data(), "$.data.viewer.editingContext.expressionTextualRepresentation"); + String textualRepresentation = JsonPath.read(result.data(), "$.data.viewer.editingContext.expressionTextualRepresentation.textualRepresentation"); assertThat(textualRepresentation).as("elementId: {}", elementId).isEqualTo(expectedTextualRepresentation); + if (expectedIsDefault == null && expectedIsInitial == null) { + Object featureValueProperties = JsonPath.read(result.data(), "$.data.viewer.editingContext.expressionTextualRepresentation.featureValueProperties"); + assertThat(featureValueProperties).as("elementId: {}", elementId).isNull(); + } else { + Boolean isDefault = JsonPath.read(result.data(), "$.data.viewer.editingContext.expressionTextualRepresentation.featureValueProperties.isDefault"); + Boolean isInitial = JsonPath.read(result.data(), "$.data.viewer.editingContext.expressionTextualRepresentation.featureValueProperties.isInitial"); + assertThat(isDefault).as("elementId: {}", elementId).isEqualTo(expectedIsDefault); + assertThat(isInitial).as("elementId: {}", elementId).isEqualTo(expectedIsInitial); + } }; } @@ -900,4 +1115,27 @@ private Runnable checkElementHasExpression(String editingCon return new ExecuteEditingContextFunctionSuccessPayload(input.id(), optionalElement.get()); }); } + + private Runnable checkAttributeFeatureValue(String editingContextId, String attributeId, String expectedExpressionTextualRepresentation, boolean expectedIsDefault, + boolean expectedIsInitial) { + return this.semanticCheck(editingContextId, (editingContext, input) -> { + var optionalElement = this.objectSearchService.getObject(editingContext, attributeId); + assertThat(optionalElement).containsInstanceOf(AttributeUsage.class); + AttributeUsage attributeUsage = (AttributeUsage) optionalElement.get(); + Optional optionalFeatureValue = attributeUsage.getOwnedRelationship().stream() + .filter(FeatureValue.class::isInstance) + .map(FeatureValue.class::cast) + .findFirst(); + assertThat(optionalFeatureValue).isPresent(); + assertThat(this.metamodelQueryElementService.getExpressionTextualRepresentation(optionalFeatureValue.get().getValue())) + .isEqualTo(expectedExpressionTextualRepresentation); + assertThat(optionalFeatureValue.get().isIsDefault()).isEqualTo(expectedIsDefault); + assertThat(optionalFeatureValue.get().isIsInitial()).isEqualTo(expectedIsInitial); + return new ExecuteEditingContextFunctionSuccessPayload(input.id(), optionalElement.get()); + }); + } + + private ExpressionPropertiesInput featureValueProperties(Boolean isDefault, Boolean isInitial) { + return new ExpressionPropertiesInput(new FeatureValueExpressionPropertiesInput(isDefault, isInitial)); + } } diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/expressions/graphql/ExpressionTextualRepresentationQueryRunner.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/expressions/graphql/ExpressionTextualRepresentationQueryRunner.java index c279d886b..ea225a101 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/expressions/graphql/ExpressionTextualRepresentationQueryRunner.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/expressions/graphql/ExpressionTextualRepresentationQueryRunner.java @@ -31,7 +31,13 @@ public class ExpressionTextualRepresentationQueryRunner implements IQueryRunner query getExpressionTextualRepresentation($editingContextId: ID!, $elementId: ID!) { viewer { editingContext(editingContextId: $editingContextId) { - expressionTextualRepresentation(elementId: $elementId) + expressionTextualRepresentation(elementId: $elementId) { + textualRepresentation + featureValueProperties { + isDefault + isInitial + } + } } } } diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/data/ExpressionSamplesProjectData.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/data/ExpressionSamplesProjectData.java index 6da4bfbf0..ec9d8c94e 100644 --- a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/data/ExpressionSamplesProjectData.java +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/data/ExpressionSamplesProjectData.java @@ -56,6 +56,8 @@ public static final class SemanticIds { public static final String THERMAL_CONTROL_TO_HEATING_TRANSITION_ID = "0b517687-ef3f-4057-9ce5-27e27f627a22"; + public static final String THERMAL_CONTROL_TO_HEATING_SUCCESSION_ID = "c59c0174-da0a-4972-a5ca-6743ab295651"; + public static final String THERMAL_CONTROL_TO_COOLING_TRANSITION_ID = "e1c27d0b-476e-42d4-9b81-b5f064d9cbbe"; public static final String THERMAL_CONTROL_TO_COOLING_TRANSITION_GUARD_EXPRESSION_ID = "ec67ead5-7739-4dd3-a82e-f1bb5ef34268"; @@ -81,6 +83,7 @@ public static final class SemanticIds { PERFORMANCE_CONCERN_REQUIRE_EXPRESSION_ID, THERMAL_CONTROL_STATE_DEFINITION_ID, THERMAL_CONTROL_TO_HEATING_TRANSITION_ID, + THERMAL_CONTROL_TO_HEATING_SUCCESSION_ID, THERMAL_CONTROL_TO_COOLING_TRANSITION_ID, THERMAL_CONTROL_TO_COOLING_TRANSITION_GUARD_EXPRESSION_ID); } diff --git a/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/dto/CreateExpressionInput.java b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/dto/CreateExpressionInput.java index 7cdddbc3c..e4c95d918 100644 --- a/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/dto/CreateExpressionInput.java +++ b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/dto/CreateExpressionInput.java @@ -19,7 +19,32 @@ /** * The input object of the createExpression operation. * + * @param id + * the mutation identifier. + * @param editingContextId + * the editing context identifier. + * @param parentElementId + * the identifier of the element in which the expression should be created. + * @param expressionText + * the textual representation of the expression to create. + * @param properties + * the optional typed properties to apply once the expression is created successfully. * @author pcdavid */ -public record CreateExpressionInput(UUID id, String editingContextId, String parentElementId, String expressionText) implements IInput { +public record CreateExpressionInput(UUID id, String editingContextId, String parentElementId, String expressionText, ExpressionPropertiesInput properties) implements IInput { + /** + * Creates an input without optional properties. + * + * @param id + * the mutation identifier. + * @param editingContextId + * the editing context identifier. + * @param parentElementId + * the identifier of the element in which the expression should be created. + * @param expressionText + * the textual representation of the expression to create. + */ + public CreateExpressionInput(UUID id, String editingContextId, String parentElementId, String expressionText) { + this(id, editingContextId, parentElementId, expressionText, null); + } } diff --git a/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/dto/EditExpressionInput.java b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/dto/EditExpressionInput.java index ca0d50bda..d0eef2c69 100644 --- a/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/dto/EditExpressionInput.java +++ b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/dto/EditExpressionInput.java @@ -19,7 +19,32 @@ /** * The input object of the {@code editExpression} mutation. * + * @param id + * the mutation identifier. + * @param editingContextId + * the editing context identifier. + * @param elementId + * the identifier of the expression itself or of the parent element that owns a single expression. + * @param newExpressionText + * the textual representation of the replacement expression. + * @param properties + * the optional typed properties to apply once the expression is edited successfully. * @author pcdavid */ -public record EditExpressionInput(UUID id, String editingContextId, String elementId, String newExpressionText) implements IInput { +public record EditExpressionInput(UUID id, String editingContextId, String elementId, String newExpressionText, ExpressionPropertiesInput properties) implements IInput { + /** + * Creates an input without optional properties. + * + * @param id + * the mutation identifier. + * @param editingContextId + * the editing context identifier. + * @param elementId + * the identifier of the expression itself or of the parent element that owns a single expression. + * @param newExpressionText + * the textual representation of the replacement expression. + */ + public EditExpressionInput(UUID id, String editingContextId, String elementId, String newExpressionText) { + this(id, editingContextId, elementId, newExpressionText, null); + } } diff --git a/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/dto/ExpressionPropertiesInput.java b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/dto/ExpressionPropertiesInput.java new file mode 100644 index 000000000..60bb4e15a --- /dev/null +++ b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/dto/ExpressionPropertiesInput.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * 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.dto; + +/** + * Optional typed properties associated with an expression mutation. + * + * @param featureValue + * properties to apply when the expression belongs to a {@code FeatureValue}. + * @author arichard + */ +public record ExpressionPropertiesInput(FeatureValueExpressionPropertiesInput featureValue) { +} diff --git a/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/dto/FeatureValueExpressionPropertiesInput.java b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/dto/FeatureValueExpressionPropertiesInput.java new file mode 100644 index 000000000..2d8d42881 --- /dev/null +++ b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/dto/FeatureValueExpressionPropertiesInput.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * 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.dto; + +/** + * Optional properties that can be applied to a {@code FeatureValue} expression owner. + * + * @param isDefault + * whether the feature value is marked as default. A {@code null} value leaves the current state unchanged. + * @param isInitial + * whether the feature value is marked as initial. A {@code null} value leaves the current state unchanged. + * @author arichard + */ +public record FeatureValueExpressionPropertiesInput(Boolean isDefault, Boolean isInitial) { +} diff --git a/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/CreateExpressionEventHandler.java b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/CreateExpressionEventHandler.java index 15ce52584..26235594c 100644 --- a/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/CreateExpressionEventHandler.java +++ b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/CreateExpressionEventHandler.java @@ -90,7 +90,8 @@ public void handle(One payloadSink, Many changeDesc if (this.metamodelQueryElementService.hasSingleExpressionDefinition(optionalParentElement.get())) { payload = new ErrorPayload(input.id(), "The parent element already has an expression"); } else { - var result = this.expressionEditor.createExpression(emfEditingContext, optionalParentElement.get(), createExpressionInput.expressionText()); + var result = this.expressionEditor.createExpression(emfEditingContext, optionalParentElement.get(), createExpressionInput.expressionText(), + createExpressionInput.properties()); if (result.createdExpression() != null) { var createdExpression = result.createdExpression(); var createdExpressionId = this.identityService.getId(createdExpression); diff --git a/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/EditExpressionEventHandler.java b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/EditExpressionEventHandler.java index 2c11c5214..095e315e4 100644 --- a/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/EditExpressionEventHandler.java +++ b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/EditExpressionEventHandler.java @@ -87,7 +87,8 @@ public void handle(Sinks.One payloadSink, Sinks.Manyreplace this one instead of just being added. * @param expressionText * the textual representation of the expression to create. + * @param properties + * the optional typed properties to apply after the expression is created or replaced. * @return the result of creating a new expression. It can either contain the actual expression created (on success) * or a list of messages on failure. */ - private ExpressionCreationResult createOrEditExpression(IEMFEditingContext emfEditingContext, Element parentElement, Optional expressionToReplace, String expressionText) { + private ExpressionCreationResult createOrEditExpression(IEMFEditingContext emfEditingContext, Element parentElement, Optional expressionToReplace, String expressionText, + ExpressionPropertiesInput properties) { List newObjects = List.of(); List messages = new ArrayList<>(); @@ -106,12 +113,57 @@ private ExpressionCreationResult createOrEditExpression(IEMFEditingContext emfEd if (expressionToReplace.isPresent()) { this.replace(expressionToReplace.get(), optionalResult.get()); } + this.applyProperties(optionalResult.get(), properties); return new ExpressionCreationResult(optionalResult.get(), messages); } else { return new ExpressionCreationResult(null, messages); } } + /** + * Applies supported properties to the owner of the created or edited expression. + * + * @param expression + * the created or edited expression. + * @param properties + * the optional properties to apply. + */ + private void applyProperties(Expression expression, ExpressionPropertiesInput properties) { + if (properties != null && properties.featureValue() != null) { + this.getOwningFeatureValue(expression).ifPresent(featureValue -> this.applyFeatureValueProperties(featureValue, properties.featureValue())); + } + } + + /** + * Gets the owning {@link FeatureValue} of the provided expression, if any. + * + * @param expression + * the expression whose owner should be inspected. + * @return the owning {@link FeatureValue}, if one exists. + */ + private Optional getOwningFeatureValue(Expression expression) { + return Optional.ofNullable(expression.getOwningRelationship()) + .filter(FeatureValue.class::isInstance) + .map(FeatureValue.class::cast); + } + + /** + * Applies optional {@link FeatureValue} properties while preserving existing values for omitted fields. + * + * @param featureValue + * the feature value to update. + * @param properties + * the optional properties to apply. + */ + private void applyFeatureValueProperties(FeatureValue featureValue, FeatureValueExpressionPropertiesInput properties) { + if (properties.isDefault() != null) { + featureValue.setIsDefault(properties.isDefault()); + } + if (properties.isInitial() != null) { + featureValue.setIsInitial(properties.isInitial()); + } + } + /** * Determines if the messages we received from the parser and converter indicate an error. We treat warnings as * errors here. diff --git a/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/api/ISysMLExpressionEditor.java b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/api/ISysMLExpressionEditor.java index 939b5d561..a9191e2aa 100644 --- a/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/api/ISysMLExpressionEditor.java +++ b/backend/application/syson-sysml-import/src/main/java/org/eclipse/syson/sysml/services/api/ISysMLExpressionEditor.java @@ -18,6 +18,7 @@ import org.eclipse.syson.sysml.LiteralBoolean; import org.eclipse.syson.sysml.OperatorExpression; import org.eclipse.syson.sysml.Relationship; +import org.eclipse.syson.sysml.dto.ExpressionPropertiesInput; /** * Use to create of edit a SySMLv2 {@link Expression} in a given context from a given text fragment. @@ -35,10 +36,12 @@ public interface ISysMLExpressionEditor { * relationship} into which the expression is added will depend on the parent's type. * @param expressionText * the plain text representation of the expression to create. + * @param properties + * the optional typed properties to apply after creation succeeds. * @return the result of creating a new expression. It can either contain the actual expression created (on success) * or a list of messages on failure. */ - ExpressionCreationResult createExpression(IEMFEditingContext emfEditingContext, Element parentElement, String expressionText); + ExpressionCreationResult createExpression(IEMFEditingContext emfEditingContext, Element parentElement, String expressionText, ExpressionPropertiesInput properties); /** * "Edit" an existing {@link Expression expression} inside the specified parent element from a textual @@ -54,8 +57,10 @@ public interface ISysMLExpressionEditor { * which the expression is added will depend on the parent's type. * @param expressionText * the plain text representation of the expression to create as a replacement. + * @param properties + * the optional typed properties to apply after the replacement succeeds. * @return the result of editing the expression. It can either contain the actual new expression created as a * replacement (on success) or a list of messages on failure. */ - ExpressionCreationResult editExpression(IEMFEditingContext emfEditingContext, Element parentElement, Expression expression, String expressionText); + ExpressionCreationResult editExpression(IEMFEditingContext emfEditingContext, Element parentElement, Expression expression, String expressionText, ExpressionPropertiesInput properties); } diff --git a/backend/application/syson-sysml-import/src/main/resources/schema/syson-import.graphqls b/backend/application/syson-sysml-import/src/main/resources/schema/syson-import.graphqls index 3eb3b4ada..d33870d57 100644 --- a/backend/application/syson-sysml-import/src/main/resources/schema/syson-import.graphqls +++ b/backend/application/syson-sysml-import/src/main/resources/schema/syson-import.graphqls @@ -21,6 +21,7 @@ input CreateExpressionInput { editingContextId: ID! parentElementId: ID! expressionText: String! + properties: ExpressionPropertiesInput } union CreateExpressionPayload = ErrorPayload | CreateExpressionSuccessPayload @@ -36,6 +37,16 @@ input EditExpressionInput { editingContextId: ID! elementId: ID! newExpressionText: String! + properties: ExpressionPropertiesInput +} + +input ExpressionPropertiesInput { + featureValue: FeatureValueExpressionPropertiesInput +} + +input FeatureValueExpressionPropertiesInput { + isDefault: Boolean + isInitial: Boolean } union EditExpressionPayload = ErrorPayload | EditExpressionSuccessPayload @@ -44,4 +55,4 @@ type EditExpressionSuccessPayload { id: ID! newExpressionId: ID! messages: [Message]! -} \ No newline at end of file +} diff --git a/doc/content/modules/user-manual/assets/images/expression-feature-value-properties.png b/doc/content/modules/user-manual/assets/images/expression-feature-value-properties.png new file mode 100644 index 000000000..e7765f41e Binary files /dev/null and b/doc/content/modules/user-manual/assets/images/expression-feature-value-properties.png differ diff --git a/doc/content/modules/user-manual/assets/images/release-notes-expression-feature-value-properties.png b/doc/content/modules/user-manual/assets/images/release-notes-expression-feature-value-properties.png new file mode 100644 index 000000000..e7765f41e Binary files /dev/null and b/doc/content/modules/user-manual/assets/images/release-notes-expression-feature-value-properties.png differ diff --git a/doc/content/modules/user-manual/pages/features/expressions.adoc b/doc/content/modules/user-manual/pages/features/expressions.adoc index 3b7adfb9c..05893fa78 100644 --- a/doc/content/modules/user-manual/pages/features/expressions.adoc +++ b/doc/content/modules/user-manual/pages/features/expressions.adoc @@ -42,6 +42,16 @@ Fix any error reported before clicking on _Update_ again (or canceling the opera image::edit-expression-modal.png[Edit expression modal, width=80%] image::edit-expression-modal-error.png[Edit expression modal showing an error message if the new expression is invalid, width=80%] +When the expression belongs to a `FeatureValue` context, such as the value of an `AttributeUsage`, the modal also displays two additional checkboxes: + +* _Default value_ +* _Initial value_ + +These checkboxes allow editing the properties of the owning `FeatureValue` together with the expression text. +They are only shown when the selected semantic element can host a `FeatureValue`; they are not shown for expression contexts such as `ConstraintUsage` or transition guard expressions. ++ +image::expression-feature-value-properties-placeholder.png[Expression modal checkboxes in a FeatureValue context, width=60%,height=60%] + == Edition To edit an already existing expression, invoke the _Edit expression_ action directly on the existing expression or on its parent element. @@ -54,6 +64,11 @@ Edit the textual representation of the expression as required, and click on the As for expression creation, the change will only be applied if there are no errors detected; otherwise the modal will display any errors (e.g. names used in the expression that can not be resolve to existing elements) so that you can fix the input. +If the edited expression is owned by a `FeatureValue`, the _Default value_ and _Initial value_ checkboxes are also pre-filled with the current state of the owning `FeatureValue`. +Updating either checkbox and applying the dialog persists the change together with the expression edition. ++ +image::expression-feature-value-properties-placeholder.png[Expression modal checkboxes in a FeatureValue context, width=60%,height=60%] + Note that if the new text value entered for the expression is empty, applying the change will _delete_ the expression. == Deletion diff --git a/doc/content/modules/user-manual/pages/release-notes/2026.7.0.adoc b/doc/content/modules/user-manual/pages/release-notes/2026.7.0.adoc index 2443c6d9f..368fe2865 100644 --- a/doc/content/modules/user-manual/pages/release-notes/2026.7.0.adoc +++ b/doc/content/modules/user-manual/pages/release-notes/2026.7.0.adoc @@ -85,6 +85,16 @@ Fix any error reported before clicking on _Update_ again (or canceling the opera image::edit-expression-modal.png[Edit expression modal, width=80%] image::edit-expression-modal-error.png[Edit expression modal showing an error message if the new expression is invalid, width=80%] + +When the edited expression is the value of a `FeatureValue` (for example the value of an `AttributeUsage`), the modal now also exposes two dedicated checkboxes: + +* _Default value_ +* _Initial value_ + +These checkboxes make it possible to update the `FeatureValue` properties at the same time as the expression text. +They are only displayed when the selected semantic context can actually host a `FeatureValue`. ++ +image::release-notes-expression-feature-value-properties.png[Expression modal checkboxes on a FeatureValue context, width=60%,height=60%] ++ To _edit_ an existing expression, invoke the _Edit expression_ context menu action directly on the existing expression or on its parent element. The same modal will open with the current textual representation of the expression pre-filled. + To _delete_ an existing expression, you can simply invoke the normal _Delete_ menu item action on the expression itself or the new _Delete expression_ on its parent element. @@ -94,6 +104,9 @@ To _delete_ an existing expression, you can simply invoke the normal _Delete_ me ** When an `Expression` element or an `Element` that contains a single `Expression` element is selected (for example from the _Explorer_ view), the _Details_ view displays the textual representation of the `Expression`, with a button to open the expression editing modal (see above) directly on it: + image::expression-details.png[Expression value displayed in the _Details_ view, width=80%] ++ +When this modal is opened on the value expression of a `FeatureValue`, it also displays the _Default value_ and _Initial value_ checkboxes. +These controls are not shown for other expression contexts such as `ConstraintUsage` predicate expressions or transition guard expressions. == Bug fixes diff --git a/frontend/syson-components/src/extensions/expressions/EditSysMLExpressionModal.tsx b/frontend/syson-components/src/extensions/expressions/EditSysMLExpressionModal.tsx index b176008da..ea91a141d 100644 --- a/frontend/syson-components/src/extensions/expressions/EditSysMLExpressionModal.tsx +++ b/frontend/syson-components/src/extensions/expressions/EditSysMLExpressionModal.tsx @@ -30,7 +30,13 @@ import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import { useEffect, useRef, useState } from 'react'; import { makeStyles } from 'tss-react/mui'; +import { ExpressionFeatureValueProperties } from './ExpressionFeatureValueProperties'; import { EditSysMLExpressionModalProps, EditSysMLExpressionModalState } from './EditSysMLExpressionModal.types'; +import { + defaultFeatureValueExpressionProperties, + FeatureValueExpressionProperties, + GQLExpressionProperties, +} from './ExpressionProperties.types'; import { useCreateExpression } from './useCreateExpression'; import { useDeleteExpression } from './useDeleteExpression'; import { useEditExpression } from './useEditExpression'; @@ -80,6 +86,19 @@ const computeValidationStatus = (validationResult: GQLMessage[] | null): Validat } }; +const sanitizeFeatureValueProperties = ( + featureValueProperties: FeatureValueExpressionProperties | null | undefined +): FeatureValueExpressionProperties => ({ + isDefault: featureValueProperties?.isDefault ?? false, + isInitial: featureValueProperties?.isInitial ?? false, +}); + +const toExpressionProperties = ( + featureValueProperties: FeatureValueExpressionProperties | null | undefined +): GQLExpressionProperties => ({ + featureValue: sanitizeFeatureValueProperties(featureValueProperties ?? defaultFeatureValueExpressionProperties), +}); + export const EditSysMLExpressionModal = ({ editingContextId, elementId, @@ -92,13 +111,14 @@ export const EditSysMLExpressionModal = ({ textualContent: null, operationInProgress: null, validationResult: null, + properties: toExpressionProperties(null), }); const validationStatus = computeValidationStatus(state.validationResult); const busy = state.operationInProgress !== null; const inputRef = useRef(null); - const { textualRepresentation, loading } = useExpressionTextualRepresentation(editingContextId, elementId); + const { editorState, loading } = useExpressionTextualRepresentation(editingContextId, elementId); useEffect(() => { if (loading) { setState((prevState) => ({ ...prevState, operationInProgress: 'loading' })); @@ -106,11 +126,14 @@ export const EditSysMLExpressionModal = ({ setState((prevState) => ({ ...prevState, operationInProgress: null, - textualContent: textualRepresentation, + textualContent: editorState?.textualRepresentation ?? '', validationResult: null, + properties: toExpressionProperties(editorState?.featureValueProperties), })); } - }, [textualRepresentation, loading]); + }, [editorState, loading]); + + const supportsFeatureValueProperties = editorState?.featureValueProperties != null; const fieldReady = !busy && state.textualContent !== null; useEffect(() => { @@ -130,14 +153,24 @@ export const EditSysMLExpressionModal = ({ if (state.textualContent !== null) { if (mode === 'create' && state.textualContent.trim() !== '') { setState((prevState) => ({ ...prevState, operationInProgress: 'creating' })); - createExpression(editingContextId, elementId, state.textualContent); + createExpression( + editingContextId, + elementId, + state.textualContent, + supportsFeatureValueProperties ? state.properties : null + ); } else if (mode === 'edit' && state.textualContent !== null) { if (state.textualContent.trim() === '') { deleteExpression(editingContextId, elementId); onClose(); } else { setState((prevState) => ({ ...prevState, operationInProgress: 'editing' })); - editExpression(editingContextId, elementId, state.textualContent); + editExpression( + editingContextId, + elementId, + state.textualContent, + supportsFeatureValueProperties ? state.properties : null + ); } } } @@ -203,6 +236,21 @@ export const EditSysMLExpressionModal = ({ }, }} /> + {supportsFeatureValueProperties ? ( + + setState((prevState) => ({ + ...prevState, + properties: { + ...prevState.properties, + featureValue, + }, + })) + } + /> + ) : null}
{validationStatus === 'invalid' && state.validationResult ? ( diff --git a/frontend/syson-components/src/extensions/expressions/EditSysMLExpressionModal.types.ts b/frontend/syson-components/src/extensions/expressions/EditSysMLExpressionModal.types.ts index 2d1754fbd..fced3f474 100644 --- a/frontend/syson-components/src/extensions/expressions/EditSysMLExpressionModal.types.ts +++ b/frontend/syson-components/src/extensions/expressions/EditSysMLExpressionModal.types.ts @@ -12,6 +12,7 @@ *******************************************************************************/ import { GQLMessage } from '@eclipse-sirius/sirius-components-core'; +import { GQLExpressionProperties } from './ExpressionProperties.types'; export type Mode = 'create' | 'edit'; @@ -28,4 +29,5 @@ export interface EditSysMLExpressionModalState { operationInProgress: Operation | null; textualContent: string | null; validationResult: GQLMessage[] | null; + properties: GQLExpressionProperties; } diff --git a/frontend/syson-components/src/extensions/expressions/ExpressionFeatureValueProperties.tsx b/frontend/syson-components/src/extensions/expressions/ExpressionFeatureValueProperties.tsx new file mode 100644 index 000000000..fa2d62dd6 --- /dev/null +++ b/frontend/syson-components/src/extensions/expressions/ExpressionFeatureValueProperties.tsx @@ -0,0 +1,84 @@ +/******************************************************************************* + * 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 Checkbox from '@mui/material/Checkbox'; +import FormLabel from '@mui/material/FormLabel'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormGroup from '@mui/material/FormGroup'; +import { Theme } from '@mui/material/styles'; +import { FeatureValueExpressionProperties } from './ExpressionProperties.types'; +import { makeStyles } from 'tss-react/mui'; + +const useStyles = makeStyles()((theme: Theme) => ({ + label: { + fontFamily: theme.typography.body2.fontFamily, + fontSize: theme.typography.body2.fontSize, + fontWeight: theme.typography.body2.fontWeight, + lineHeight: theme.typography.body2.lineHeight, + letterSpacing: theme.typography.body2.letterSpacing, + color: theme.palette.text.secondary, + }, + checkboxLabel: { + '& .MuiFormControlLabel-label': { + fontFamily: theme.typography.body2.fontFamily, + fontSize: theme.typography.body2.fontSize, + fontWeight: theme.typography.body2.fontWeight, + lineHeight: theme.typography.body2.lineHeight, + letterSpacing: theme.typography.body2.letterSpacing, + color: theme.palette.text.secondary, + }, + }, +})); + +export interface ExpressionFeatureValuePropertiesProps { + disabled: boolean; + properties: FeatureValueExpressionProperties; + onChange: (properties: FeatureValueExpressionProperties) => void; +} + +export const ExpressionFeatureValueProperties = ({ + disabled, + properties, + onChange, +}: ExpressionFeatureValuePropertiesProps) => { + const { classes } = useStyles(); + + return ( +
+ Properties + + onChange({ ...properties, isDefault: checked })} + /> + } + label="Default value" + /> + onChange({ ...properties, isInitial: checked })} + /> + } + label="Initial value" + /> + +
+ ); +}; diff --git a/frontend/syson-components/src/extensions/expressions/ExpressionProperties.types.ts b/frontend/syson-components/src/extensions/expressions/ExpressionProperties.types.ts new file mode 100644 index 000000000..e55bbe9b5 --- /dev/null +++ b/frontend/syson-components/src/extensions/expressions/ExpressionProperties.types.ts @@ -0,0 +1,26 @@ +/******************************************************************************* + * 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 FeatureValueExpressionProperties { + isDefault: boolean; + isInitial: boolean; +} + +export interface GQLExpressionProperties { + featureValue: FeatureValueExpressionProperties; +} + +export const defaultFeatureValueExpressionProperties: FeatureValueExpressionProperties = { + isDefault: false, + isInitial: false, +}; diff --git a/frontend/syson-components/src/extensions/expressions/useCreateExpression.ts b/frontend/syson-components/src/extensions/expressions/useCreateExpression.ts index e41ef325c..0cd1df75e 100644 --- a/frontend/syson-components/src/extensions/expressions/useCreateExpression.ts +++ b/frontend/syson-components/src/extensions/expressions/useCreateExpression.ts @@ -12,6 +12,7 @@ *******************************************************************************/ import { gql, useMutation } from '@apollo/client'; import { GQLMessage } from '@eclipse-sirius/sirius-components-core'; +import { GQLExpressionProperties } from './ExpressionProperties.types'; import { GQLCreateExpressionData, GQLCreateExpressionInput, @@ -51,12 +52,18 @@ export const useCreateExpression = (): UseCreateExpressionValue => { GQLCreateExpressionVariables >(createExpressionMutation); - const createExpression = (editingContextId: string, parentElementId: string, expressionText: string) => { + const createExpression = ( + editingContextId: string, + parentElementId: string, + expressionText: string, + properties?: GQLExpressionProperties | null + ) => { const input: GQLCreateExpressionInput = { id: crypto.randomUUID(), editingContextId, parentElementId, expressionText, + properties, }; performCreateExpression({ variables: { input } }); }; diff --git a/frontend/syson-components/src/extensions/expressions/useCreateExpression.types.ts b/frontend/syson-components/src/extensions/expressions/useCreateExpression.types.ts index 77dec46af..ed6352cda 100644 --- a/frontend/syson-components/src/extensions/expressions/useCreateExpression.types.ts +++ b/frontend/syson-components/src/extensions/expressions/useCreateExpression.types.ts @@ -11,9 +11,15 @@ * Obeo - initial API and implementation *******************************************************************************/ import { GQLMessage } from '@eclipse-sirius/sirius-components-core'; +import { GQLExpressionProperties } from './ExpressionProperties.types'; export interface UseCreateExpressionValue { - createExpression: (editingContextId: string, parentElementId: string, expressionText: string) => void; + createExpression: ( + editingContextId: string, + parentElementId: string, + expressionText: string, + properties?: GQLExpressionProperties | null + ) => void; messages: GQLMessage[] | null; newExpressionId: string | null; loading: boolean; @@ -28,6 +34,7 @@ export interface GQLCreateExpressionInput { editingContextId: string; parentElementId: string; expressionText: string; + properties?: GQLExpressionProperties | null; } export interface GQLCreateExpressionData { diff --git a/frontend/syson-components/src/extensions/expressions/useEditExpression.ts b/frontend/syson-components/src/extensions/expressions/useEditExpression.ts index 6cc80e48b..ee663177b 100644 --- a/frontend/syson-components/src/extensions/expressions/useEditExpression.ts +++ b/frontend/syson-components/src/extensions/expressions/useEditExpression.ts @@ -13,6 +13,7 @@ import { gql, useMutation } from '@apollo/client'; import { GQLMessage } from '@eclipse-sirius/sirius-components-core'; +import { GQLExpressionProperties } from './ExpressionProperties.types'; import { GQLEditExpressionData, GQLEditExpressionInput, @@ -52,12 +53,18 @@ export const useEditExpression = (): UseEditExpressionValue => { GQLEditExpressionVariables >(editExpressionMutation); - const editExpression = (editingContextId: string, elementId: string, newExpressionText: string) => { + const editExpression = ( + editingContextId: string, + elementId: string, + newExpressionText: string, + properties?: GQLExpressionProperties | null + ) => { const input: GQLEditExpressionInput = { id: crypto.randomUUID(), editingContextId, elementId: elementId, newExpressionText, + properties, }; performEditExpression({ variables: { input } }); diff --git a/frontend/syson-components/src/extensions/expressions/useEditExpression.types.ts b/frontend/syson-components/src/extensions/expressions/useEditExpression.types.ts index a7fbd4be3..30afc8954 100644 --- a/frontend/syson-components/src/extensions/expressions/useEditExpression.types.ts +++ b/frontend/syson-components/src/extensions/expressions/useEditExpression.types.ts @@ -12,9 +12,15 @@ *******************************************************************************/ import { GQLMessage } from '@eclipse-sirius/sirius-components-core'; +import { GQLExpressionProperties } from './ExpressionProperties.types'; export interface UseEditExpressionValue { - editExpression: (editingContextId: string, elementId: string, newValue: string) => void; + editExpression: ( + editingContextId: string, + elementId: string, + newValue: string, + properties?: GQLExpressionProperties | null + ) => void; loading: boolean; newExpressionId: string | null; messages: GQLMessage[] | null; @@ -29,6 +35,7 @@ export interface GQLEditExpressionInput { editingContextId: string; elementId: string; newExpressionText: string; + properties?: GQLExpressionProperties | null; } export interface GQLEditExpressionData { diff --git a/frontend/syson-components/src/extensions/expressions/useExpressionTextualRepresentation.ts b/frontend/syson-components/src/extensions/expressions/useExpressionTextualRepresentation.ts index 6c3884b82..47675f766 100644 --- a/frontend/syson-components/src/extensions/expressions/useExpressionTextualRepresentation.ts +++ b/frontend/syson-components/src/extensions/expressions/useExpressionTextualRepresentation.ts @@ -15,6 +15,7 @@ import { gql, useQuery } from '@apollo/client'; import { useMultiToast } from '@eclipse-sirius/sirius-components-core'; import { useEffect } from 'react'; import { + GQLExpressionEditorState, GQLGetExpressionTextualRepresentationData, GQLGetExpressionTextualRepresentationVariables, UseExpressionTextualRepresentationValue, @@ -24,7 +25,13 @@ const getExpressionTextualRepresentationQuery = gql` query getExpressionTextualRepresentation($editingContextId: ID!, $elementId: ID!) { viewer { editingContext(editingContextId: $editingContextId) { - expressionTextualRepresentation(elementId: $elementId) + expressionTextualRepresentation(elementId: $elementId) { + textualRepresentation + featureValueProperties { + isDefault + isInitial + } + } } } } @@ -50,7 +57,8 @@ export const useExpressionTextualRepresentation = ( } }, [error]); - const textualRepresentation: string | null = data?.viewer.editingContext.expressionTextualRepresentation ?? null; + const editorState: GQLExpressionEditorState | null = + data?.viewer.editingContext.expressionTextualRepresentation ?? null; - return { textualRepresentation, loading }; + return { editorState, loading }; }; diff --git a/frontend/syson-components/src/extensions/expressions/useExpressionTextualRepresentation.types.ts b/frontend/syson-components/src/extensions/expressions/useExpressionTextualRepresentation.types.ts index 988871091..d531f14f4 100644 --- a/frontend/syson-components/src/extensions/expressions/useExpressionTextualRepresentation.types.ts +++ b/frontend/syson-components/src/extensions/expressions/useExpressionTextualRepresentation.types.ts @@ -11,8 +11,10 @@ * Obeo - initial API and implementation *******************************************************************************/ +import { FeatureValueExpressionProperties } from './ExpressionProperties.types'; + export interface UseExpressionTextualRepresentationValue { - textualRepresentation: string | null; + editorState: GQLExpressionEditorState | null; loading: boolean; } @@ -30,5 +32,10 @@ export interface GQLGetExpressionTextualRepresentationViewer { } export interface GQLGetExpressionTextualRepresentationEditingContext { - expressionTextualRepresentation: string | null; + expressionTextualRepresentation: GQLExpressionEditorState | null; +} + +export interface GQLExpressionEditorState { + textualRepresentation: string; + featureValueProperties: FeatureValueExpressionProperties | null; }