Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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");
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -570,12 +573,16 @@ public Element setNewDocumentationValue(Element self, String newValue) {
* a {@link FeatureValue} or {@link Feature}
* @return a {@link FeatureValue} or <code>null</code>
*/
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;
}
Expand Down Expand Up @@ -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 <code>null</code>
*/
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 <code>null</code>
*/
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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <code>null</code> if not found.
*/
public ResultExpressionMembership getResultExpressionMembership(Namespace namespace) {
return namespace.getOwnedMembership().stream()
.filter(ResultExpressionMembership.class::isInstance)
.map(ResultExpressionMembership.class::cast)
.findFirst()
.orElse(null);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}

Expand Down Expand Up @@ -69,4 +73,14 @@ export class SysONExtensionRegistryMergeStrategy
data: [...existingContributions.data, ...newContributions.data],
};
}

private mergeWidgetContributions(
existingContributions: DataExtension<any>,
newContributions: DataExtension<any>
): DataExtension<any> {
return {
identifier: `syson_${widgetContributionExtensionPoint.identifier}`,
data: [...existingContributions.data, ...newContributions.data],
};
}
}
Original file line number Diff line number Diff line change
@@ -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<TextfieldStyleProps>()(
(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<GQLTextfield | GQLTextarea> = ({
editingContextId,
formId,
widget,
}: PropertySectionComponentProps<GQLTextfield | GQLTextarea>) => {
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<ExpressionPropertySectionState>({
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 = (
<EditSysMLExpressionModal
editingContextId={editingContextId}
mode="edit"
elementId={targetObjectId}
onClose={onCloseModal}
/>
);
}

const labelOverride = 'Expression value';
const widgetForLabel = { ...widget, label: labelOverride };
return (
<div>
<div className={classes.propertySectionLabel}>
<PropertySectionLabel editingContextId={editingContextId} formId={formId} widget={widgetForLabel} />
</div>
<TextField
name={labelOverride}
placeholder={labelOverride}
variant="standard"
value={widget.stringValue}
spellCheck={false}
margin="dense"
multiline={true}
maxRows={4}
fullWidth
data-testid={labelOverride}
disabled={true}
error={widget.diagnostics.length > 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: (
<InputAdornment position="end">
<Tooltip title={'Edit'}>
<IconButton size="small" onClick={onEditExpression}>
<MoreHorizIcon />
</IconButton>
</Tooltip>
</InputAdornment>
),
},
}}
/>
{modalElement}
</div>
);
};
Loading
Loading