From ca7ae3d8bdfe53b583ee704210e367efb96f2cf4 Mon Sep 17 00:00:00 2001 From: Pierre-Charles David Date: Wed, 27 May 2026 14:05:09 +0200 Subject: [PATCH] [2119] Display expressions values in the Details view Bug: https://github.com/eclipse-syson/syson/issues/2119 Signed-off-by: Pierre-Charles David --- CHANGELOG.adoc | 4 +- .../SysMLv2PropertiesConfigurer.java | 25 +++ .../services/DetailsViewService.java | 46 ++++- .../MetamodelQueryElementService.java | 18 ++ .../SysONExtensionRegistryMergeStrategy.ts | 14 ++ .../expressions/ExpressionPropertySection.tsx | 187 ++++++++++++++++++ ...Registry.ts => SysONExtensionRegistry.tsx} | 26 +++ 7 files changed, 315 insertions(+), 5 deletions(-) create mode 100644 frontend/syson-components/src/extensions/expressions/ExpressionPropertySection.tsx rename frontend/syson-components/src/extensions/registry/{SysONExtensionRegistry.ts => SysONExtensionRegistry.tsx} (89%) 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 };