Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
=== Improvements

- 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_.

=== New features

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*******************************************************************************
* 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.AssertionsForClassTypes.assertThat;
import static org.eclipse.sirius.components.diagrams.tests.DiagramEventPayloadConsumer.assertRefreshedDiagramThat;

import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.TypeRef;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;

import java.time.Duration;
import java.util.List;
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.DiagramRefreshedEventPayload;
import org.eclipse.sirius.components.collaborative.diagrams.dto.DropNodesInput;
import org.eclipse.sirius.components.diagrams.Diagram;
import org.eclipse.sirius.components.diagrams.events.ReconnectEdgeKind;
import org.eclipse.sirius.components.diagrams.layoutdata.Position;
import org.eclipse.sirius.components.graphql.tests.api.GraphQLResult;
import org.eclipse.sirius.components.representations.Message;
import org.eclipse.sirius.components.representations.MessageLevel;
import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState;
import org.eclipse.syson.AbstractIntegrationTests;
import org.eclipse.syson.GivenSysONServer;
import org.eclipse.syson.application.controllers.diagrams.checkers.CheckDiagramElementCount;
import org.eclipse.syson.application.controllers.diagrams.testers.DropNodesWithMessageMutationRunner;
import org.eclipse.syson.application.controllers.diagrams.testers.EdgeReconnectionTester;
import org.eclipse.syson.application.data.GVWithReadOnlyNodesProjectData;
import org.eclipse.syson.services.diagrams.DiagramComparator;
import org.eclipse.syson.services.diagrams.api.IGivenDiagramSubscription;
import org.junit.jupiter.api.BeforeEach;
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.transaction.annotation.Transactional;

import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;

/**
* Tests the feedback messages returned after forbidden move or edge reconnection resulting in a move operation.
*
* @author Arthur Daussy
*/
@Transactional
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class GVForbiddenMoveTests extends AbstractIntegrationTests {

@Autowired
private IGivenInitialServerState givenInitialServerState;

@Autowired
private IGivenDiagramSubscription givenDiagramSubscription;

@Autowired
private EdgeReconnectionTester edgeReconnectionTester;

@Autowired
private DropNodesWithMessageMutationRunner dropRunner;

@Autowired
private DiagramComparator diagramComparator;

@BeforeEach
public void setUp() {
this.givenInitialServerState.initialize();
}

@DisplayName("GIVEN composite edge, WHEN when reconnecting to a read only source, THEN no new edge is created and a feedback message is reported")
@GivenSysONServer({ GVWithReadOnlyNodesProjectData.SCRIPT_PATH })
@Test
public void reconnectCompositeEdgeSourceToReadOnlyObject() {
var flux = this.givenSubscriptionToDiagram(GVWithReadOnlyNodesProjectData.EDITING_CONTEXT_ID);

AtomicReference<Diagram> diagram = new AtomicReference<>();
Consumer<Object> initialDiagramContentConsumer = assertRefreshedDiagramThat(diagram::set);

Runnable reconnectEdgeRunnable = this.buildReconnectRunnable(
GVWithReadOnlyNodesProjectData.GraphicalIds.PART_ID,
ReconnectEdgeKind.SOURCE,
diagram,
List.of(new Message("Unable to move part2 in Part: Unable to move a Element to a read only Element", MessageLevel.WARNING)));

Consumer<Object> newEdgeConsumer = assertRefreshedDiagramThat(newDiagram -> {
new CheckDiagramElementCount(this.diagramComparator)
.hasNewEdgeCount(0)
.check(diagram.get(), newDiagram, true);
});

StepVerifier.create(flux)
.consumeNextWith(initialDiagramContentConsumer)
.then(reconnectEdgeRunnable)
.consumeNextWith(newEdgeConsumer)
.thenCancel()
.verify(Duration.ofSeconds(10));

}

@DisplayName("GIVEN a part and a read only package, WHEN when dropping the part on the read only package, THEN the drop should be prevented and a feedback message should be sent")
@GivenSysONServer({ GVWithReadOnlyNodesProjectData.SCRIPT_PATH })
@Test
public void moveElementToReadOnlyElement() {
var flux = this.givenSubscriptionToDiagram(GVWithReadOnlyNodesProjectData.EDITING_CONTEXT_ID);

AtomicReference<Diagram> diagram = new AtomicReference<>();
Consumer<Object> initialDiagramContentConsumer = assertRefreshedDiagramThat(diagram::set);

Runnable dropPartOnPartsRunnable = () -> {
GraphQLResult response = this.dropRunner.run(new DropNodesInput(UUID.randomUUID(),
GVWithReadOnlyNodesProjectData.EDITING_CONTEXT_ID,
GVWithReadOnlyNodesProjectData.GraphicalIds.DIAGRAM_ID,
List.of(GVWithReadOnlyNodesProjectData.GraphicalIds.PART1_ID),
GVWithReadOnlyNodesProjectData.GraphicalIds.PARTS_PKG_ID,
List.of(new Position(0, 0))));

assertThat(this.readDropNodesMessages(response.data()))
.isEqualTo(List.of(new Message("Unable to move part1 in Parts: Unable to move a Element to a read only Element", MessageLevel.WARNING)));
};
Consumer<Object> newEdgeConsumer = assertRefreshedDiagramThat(newDiagram -> {
new CheckDiagramElementCount(this.diagramComparator)
.hasNewEdgeCount(0)
.hasNewNodeCount(0)
.check(diagram.get(), newDiagram, true);
});

StepVerifier.create(flux)
.consumeNextWith(initialDiagramContentConsumer)
.then(dropPartOnPartsRunnable)
.consumeNextWith(newEdgeConsumer)
.thenCancel()
.verify(Duration.ofSeconds(10));

}

private List<Message> readDropNodesMessages(String responseData) {
Configuration conf = Configuration.builder()
.jsonProvider(new JacksonJsonProvider())
.mappingProvider(new JacksonMappingProvider())
.build();
DocumentContext ctx = JsonPath.using(conf).parse(responseData);
return ctx.read("$.data.dropNodes.messages", new TypeRef<List<Message>>() {
});
}

private Runnable buildReconnectRunnable(String newTarget, ReconnectEdgeKind reconnectionKind, AtomicReference<Diagram> diagram, List<Message> expectedMessages) {
return () ->
assertThat(
this.edgeReconnectionTester.reconnectEdge(GVWithReadOnlyNodesProjectData.EDITING_CONTEXT_ID,
diagram,
GVWithReadOnlyNodesProjectData.GraphicalIds.NESTED_USAGE_EDGE,
newTarget,
reconnectionKind)).isEqualTo(expectedMessages);
}

private Flux<DiagramRefreshedEventPayload> givenSubscriptionToDiagram(String editingContextId) {
var diagramEventInput = new DiagramEventInput(UUID.randomUUID(), editingContextId, GVWithReadOnlyNodesProjectData.GraphicalIds.DIAGRAM_ID);
return this.givenDiagramSubscription.subscribe(diagramEventInput);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*******************************************************************************
* 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.testers;

import java.util.Objects;

import org.eclipse.sirius.components.collaborative.diagrams.dto.DropNodesInput;
import org.eclipse.sirius.components.graphql.tests.api.GraphQLResult;
import org.eclipse.sirius.components.graphql.tests.api.IGraphQLRequestor;
import org.eclipse.sirius.components.graphql.tests.api.IMutationRunner;
import org.springframework.stereotype.Service;
/**
* Same as {@link org.eclipse.sirius.components.diagrams.tests.graphql.DropNodesMutationRunner} but with feedback messages.
*
* @author Arthur Daussy
*/
@Service
public class DropNodesWithMessageMutationRunner implements IMutationRunner<DropNodesInput> {
private static final String DROP_NODE_MUTATION = """
mutation dropNodes($input: DropNodesInput!) {
dropNodes(input: $input) {
__typename
... on SuccessPayload {
messages {
body
level
}
}
... on ErrorPayload {
message
messages {
body
level
}
}
}
}
""";

private final IGraphQLRequestor graphQLRequestor;

public DropNodesWithMessageMutationRunner(IGraphQLRequestor graphQLRequestor) {
this.graphQLRequestor = Objects.requireNonNull(graphQLRequestor);
}

@Override
public GraphQLResult run(DropNodesInput input) {
return this.graphQLRequestor.execute(DROP_NODE_MUTATION, input);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@

import static org.assertj.core.api.Assertions.assertThat;

import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.TypeRef;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;

import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;

Expand All @@ -24,6 +30,7 @@
import org.eclipse.sirius.components.diagrams.Diagram;
import org.eclipse.sirius.components.diagrams.events.ReconnectEdgeKind;
import org.eclipse.sirius.components.diagrams.tests.graphql.ReconnectEdgeMutationRunner;
import org.eclipse.sirius.components.representations.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

Expand All @@ -38,7 +45,7 @@ public class EdgeReconnectionTester {
@Autowired
private ReconnectEdgeMutationRunner reconnectEdgeMutationRunner;

public void reconnectEdge(String projectId, AtomicReference<Diagram> diagram, String edgeId, String newEdgeEnd, ReconnectEdgeKind reconnectEdgeKind) {
public List<Message> reconnectEdge(String projectId, AtomicReference<Diagram> diagram, String edgeId, String newEdgeEnd, ReconnectEdgeKind reconnectEdgeKind) {
var reconnectEdgeInput = new ReconnectEdgeInput(
UUID.randomUUID(),
projectId,
Expand All @@ -51,6 +58,16 @@ public void reconnectEdge(String projectId, AtomicReference<Diagram> diagram, St
var createEdgeResult = this.reconnectEdgeMutationRunner.run(reconnectEdgeInput);
var typename = JsonPath.read(createEdgeResult.data(), "$.data.reconnectEdge.__typename");
assertThat(typename).isEqualTo(SuccessPayload.class.getSimpleName());

// Return feedback messages
Configuration conf = Configuration.builder()
.jsonProvider(new JacksonJsonProvider())
.mappingProvider(new JacksonMappingProvider())
.build();
DocumentContext ctx = JsonPath.using(conf).parse(createEdgeResult.data());
return ctx.read("$.data.reconnectEdge.messages", new TypeRef<List<Message>>() {
});

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*******************************************************************************
* 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.data;

/**
* Project data for project "SysMLv2-WithReadOnlyNode".
*
* @author Arthur Daussy
*/
public class GVWithReadOnlyNodesProjectData {

public static final String SCRIPT_PATH = "/scripts/database-content/SysMLv2-WithReadOnlyNode.sql";

public static final String EDITING_CONTEXT_ID = "6e261505-a9ee-49df-8695-20c43a748d51";

/**
* Ids of graphical elements.
*/
public static final class GraphicalIds {

public static final String DIAGRAM_ID = "032613cb-2bbb-41be-857a-2687a3901b91";

public static final String PART_ID = "d4c076f3-8158-39e5-849d-39ab4e6e9e1a";

public static final String PART1_ID = "7b744096-f831-3aa8-8728-c2f61bc52f73";

public static final String PARTS_PAR_ID = "34c0a2e2-6bf4-3e76-8cf6-8bbaea855a3a";

public static final String PARTS_PKG_ID = "592ab045-03de-3f13-9812-5541588848c8";

public static final String NESTED_USAGE_EDGE = "5755ae43-957a-3513-b64a-cc2776759a7b";

}
}
Loading
Loading