diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index 3719fa8b3..05ac3ce14 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -26,8 +26,8 @@
- https://github.com/eclipse-syson/syson/issues/2198[#2198] [diagrams] Improve diagram-to-diagram drag and drop to support dropping multiple graphical nodes at once, leveraging Sirius Web's `droppedNodes` and `droppedElements` variables.
- https://github.com/eclipse-syson/syson/issues/2194[#2194] [diagrams] Properly report feedback messages to user when using _ISysMLMoveElementService_.
-- https://github.com/eclipse-syson/syson/issues/2182[#2182] [services] Provide a way for downstream applications to extend _ISysMLMoveElementService_;
-
+- https://github.com/eclipse-syson/syson/issues/2182[#2182] [services] Provide a way for downstream applications to extend _ISysMLMoveElementService_.
+- https://github.com/eclipse-syson/syson/issues/2119[#2119] [details] Display expressions values in the _Details_ view and allow to edit them from there.
=== New features
diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/configuration/SysMLv2PropertiesConfigurer.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/configuration/SysMLv2PropertiesConfigurer.java
index ddbf1b316..e10f7201d 100644
--- a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/configuration/SysMLv2PropertiesConfigurer.java
+++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/configuration/SysMLv2PropertiesConfigurer.java
@@ -79,6 +79,8 @@
@Configuration
public class SysMLv2PropertiesConfigurer implements IPropertiesDescriptionRegistryConfigurer {
+ private static final String CUSTOM_EXPRESSION_WIDGET_KEY = "syson:expression-value-widget";
+
private static final String CORE_PROPERTIES = "Core Properties";
private static final String ADVANCED_PROPERTIES = "Advanced Properties";
@@ -189,6 +191,7 @@ private FormDescription createDetailsViewForElement() {
pageCore.getGroups().add(this.createExtraAcceptActionUsagePropertiesGroup());
pageCore.getGroups().add(this.createExtraTransitionSourceTargetPropertiesGroup());
pageCore.getGroups().add(this.createFeatureValuePropertiesGroup());
+ pageCore.getGroups().add(this.createExpressionPropertiesGroup());
PageDescription pageAdvanced = FormFactory.eINSTANCE.createPageDescription();
pageAdvanced.setName("SysON-DetailsView-Advanced");
@@ -203,6 +206,28 @@ private FormDescription createDetailsViewForElement() {
return form;
}
+ /**
+ * Creates a group to display the value of an Expression.
+ *
+ * @return a {@link GroupDescription}
+ */
+ private GroupDescription createExpressionPropertiesGroup() {
+ GroupDescription group = FormFactory.eINSTANCE.createGroupDescription();
+ group.setDisplayMode(GroupDisplayMode.LIST);
+ group.setName("Expression Value");
+ group.setLabelExpression("");
+ group.setSemanticCandidatesExpression(ServiceMethod.of0(DetailsViewService::getExpression).aqlSelf());
+
+ TextAreaDescription expressionWidget = FormFactory.eINSTANCE.createTextAreaDescription();
+ expressionWidget.setName("Expression");
+ expressionWidget.setLabelExpression(CUSTOM_EXPRESSION_WIDGET_KEY);
+ expressionWidget.setValueExpression(ServiceMethod.of0(DetailsViewService::getExpressionTextualRepresentation).aqlSelf());
+ expressionWidget.setIsEnabledExpression(AQLConstants.AQL_FALSE);
+
+ group.getChildren().add(expressionWidget);
+
+ return group;
+ }
/**
* Creates a group to display the value of a Feature or FeatureValue.
diff --git a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/services/DetailsViewService.java b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/services/DetailsViewService.java
index def9195d8..a7c338ce1 100644
--- a/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/services/DetailsViewService.java
+++ b/backend/application/syson-application-configuration/src/main/java/org/eclipse/syson/application/services/DetailsViewService.java
@@ -44,6 +44,7 @@
import org.eclipse.syson.sysml.Annotation;
import org.eclipse.syson.sysml.Comment;
import org.eclipse.syson.sysml.ConjugatedPortDefinition;
+import org.eclipse.syson.sysml.Definition;
import org.eclipse.syson.sysml.Documentation;
import org.eclipse.syson.sysml.Element;
import org.eclipse.syson.sysml.EndFeatureMembership;
@@ -55,6 +56,7 @@
import org.eclipse.syson.sysml.FeatureValue;
import org.eclipse.syson.sysml.Import;
import org.eclipse.syson.sysml.Membership;
+import org.eclipse.syson.sysml.Namespace;
import org.eclipse.syson.sysml.ParameterMembership;
import org.eclipse.syson.sysml.ReferenceSubsetting;
import org.eclipse.syson.sysml.ReferenceUsage;
@@ -68,6 +70,7 @@
import org.eclipse.syson.sysml.SysmlPackage;
import org.eclipse.syson.sysml.TransitionUsage;
import org.eclipse.syson.sysml.Type;
+import org.eclipse.syson.sysml.Usage;
import org.eclipse.syson.sysml.ViewUsage;
import org.eclipse.syson.sysml.metamodel.services.ElementInitializerSwitch;
import org.eclipse.syson.sysml.metamodel.services.MetamodelQueryElementService;
@@ -570,12 +573,16 @@ public Element setNewDocumentationValue(Element self, String newValue) {
* a {@link FeatureValue} or {@link Feature}
* @return a {@link FeatureValue} or null
*/
- public Element getFeatureValue(Element self) {
- Element result = null;
+ public FeatureValue getFeatureValue(Element self) {
+ FeatureValue result = null;
if (self instanceof FeatureValue featureValue && featureValue.getValue() != null) {
result = featureValue;
} else if (self instanceof Feature feature) {
- result = this.metamodelQueryElementService.getValueExpression(feature).orElse(null);
+ result = feature.getOwnedRelationship().stream()
+ .filter(FeatureValue.class::isInstance)
+ .map(FeatureValue.class::cast)
+ .findFirst()
+ .orElse(null);
}
return result;
}
@@ -626,6 +633,39 @@ private String getExpressionAsText(Expression expression) {
return this.metamodelQueryElementService.getExpressionTextualRepresentation(expression);
}
+ /**
+ * Gets the {@link ResultExpressionMembership} from a {@link Namespace} or a {@link ResultExpressionMembership}.
+ *
+ * @param self
+ * a {@link Namespace} or a {@link ResultExpressionMembership}.
+ * @return a {@link ResultExpressionMembership} or null
+ */
+ public Element getResultExpression(Element self) {
+ Element result = null;
+ if (self instanceof ResultExpressionMembership expressionMembership && expressionMembership.getOwnedResultExpression() != null) {
+ result = expressionMembership;
+ } else if (self instanceof Namespace namespace && this.metamodelQueryElementService.getResultExpressionMembership(namespace) != null
+ && this.metamodelQueryElementService.getResultExpressionMembership(namespace).getOwnedResultExpression() != null) {
+ result = this.metamodelQueryElementService.getResultExpressionMembership(namespace);
+ }
+ return result;
+ }
+
+ /**
+ * Gets the {@link ResultExpressionMembership} from a {@link Namespace} or a {@link ResultExpressionMembership}.
+ *
+ * @param self
+ * a {@link Namespace} or a {@link ResultExpressionMembership}.
+ * @return a {@link ResultExpressionMembership} or null
+ */
+ public Element getExpression(Element self) {
+ if (self instanceof Expression && !(self instanceof Usage) && !(self instanceof Definition)) {
+ return self;
+ } else {
+ return null;
+ }
+ }
+
/**
* Returns the element that owns the visibility feature of the given element.
*
diff --git a/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/MetamodelQueryElementService.java b/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/MetamodelQueryElementService.java
index 54b3fb429..373bf1965 100644
--- a/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/MetamodelQueryElementService.java
+++ b/backend/services/syson-sysml-metamodel-services/src/main/java/org/eclipse/syson/sysml/metamodel/services/MetamodelQueryElementService.java
@@ -27,9 +27,11 @@
import org.eclipse.syson.sysml.Feature;
import org.eclipse.syson.sysml.FeatureValue;
import org.eclipse.syson.sysml.FramedConcernMembership;
+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.ResultExpressionMembership;
import org.eclipse.syson.sysml.StakeholderMembership;
import org.eclipse.syson.sysml.SubjectMembership;
import org.eclipse.syson.sysml.SysmlFactory;
@@ -306,4 +308,20 @@ public ConcernUsage getFramedConcernTarget(FramedConcernMembership framedConcern
}
return null;
}
+
+ /**
+ * Get the {@link ResultExpressionMembership} contained inside a given {@link Namespace}.
+ *
+ * @param namespace
+ * a given {@link Namespace}.
+ * @return a {@link ResultExpressionMembership}, or null if not found.
+ */
+ public ResultExpressionMembership getResultExpressionMembership(Namespace namespace) {
+ return namespace.getOwnedMembership().stream()
+ .filter(ResultExpressionMembership.class::isInstance)
+ .map(ResultExpressionMembership.class::cast)
+ .findFirst()
+ .orElse(null);
+ }
+
}
diff --git a/frontend/syson-components/src/extensions/SysONExtensionRegistryMergeStrategy.ts b/frontend/syson-components/src/extensions/SysONExtensionRegistryMergeStrategy.ts
index f2dfab167..1fd3d036a 100644
--- a/frontend/syson-components/src/extensions/SysONExtensionRegistryMergeStrategy.ts
+++ b/frontend/syson-components/src/extensions/SysONExtensionRegistryMergeStrategy.ts
@@ -12,6 +12,7 @@
*******************************************************************************/
import { DataExtension, ExtensionRegistryMergeStrategy } from '@eclipse-sirius/sirius-components-core';
+import { widgetContributionExtensionPoint } from '@eclipse-sirius/sirius-components-forms';
import { omniboxCommandOverrideContributionExtensionPoint } from '@eclipse-sirius/sirius-components-omnibox';
import { treeItemContextMenuEntryOverrideExtensionPoint } from '@eclipse-sirius/sirius-components-trees';
import {
@@ -37,6 +38,9 @@ export class SysONExtensionRegistryMergeStrategy
if (identifier === treeItemContextMenuEntryOverrideExtensionPoint.identifier) {
return this.mergeTreeItemContributions(existingValues, newValues);
}
+ if (identifier === widgetContributionExtensionPoint.identifier) {
+ return this.mergeWidgetContributions(existingValues, newValues);
+ }
return newValues;
}
@@ -69,4 +73,14 @@ export class SysONExtensionRegistryMergeStrategy
data: [...existingContributions.data, ...newContributions.data],
};
}
+
+ private mergeWidgetContributions(
+ existingContributions: DataExtension,
+ newContributions: DataExtension
+ ): DataExtension {
+ return {
+ identifier: `syson_${widgetContributionExtensionPoint.identifier}`,
+ data: [...existingContributions.data, ...newContributions.data],
+ };
+ }
}
diff --git a/frontend/syson-components/src/extensions/expressions/ExpressionPropertySection.tsx b/frontend/syson-components/src/extensions/expressions/ExpressionPropertySection.tsx
new file mode 100644
index 000000000..d45041e5f
--- /dev/null
+++ b/frontend/syson-components/src/extensions/expressions/ExpressionPropertySection.tsx
@@ -0,0 +1,187 @@
+/*******************************************************************************
+ * 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 { getCSSColor } from '@eclipse-sirius/sirius-components-core';
+import {
+ GQLTextarea,
+ GQLTextfield,
+ PropertySectionComponent,
+ PropertySectionComponentProps,
+ PropertySectionLabel,
+ TextfieldStyleProps,
+} from '@eclipse-sirius/sirius-components-forms';
+
+import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
+import IconButton from '@mui/material/IconButton';
+import InputAdornment from '@mui/material/InputAdornment';
+import TextField from '@mui/material/TextField';
+import Tooltip from '@mui/material/Tooltip';
+import { useState } from 'react';
+import { makeStyles } from 'tss-react/mui';
+import { EditSysMLExpressionModal } from './EditSysMLExpressionModal';
+
+const useStyle = makeStyles()(
+ (theme, { backgroundColor, foregroundColor, fontSize, italic, bold, gridLayout }) => {
+ const {
+ gridTemplateColumns,
+ gridTemplateRows,
+ labelGridColumn,
+ labelGridRow,
+ widgetGridColumn,
+ widgetGridRow,
+ gap,
+ } = {
+ ...gridLayout,
+ };
+ return {
+ style: {
+ backgroundColor: backgroundColor ? getCSSColor(backgroundColor, theme) : undefined,
+ color: foregroundColor ? getCSSColor(foregroundColor, theme) : undefined,
+ fontSize: fontSize ? fontSize : undefined,
+ fontStyle: italic ? 'italic' : undefined,
+ fontWeight: bold ? 'bold' : undefined,
+ },
+ input: {
+ paddingTop: theme.spacing(0.5),
+ paddingBottom: theme.spacing(0.5),
+ },
+ textfield: {
+ marginTop: theme.spacing(0.5),
+ marginBottom: theme.spacing(0.5),
+ },
+ formControl: {},
+ propertySection: {
+ display: 'grid',
+ gridTemplateColumns,
+ gridTemplateRows,
+ alignItems: 'center',
+ gap: gap ?? '',
+ },
+ propertySectionLabel: {
+ gridColumn: labelGridColumn,
+ gridRow: labelGridRow,
+ display: 'flex',
+ flexDirection: 'row',
+ gap: theme.spacing(2),
+ alignItems: 'center',
+ },
+ propertySectionWidget: {
+ gridColumn: widgetGridColumn,
+ gridRow: widgetGridRow,
+ },
+ };
+ }
+);
+
+// Extracts the UUID from the string of the form "details://?objectIds=[c5f78f3a-8b39-4cb0-903a-cedd8e6e71f6]" if it contains a single UUID, otherwise returns null.
+const extractObjectIdFromDetailsString = (detailsString: string): string | null => {
+ const regex = /objectIds=\[([0-9a-fA-F-]{36})\]/;
+ const match = detailsString.match(regex);
+ return match && match[1] ? match[1] : null;
+};
+
+type ExpressionPropertySectionState = {
+ state: 'idle' | 'modal';
+};
+
+export const ExpressionPropertySection: PropertySectionComponent = ({
+ editingContextId,
+ formId,
+ widget,
+}: PropertySectionComponentProps) => {
+ const props: TextfieldStyleProps = {
+ backgroundColor: widget.style?.backgroundColor ?? null,
+ foregroundColor: widget.style?.foregroundColor ?? null,
+ fontSize: widget.style?.fontSize ?? null,
+ italic: widget.style?.italic ?? null,
+ bold: widget.style?.bold ?? null,
+ underline: widget.style?.underline ?? null,
+ strikeThrough: widget.style?.strikeThrough ?? null,
+ gridLayout: widget.style?.widgetGridLayout ?? null,
+ };
+ const { classes } = useStyle(props);
+ const [state, setState] = useState({
+ state: 'idle',
+ });
+ const onCloseModal = () => {
+ setState((prevState) => ({ ...prevState, state: 'idle' }));
+ };
+ const onEditExpression = () => {
+ setState((prevState) => ({ ...prevState, state: 'modal' }));
+ };
+
+ const targetObjectId = extractObjectIdFromDetailsString(formId);
+
+ let modalElement: JSX.Element | null = null;
+ if (state.state === 'modal' && targetObjectId !== null) {
+ modalElement = (
+
+ );
+ }
+
+ const labelOverride = 'Expression value';
+ const widgetForLabel = { ...widget, label: labelOverride };
+ return (
+
+
+
0}
+ helperText={widget.diagnostics[0]?.message}
+ className={classes.textfield}
+ InputProps={
+ widget.style
+ ? {
+ className: classes.style,
+ }
+ : {}
+ }
+ inputProps={{
+ 'data-testid': `input-${labelOverride}`,
+ className: classes.input,
+ }}
+ slotProps={{
+ input: {
+ endAdornment: (
+
+
+
+
+
+
+
+ ),
+ },
+ }}
+ />
+ {modalElement}
+
+ );
+};
diff --git a/frontend/syson-components/src/extensions/registry/SysONExtensionRegistry.ts b/frontend/syson-components/src/extensions/registry/SysONExtensionRegistry.tsx
similarity index 89%
rename from frontend/syson-components/src/extensions/registry/SysONExtensionRegistry.ts
rename to frontend/syson-components/src/extensions/registry/SysONExtensionRegistry.tsx
index 9f6e48a70..4d8468138 100644
--- a/frontend/syson-components/src/extensions/registry/SysONExtensionRegistry.ts
+++ b/frontend/syson-components/src/extensions/registry/SysONExtensionRegistry.tsx
@@ -10,6 +10,7 @@
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
+
import { ExtensionRegistry } from '@eclipse-sirius/sirius-components-core';
import {
diagramToolbarActionExtensionPoint,
@@ -21,6 +22,11 @@ import {
paletteAppearanceSectionExtensionPoint,
RectangularNodeAppearanceSection,
} from '@eclipse-sirius/sirius-components-diagrams';
+import {
+ GQLWidget,
+ PropertySectionComponent,
+ widgetContributionExtensionPoint,
+} from '@eclipse-sirius/sirius-components-forms';
import {
OmniboxCommand,
OmniboxCommandOverrideContribution,
@@ -37,6 +43,7 @@ import {
ImportLibraryCommand,
navigationBarMenuIconExtensionPoint,
} from '@eclipse-sirius/sirius-web-application';
+import QuestionMarkOutlinedIcon from '@mui/icons-material/QuestionMarkOutlined';
import { Edge, Node, useStoreApi } from '@xyflow/react';
import { SysMLImportedPackageNodePaletteAppearanceSection } from '../../nodes/imported_package/SysMLImportedPackageNodePaletteAppearanceSection';
import { SysMLNoteNodePaletteAppearanceSection } from '../../nodes/note/SysMLNoteNodePaletteAppearanceSection';
@@ -45,6 +52,7 @@ import { sysMLNodesStyleDocumentTransform } from '../../nodes/SysMLNodesDocument
import { SysMLViewFrameNodePaletteAppearanceSection } from '../../nodes/view_frame/SysMLViewFrameNodePaletteAppearanceSection';
import { DeleteSysMLExpressionMenuContribution } from '../expressions/DeleteSysMLExpressionMenuContribution';
import { EditSysMLExpressionMenuContribution } from '../expressions/EditSysMLExpressionMenuContribution';
+import { ExpressionPropertySection } from '../expressions/ExpressionPropertySection';
import { NewSysMLExpressionMenuContribution } from '../expressions/NewSysMLExpressionMenuContribution';
import { InsertTextualSysMLMenuContribution } from '../InsertTextualSysMLv2MenuContribution';
import { SysONNavigationBarMenuIcon } from '../navigationBarMenu/SysONNavigationBarMenuIcon';
@@ -215,4 +223,22 @@ sysONExtensionRegistry.putData(pale
data: customNodePaletteAppearanceSectionContribution,
});
+sysONExtensionRegistry.putData(widgetContributionExtensionPoint, {
+ identifier: `syson_${widgetContributionExtensionPoint.identifier}`,
+ data: [
+ {
+ name: 'ExpressionValuePropertySectionOverride',
+ icon: ,
+ previewComponent: () => null,
+ component: (widget: GQLWidget): PropertySectionComponent | null => {
+ let propertySectionComponent: PropertySectionComponent | null = null;
+ if (widget.__typename == 'Textarea' && widget.label.startsWith('syson:expression-value-widget')) {
+ propertySectionComponent = ExpressionPropertySection as PropertySectionComponent;
+ }
+ return propertySectionComponent;
+ },
+ },
+ ],
+});
+
export { sysONExtensionRegistry };