From faf3a4140c3eb1dafa12c294bd4c967dfd7509b0 Mon Sep 17 00:00:00 2001 From: Michael Charfadi Date: Thu, 22 Jan 2026 16:40:41 +0100 Subject: [PATCH] [1870] Hide compartments of elements dropped from the explorer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: https://github.com/eclipse-syson/syson/issues/1870 Signed-off-by: Michaƫl Charfadi --- CHANGELOG.adoc | 2 +- .../GVDropFromExplorerVisibilityTests.java | 162 ++++++++++++++++++ .../DiagramMutationDropDefaultVisibility.java | 110 ++++++++++++ .../pages/release-notes/2026.3.0.adoc | 19 +- 4 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVDropFromExplorerVisibilityTests.java create mode 100644 backend/views/syson-standard-diagrams-view/src/main/java/org/eclipse/syson/standard/diagrams/view/services/DiagramMutationDropDefaultVisibility.java diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 0ad4c5a8b..28909051f 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -27,7 +27,7 @@ This allows to limit the size of the indices, and ensure information stored in t You can find more information on how to setup Elasticsearch, how elements are mapped to index documents, and how to query them in the documentation. - https://github.com/eclipse-syson/syson/issues/1861[#1861] [publication] Split `SysONLibraryPublicationHandler` in two distinct classes so the publishing logic can be extended or re-used through the `ISysMLLibraryPublisher` API. - https://github.com/eclipse-syson/syson/issues/1895[#1895] [export] Implement textual export of `StateUsage` and `StateDefinition`. - +- https://github.com/eclipse-syson/syson/issues/1870[#1870] [diagrams] When dragging some `Element` into a diagram, hide its graphical node compartments unless it's an _interconnection_ compartment === New features diff --git a/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVDropFromExplorerVisibilityTests.java b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVDropFromExplorerVisibilityTests.java new file mode 100644 index 000000000..a66a243c5 --- /dev/null +++ b/backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/diagrams/general/view/GVDropFromExplorerVisibilityTests.java @@ -0,0 +1,162 @@ +/******************************************************************************* + * 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.controllers.diagrams.general.view; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.sirius.components.diagrams.tests.DiagramEventPayloadConsumer.assertRefreshedDiagramThat; + +import com.jayway.jsonpath.JsonPath; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import org.eclipse.sirius.components.collaborative.diagrams.dto.DiagramEventInput; +import org.eclipse.sirius.components.collaborative.diagrams.dto.DropOnDiagramInput; +import org.eclipse.sirius.components.collaborative.diagrams.dto.DropOnDiagramSuccessPayload; +import org.eclipse.sirius.components.collaborative.diagrams.dto.InvokeSingleClickOnDiagramElementToolInput; +import org.eclipse.sirius.components.collaborative.diagrams.dto.InvokeSingleClickOnDiagramElementToolSuccessPayload; +import org.eclipse.sirius.components.diagrams.ViewModifier; +import org.eclipse.sirius.components.diagrams.tests.graphql.DropOnDiagramMutationRunner; +import org.eclipse.sirius.components.diagrams.tests.graphql.InvokeSingleClickOnDiagramElementToolMutationRunner; +import org.eclipse.sirius.components.diagrams.tests.graphql.PaletteQueryRunner; +import org.eclipse.sirius.components.diagrams.tests.navigation.DiagramNavigator; +import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState; +import org.eclipse.syson.AbstractIntegrationTests; +import org.eclipse.syson.SysONTestsProperties; +import org.eclipse.syson.application.data.GeneralViewItemAndAttributeProjectData; +import org.eclipse.syson.services.diagrams.api.IGivenDiagramSubscription; +import org.eclipse.syson.sysml.helper.LabelConstants; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.transaction.annotation.Transactional; + +import reactor.test.StepVerifier; + +/** + * Tests the visibility of dropped elements from the explorer on the General View diagram. + * + * @author mcharfadi + */ +@Transactional +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = { SysONTestsProperties.NO_DEFAULT_LIBRARIES_PROPERTY }) +public class GVDropFromExplorerVisibilityTests extends AbstractIntegrationTests { + + @Autowired + private IGivenInitialServerState givenInitialServerState; + + @Autowired + private IGivenDiagramSubscription givenDiagramSubscription; + + @Autowired + private DropOnDiagramMutationRunner dropOnDiagramMutationRunner; + + @Autowired + private PaletteQueryRunner paletteQueryRunner; + + @Autowired + private InvokeSingleClickOnDiagramElementToolMutationRunner invokeSingleClickOnDiagramElementToolMutationRunner; + + @DisplayName("GIVEN a diagram, WHEN we drop a PartUsage with no empty compartments from the Explorer view, THEN the PartUsage is displayed on the diagram with its compartments hidden") + @Sql(scripts = { GeneralViewItemAndAttributeProjectData.SCRIPT_PATH }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, + config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + @Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + @Test + public void dropPartFromTheExplorer() { + this.givenInitialServerState.initialize(); + var diagramEventInput = new DiagramEventInput(UUID.randomUUID(), + GeneralViewItemAndAttributeProjectData.EDITING_CONTEXT_ID, + GeneralViewItemAndAttributeProjectData.GraphicalIds.DIAGRAM_ID); + + var flux = this.givenDiagramSubscription.subscribe(diagramEventInput); + + var diagramId = new AtomicReference(); + var removeFromDiagramToolId = new AtomicReference(); + var diagramTargetId = new AtomicReference(); + var partNodeId = new AtomicReference(); + var partNodeSemanticId = new AtomicReference(); + + Consumer diagramContentConsumerBeforeDrop = assertRefreshedDiagramThat(diagram -> { + assertThat(diagram.getNodes()).hasSize(3); + diagramTargetId.set(diagram.getTargetObjectId()); + diagramId.set(diagram.getId()); + var partNode = new DiagramNavigator(diagram).nodeWithLabel(LabelConstants.OPEN_QUOTE + "part" + LabelConstants.CLOSE_QUOTE + LabelConstants.CR + "p1").getNode(); + assertThat(partNode.getChildNodes().stream().filter(node -> node.getModifiers().contains(ViewModifier.Hidden))).hasSize(9); + partNodeSemanticId.set(partNode.getTargetObjectId()); + partNodeId.set(partNode.getId()); + }); + + Runnable getRemoveFromDiagramTool = () -> { + Map variables = Map.of( + "editingContextId", GeneralViewItemAndAttributeProjectData.EDITING_CONTEXT_ID, + "representationId", diagramId.get(), + "diagramElementIds", List.of(partNodeId.get()) + ); + var result = this.paletteQueryRunner.run(variables); + + List labels = JsonPath.read(result.data(), "$.data.viewer.editingContext.representation.description.palette.quickAccessTools[*].label"); + assertThat(labels).hasSize(8); + assertThat(labels.get(6)).isEqualTo("Delete from Diagram"); + List ids = JsonPath.read(result.data(), "$.data.viewer.editingContext.representation.description.palette.quickAccessTools[*].id"); + removeFromDiagramToolId.set(ids.get(6)); + }; + + // Remove the node from the diagram + Runnable executeRemoveFromDiagramTool = () -> { + var input = new InvokeSingleClickOnDiagramElementToolInput(UUID.randomUUID(), GeneralViewItemAndAttributeProjectData.EDITING_CONTEXT_ID, diagramId.get(), List.of(partNodeId.get()), removeFromDiagramToolId.get(), 0, 0, List.of()); + var result = this.invokeSingleClickOnDiagramElementToolMutationRunner.run(input); + String typename = JsonPath.read(result.data(), "$.data.invokeSingleClickOnDiagramElementTool.__typename"); + assertThat(typename).isEqualTo(InvokeSingleClickOnDiagramElementToolSuccessPayload.class.getSimpleName()); + }; + + Consumer diagramContentConsumerAfterRemove = assertRefreshedDiagramThat(diagram -> { + assertThat(diagram.getNodes()).hasSize(2); + }); + + // Drop from the explorer + Runnable executeDropPartOnDiagram = () -> { + var dropOnDiagramInput = new DropOnDiagramInput(UUID.randomUUID(), GeneralViewItemAndAttributeProjectData.EDITING_CONTEXT_ID, diagramId.get(), + diagramTargetId.get(), List.of(partNodeSemanticId.get()), 0, 0); + var dropOnDiagramResult = this.dropOnDiagramMutationRunner.run(dropOnDiagramInput); + var typename = JsonPath.read(dropOnDiagramResult.data(), "$.data.dropOnDiagram.__typename"); + assertThat(typename).isEqualTo(DropOnDiagramSuccessPayload.class.getSimpleName()); + }; + + Consumer diagramContentConsumerAfterDrop = assertRefreshedDiagramThat(diagram -> { + assertThat(diagram.getNodes()).hasSize(3); + diagramTargetId.set(diagram.getTargetObjectId()); + diagramId.set(diagram.getId()); + var partNode = new DiagramNavigator(diagram).nodeWithLabel(LabelConstants.OPEN_QUOTE + "part" + LabelConstants.CLOSE_QUOTE + LabelConstants.CR + "p1").getNode(); + assertThat(partNode.getChildNodes().stream().filter(node -> node.getModifiers().contains(ViewModifier.Hidden))).hasSize(11); + partNodeSemanticId.set(partNode.getTargetObjectId()); + partNodeId.set(partNode.getId()); + }); + + StepVerifier.create(flux) + .consumeNextWith(diagramContentConsumerBeforeDrop) + .then(getRemoveFromDiagramTool) + .then(executeRemoveFromDiagramTool) + .consumeNextWith(diagramContentConsumerAfterRemove) + .then(executeDropPartOnDiagram) + .consumeNextWith(diagramContentConsumerAfterDrop) + .thenCancel() + .verify(Duration.ofSeconds(10)); + } +} diff --git a/backend/views/syson-standard-diagrams-view/src/main/java/org/eclipse/syson/standard/diagrams/view/services/DiagramMutationDropDefaultVisibility.java b/backend/views/syson-standard-diagrams-view/src/main/java/org/eclipse/syson/standard/diagrams/view/services/DiagramMutationDropDefaultVisibility.java new file mode 100644 index 000000000..8149e0334 --- /dev/null +++ b/backend/views/syson-standard-diagrams-view/src/main/java/org/eclipse/syson/standard/diagrams/view/services/DiagramMutationDropDefaultVisibility.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * 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.standard.diagrams.view.services; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.sirius.components.collaborative.api.ChangeDescription; +import org.eclipse.sirius.components.collaborative.diagrams.api.IDiagramEventConsumer; +import org.eclipse.sirius.components.collaborative.diagrams.dto.DropOnDiagramInput; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IObjectSearchService; +import org.eclipse.sirius.components.diagrams.Diagram; +import org.eclipse.sirius.components.diagrams.ViewCreationRequest; +import org.eclipse.sirius.components.diagrams.ViewDeletionRequest; +import org.eclipse.sirius.components.diagrams.components.NodeContainmentKind; +import org.eclipse.sirius.components.diagrams.components.NodeIdProvider; +import org.eclipse.sirius.components.diagrams.description.NodeDescription; +import org.eclipse.sirius.components.diagrams.events.HideDiagramElementEvent; +import org.eclipse.sirius.components.diagrams.events.IDiagramEvent; +import org.eclipse.sirius.components.view.emf.diagram.ViewDiagramConversionData; +import org.eclipse.sirius.web.application.editingcontext.EditingContext; +import org.eclipse.syson.diagram.services.DiagramQueryElementService; +import org.eclipse.syson.standard.diagrams.view.SDVDescriptionNameGenerator; +import org.eclipse.syson.sysml.Element; +import org.eclipse.syson.util.IDescriptionNameGenerator; +import org.eclipse.syson.util.SysONRepresentationDescriptionIdentifiers; +import org.springframework.stereotype.Service; + +/** + * Service that will be executed after a DropOnDiagramInput in order to hide compartments. + * If it's an interconnection compartment then we keep the default behavior of {@link org.eclipse.syson.diagram.common.view.services.ViewNodeService#isHiddenByDefault(Element, String)} + * + * @author mcharfadi + */ +@Service +public class DiagramMutationDropDefaultVisibility implements IDiagramEventConsumer { + + private final DiagramQueryElementService diagramQueryElementService; + + private final IObjectSearchService objectSearchService; + + public DiagramMutationDropDefaultVisibility(DiagramQueryElementService diagramQueryElementService, IObjectSearchService objectSearchService) { + this.diagramQueryElementService = diagramQueryElementService; + this.objectSearchService = objectSearchService; + } + + @Override + public void accept(IEditingContext editingContext, Diagram previousDiagram, List diagramEvents, List viewDeletionRequests, List viewCreationRequests, ChangeDescription changeDescription) { + if (changeDescription.getInput() instanceof DropOnDiagramInput input && this.isStandardDiagram(previousDiagram) && editingContext instanceof EditingContext siriusEditingContext + && siriusEditingContext.getViewConversionData().get(previousDiagram.getDescriptionId()) instanceof ViewDiagramConversionData viewDiagramConversionData) { + + IDescriptionNameGenerator descriptionNameGenerator = new SDVDescriptionNameGenerator(); + var interconnectionCompartmentName = descriptionNameGenerator.getFreeFormCompartmentName("interconnection"); + var interConnectionCompartment = viewDiagramConversionData.convertedNodes().entrySet().stream() + .filter(nodeDescriptionNodeDescriptionEntry -> nodeDescriptionNodeDescriptionEntry.getKey().getName().equals(interconnectionCompartmentName)) + .map(Map.Entry::getValue) + .map(NodeDescription::getId) + .findFirst(); + + Set compartmentsToHide = new HashSet<>(); + input.objectIds().forEach(objectId -> { + var semanticParent = this.objectSearchService.getObject(editingContext, objectId); + if (semanticParent.isPresent() && semanticParent.get() instanceof Element element) { + var optionalParentNodeDescriptionId = this.diagramQueryElementService.getNodeDescriptionId(element, previousDiagram, editingContext); + if (optionalParentNodeDescriptionId.isPresent()) { + var parentNodeId = new NodeIdProvider().getNodeId(previousDiagram.getId(), + optionalParentNodeDescriptionId.get(), + NodeContainmentKind.CHILD_NODE, + objectId); + + var parentNodeDescription = viewDiagramConversionData.convertedNodes().values().stream() + .filter(nodeDescription -> nodeDescription.getId().equals(optionalParentNodeDescriptionId.get())) + .findFirst(); + + parentNodeDescription.ifPresent(nodeDescription -> nodeDescription.getReusedChildNodeDescriptionIds().stream() + .filter(reusedChildNodeDescriptionId -> interConnectionCompartment.isEmpty() || !interConnectionCompartment.get().equals(reusedChildNodeDescriptionId)) + .forEach(reusedChildNodeDescriptionId -> { + var containerNodeToHideId = new NodeIdProvider().getNodeId(parentNodeId, + reusedChildNodeDescriptionId, + NodeContainmentKind.CHILD_NODE, + objectId); + compartmentsToHide.add(containerNodeToHideId); + })); + + diagramEvents.add(new HideDiagramElementEvent(compartmentsToHide, true)); + } + } + }); + } + + } + + private boolean isStandardDiagram(Diagram diagram) { + return diagram != null && Objects.equals(SysONRepresentationDescriptionIdentifiers.GENERAL_VIEW_DIAGRAM_DESCRIPTION_ID, diagram.getDescriptionId()); + } +} diff --git a/doc/content/modules/user-manual/pages/release-notes/2026.3.0.adoc b/doc/content/modules/user-manual/pages/release-notes/2026.3.0.adoc index b245e1da6..13505b13d 100644 --- a/doc/content/modules/user-manual/pages/release-notes/2026.3.0.adoc +++ b/doc/content/modules/user-manual/pages/release-notes/2026.3.0.adoc @@ -4,23 +4,32 @@ == New features -- SysON can now be connected with Elasticsearch to support cross-project search. +* SysON can now be connected with Elasticsearch to support cross-project search. The Elasticsearch integration uses its own indexing logic instead of the default one provided by Sirius Web. This allows to keep indices compact, and ensures information stored in the indices are useful to perform cross-project search. You can find more information on how to setup Elasticsearch, how elements are mapped to index documents, and how to query them in the documentation. - ++ This feature is currently considered experimental. Try it out and give feedback by reporting bugs and suggesting new features. It's not recommended for production use. == Bug fixes -* Fix the textual export of `OccurrenceUsage` to avoid duplication of the _abstract_ keyword. -* Fix the textual export to properly escape names used in qualified names in some references. +* In textual import/export: + +** Fix the textual export of `OccurrenceUsage` to avoid duplication of the _abstract_ keyword. +** Fix the textual export to properly escape names used in qualified names in some references. == Improvements -* Implement the textual export for `StateUsage` and `StateDefinition`. +* In diagrams: + +** When dropping `Element` from the _Explorer_ view to a diagram, hide its compartments unless it's the _interconnection compartment_ on an _Interconnection View_ diagram. + +* In textual import/export: + +** Implement the textual export for `StateUsage` and `StateDefinition`. + == Technical details