Skip to content

Commit 1558137

Browse files
adaussyAxelRICHARD
authored andcommitted
[987] Implementation of DnD in SysOn Explorer view
Bug: #987 Signed-off-by: Arthur Daussy <arthur.daussy@obeo.fr>
1 parent db721e6 commit 1558137

File tree

19 files changed

+853
-110
lines changed

19 files changed

+853
-110
lines changed

CHANGELOG.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66

77
=== Breaking changes
88

9+
- https://github.com/eclipse-syson/syson/issues/987[#987] Implementation of drag and drop in SysOn Explorer view
10+
* A new service has been added to centralize the behavior of moving semantic elements in _org.eclipse.syson.services.api.ISysMLMoveElementService_.
11+
It replaces the public methods _UtilService.moveMembership_ and _ToolService.moveSemanticElement_
12+
* A new service has been added to centralize the verification of read-only elements in _org.eclipse.syson.services.api.ISysMLReadOnlyService_.
13+
14+
915
=== Dependency update
1016

1117
- Switch to Sirius Web 2025.1.5
@@ -45,6 +51,7 @@ The changes are:
4551

4652
- https://github.com/eclipse-syson/syson/issues/977[#977] [validation] SysON now implements the constraints (a.k.a. validation rules) from the SysMLv2 specification.
4753
The _Validation_ view show the results of the execution of the constraints on your models.
54+
- https://github.com/eclipse-syson/syson/issues/987[#987] Implementation of drag and drop in Explorer view.
4855

4956

5057
== v2025.1.0

backend/application/syson-application/src/test/java/org/eclipse/syson/application/controllers/projects/ProjectDataVersioningRestControllerIntegrationTests.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ public class ProjectDataVersioningRestControllerIntegrationTests extends Abstrac
4646

4747
private static final String INVALID_PROJECT = "55555555-5555-5555-5555-555555555555";
4848

49-
private static final String SIMPLE_PROJECT_PART = "a4f51a38-bfeb-4e0d-a870-55f8fe90405e";
50-
5149
@Autowired
5250
private IGivenInitialServerState givenInitialServerState;
5351

@@ -124,7 +122,7 @@ public void givenSysONRestAPIWhenWeAskForChangesOfASpecificElementThenThoseChang
124122
fail(e);
125123
}
126124

127-
var computedChangeId = UUID.nameUUIDFromBytes((SysMLv2Identifiers.SIMPLE_PROJECT + SIMPLE_PROJECT_PART).getBytes()).toString();
125+
var computedChangeId = UUID.nameUUIDFromBytes((SysMLv2Identifiers.SIMPLE_PROJECT + SysMLv2Identifiers.SIMPLE_PROJECT_PART).getBytes()).toString();
128126
var uri = String.format("/api/rest/projects/%s/commits/%s/changes/%s", SysMLv2Identifiers.SIMPLE_PROJECT, SysMLv2Identifiers.SIMPLE_PROJECT, computedChangeId);
129127
webTestClient.get()
130128
.uri(uri)
@@ -144,7 +142,7 @@ public void givenSysONRestAPIWhenWeAskForSpecificChangesInUnknownProjectThenItSh
144142
.baseUrl(this.getHTTPBaseUrl())
145143
.build();
146144

147-
var computedChangeId = UUID.nameUUIDFromBytes((INVALID_PROJECT + SIMPLE_PROJECT_PART).getBytes()).toString();
145+
var computedChangeId = UUID.nameUUIDFromBytes((INVALID_PROJECT + SysMLv2Identifiers.SIMPLE_PROJECT_PART).getBytes()).toString();
148146
var uri = String.format("/api/rest/projects/%s/commits/%s/changes/%s", INVALID_PROJECT, INVALID_PROJECT, computedChangeId);
149147
webTestClient.get()
150148
.uri(uri)

backend/application/syson-application/src/test/java/org/eclipse/syson/application/data/SysMLv2Identifiers.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ public final class SysMLv2Identifiers {
6060

6161
public static final String SIMPLE_PROJECT = "a427f187-9003-498c-9178-72e8350cc67c";
6262

63+
public static final String SIMPLE_PROJECT_DOCUMENT = "9a59f836-1df2-4e5d-803c-9eb0ba7031aa";
64+
65+
public static final String SIMPLE_PROJECT_PACKAGE1 = "127c38e7-0e15-4232-aa02-76b342e3324a";
66+
67+
public static final String SIMPLE_PROJECT_PART_DEF = "0a70220d-707e-4a88-84dc-6aa43aa97269";
68+
69+
public static final String SIMPLE_PROJECT_PART = "a4f51a38-bfeb-4e0d-a870-55f8fe90405e";
70+
71+
public static final String SIMPLE_PROJECT_PACKAGE2 = "ec12f223-8639-42a3-96c2-34163c6eccce";
72+
6373
private SysMLv2Identifiers() {
6474
// Prevent instantiation
6575
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Obeo.
3+
* This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Obeo - initial API and implementation
12+
*******************************************************************************/
13+
package org.eclipse.syson.services;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
17+
import java.util.List;
18+
import java.util.Optional;
19+
20+
import org.eclipse.emf.common.util.URI;
21+
import org.eclipse.emf.ecore.resource.Resource;
22+
import org.eclipse.sirius.components.core.api.IEditingContext;
23+
import org.eclipse.sirius.components.core.api.IEditingContextSearchService;
24+
import org.eclipse.sirius.components.emf.services.JSONResourceFactory;
25+
import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext;
26+
import org.eclipse.sirius.web.tests.services.api.IGivenCommittedTransaction;
27+
import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState;
28+
import org.eclipse.syson.AbstractIntegrationTests;
29+
import org.eclipse.syson.application.data.SysMLv2Identifiers;
30+
import org.eclipse.syson.services.api.ISysMLReadOnlyService;
31+
import org.eclipse.syson.sysml.Element;
32+
import org.eclipse.syson.sysml.helper.EMFUtils;
33+
import org.junit.jupiter.api.BeforeEach;
34+
import org.junit.jupiter.api.DisplayName;
35+
import org.junit.jupiter.api.Test;
36+
import org.springframework.beans.factory.annotation.Autowired;
37+
import org.springframework.boot.test.context.SpringBootTest;
38+
import org.springframework.test.context.jdbc.Sql;
39+
import org.springframework.test.context.jdbc.SqlConfig;
40+
import org.springframework.transaction.annotation.Transactional;
41+
42+
/**
43+
* Integration test for {@link ISysMLReadOnlyService}.
44+
*
45+
* @author Arthur Daussy
46+
*/
47+
@Transactional
48+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
49+
public class SysMLReadOnlyServiceTest extends AbstractIntegrationTests {
50+
51+
@Autowired
52+
private IGivenInitialServerState givenInitialServerState;
53+
54+
@Autowired
55+
private IGivenCommittedTransaction givenCommittedTransaction;
56+
57+
@Autowired
58+
private ISysMLReadOnlyService readOnlyService;
59+
60+
@Autowired
61+
private IEditingContextSearchService editingContextSearchService;
62+
63+
@BeforeEach
64+
public void beforeEach() {
65+
this.givenInitialServerState.initialize();
66+
}
67+
68+
@DisplayName("Given a simple SysML project, when we ask if an element is read only, then elements stored in standard libraries should be read-only whereas elements stored in other resources should be considered as editable")
69+
@Sql(scripts = { "/scripts/syson-test-database.sql" }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
70+
@Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED))
71+
@Test
72+
public void checkEditableElements() {
73+
74+
this.givenCommittedTransaction.commit();
75+
76+
Optional<IEditingContext> ed = this.editingContextSearchService.findById(SysMLv2Identifiers.SIMPLE_PROJECT);
77+
78+
assertThat(ed).isPresent();
79+
80+
URI mainResourceURI = new JSONResourceFactory().createResourceURI(SysMLv2Identifiers.SIMPLE_PROJECT_DOCUMENT);
81+
82+
for (Resource r : ((IEMFEditingContext) ed.get()).getDomain().getResourceSet().getResources()) {
83+
boolean expectedReadOnly = !mainResourceURI.equals(r.getURI());
84+
final String message = this.buildErrorMessage(expectedReadOnly);
85+
List<Element> elements = EMFUtils.allContainedObjectOfType(r, Element.class).toList();
86+
for (Element element : elements) {
87+
assertThat(this.readOnlyService.isReadOnly(element)).as(message, element.getElementId(), r.getURI()).isEqualTo(expectedReadOnly);
88+
}
89+
}
90+
}
91+
92+
private String buildErrorMessage(boolean expectedReadOnly) {
93+
final String message;
94+
if (expectedReadOnly) {
95+
message = "Expecting %s in %s to %s to be read only";
96+
} else {
97+
message = "Expecting %s in %s to %s to be editable";
98+
}
99+
return message;
100+
}
101+
102+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Obeo.
3+
* This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Obeo - initial API and implementation
12+
*******************************************************************************/
13+
package org.eclipse.syson.tree.explorer.view.handlers;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.junit.jupiter.api.Assertions.fail;
17+
18+
import com.jayway.jsonpath.JsonPath;
19+
20+
import java.time.Duration;
21+
import java.util.List;
22+
import java.util.Optional;
23+
import java.util.UUID;
24+
import java.util.function.Consumer;
25+
26+
import org.eclipse.sirius.components.collaborative.trees.dto.DropTreeItemInput;
27+
import org.eclipse.sirius.components.collaborative.trees.dto.TreeRefreshedEventPayload;
28+
import org.eclipse.sirius.components.core.api.ErrorPayload;
29+
import org.eclipse.sirius.components.core.api.SuccessPayload;
30+
import org.eclipse.sirius.components.trees.tests.graphql.DropTreeItemMutationRunner;
31+
import org.eclipse.sirius.web.application.views.explorer.ExplorerEventInput;
32+
import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState;
33+
import org.eclipse.sirius.web.tests.services.explorer.ExplorerEventSubscriptionRunner;
34+
import org.eclipse.sirius.web.tests.services.representation.RepresentationIdBuilder;
35+
import org.eclipse.syson.AbstractIntegrationTests;
36+
import org.eclipse.syson.application.data.SysMLv2Identifiers;
37+
import org.eclipse.syson.tree.explorer.view.SysONTreeViewDescriptionProvider;
38+
import org.eclipse.syson.tree.explorer.view.filters.SysONTreeFilterProvider;
39+
import org.junit.jupiter.api.BeforeEach;
40+
import org.junit.jupiter.api.DisplayName;
41+
import org.junit.jupiter.api.Test;
42+
import org.springframework.beans.factory.annotation.Autowired;
43+
import org.springframework.boot.test.context.SpringBootTest;
44+
import org.springframework.test.context.jdbc.Sql;
45+
import org.springframework.test.context.jdbc.SqlConfig;
46+
import org.springframework.test.context.transaction.TestTransaction;
47+
import org.springframework.transaction.annotation.Transactional;
48+
49+
import graphql.execution.DataFetcherResult;
50+
import reactor.test.StepVerifier;
51+
52+
/**
53+
* Integration tests for Explorer DnD.
54+
*
55+
* @author Arthur Daussy
56+
*/
57+
@Transactional
58+
@SuppressWarnings("checkstyle:MultipleStringLiterals")
59+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
60+
public class DropTreeItemHandlerTest extends AbstractIntegrationTests {
61+
62+
@Autowired
63+
private IGivenInitialServerState givenInitialServerState;
64+
65+
@Autowired
66+
private ExplorerEventSubscriptionRunner treeEventSubscriptionRunner;
67+
68+
@Autowired
69+
private DropTreeItemMutationRunner dropTreeItemMutationRunner;
70+
71+
@Autowired
72+
private RepresentationIdBuilder representationIdBuilder;
73+
74+
@Autowired
75+
private SysONTreeViewDescriptionProvider treeProvider;
76+
77+
@BeforeEach
78+
public void beforeEach() {
79+
this.givenInitialServerState.initialize();
80+
}
81+
82+
@DisplayName("Given the simple project, when drag an dropping a part definition on a package, then the part definition should be moved under the Package.")
83+
@Sql(scripts = { "/scripts/syson-test-database.sql" }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
84+
@Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED))
85+
@Test
86+
public void checkDnDPartDefinitionOnPackage() {
87+
88+
var expandedIds = List.of(
89+
SysMLv2Identifiers.SIMPLE_PROJECT_PACKAGE1.toString(),
90+
SysMLv2Identifiers.SIMPLE_PROJECT_PACKAGE2.toString(),
91+
SysMLv2Identifiers.SIMPLE_PROJECT.toString(),
92+
SysMLv2Identifiers.SIMPLE_PROJECT_DOCUMENT.toString());
93+
94+
var explorerRepresentationId = this.representationIdBuilder.buildExplorerRepresentationId(this.treeProvider.getDescriptionId(), expandedIds,
95+
List.of(SysONTreeFilterProvider.HIDE_MEMBERSHIPS_TREE_ITEM_FILTER_ID, SysONTreeFilterProvider.HIDE_ROOT_NAMESPACES_ID));
96+
var input = new ExplorerEventInput(UUID.randomUUID(), SysMLv2Identifiers.SIMPLE_PROJECT.toString(), explorerRepresentationId);
97+
var flux = this.treeEventSubscriptionRunner.run(input);
98+
99+
Consumer<Object> initialTreeContentConsumer = object -> Optional.of(object)
100+
.filter(DataFetcherResult.class::isInstance)
101+
.map(DataFetcherResult.class::cast)
102+
.map(DataFetcherResult::getData)
103+
.filter(TreeRefreshedEventPayload.class::isInstance)
104+
.map(TreeRefreshedEventPayload.class::cast)
105+
.map(TreeRefreshedEventPayload::tree)
106+
.ifPresentOrElse(tree -> {
107+
assertThat(tree).isNotNull();
108+
assertThat(tree.getChildren().get(0).getChildren().get(0).getChildren()).hasSize(2);
109+
assertThat(tree.getChildren().get(0).getChildren().get(0).getChildren()).anyMatch(treeItem -> treeItem.getId()
110+
.equals(SysMLv2Identifiers.SIMPLE_PROJECT_PART.toString()));
111+
assertThat(tree.getChildren().get(0).getChildren().get(1).getChildren()).hasSize(1);
112+
assertThat(tree.getChildren().get(0).getChildren().get(1).getChildren()).anyMatch(treeItem -> treeItem.getId()
113+
.equals(SysMLv2Identifiers.SIMPLE_PROJECT_PART_DEF.toString()));
114+
}, () -> fail("Missing tree"));
115+
116+
Runnable dropItemMutation = () -> {
117+
DropTreeItemInput dropTreeItemInput = new DropTreeItemInput(
118+
UUID.randomUUID(), SysMLv2Identifiers.SIMPLE_PROJECT.toString(),
119+
explorerRepresentationId,
120+
List.of(SysMLv2Identifiers.SIMPLE_PROJECT_PART_DEF.toString()),
121+
SysMLv2Identifiers.SIMPLE_PROJECT_PACKAGE1.toString(),
122+
-1);
123+
var result = this.dropTreeItemMutationRunner.run(dropTreeItemInput);
124+
String typename = JsonPath.read(result, "$.data.dropTreeItem.__typename");
125+
assertThat(typename).isEqualTo(SuccessPayload.class.getSimpleName());
126+
127+
TestTransaction.flagForCommit();
128+
TestTransaction.end();
129+
TestTransaction.start();
130+
};
131+
132+
Consumer<Object> updateTreeContentConsumer = object -> Optional.of(object)
133+
.filter(DataFetcherResult.class::isInstance)
134+
.map(DataFetcherResult.class::cast)
135+
.map(DataFetcherResult::getData)
136+
.filter(TreeRefreshedEventPayload.class::isInstance)
137+
.map(TreeRefreshedEventPayload.class::cast)
138+
.map(TreeRefreshedEventPayload::tree)
139+
.ifPresentOrElse(tree -> {
140+
assertThat(tree).isNotNull();
141+
assertThat(tree.getChildren().get(0).getChildren().get(0).getChildren()).hasSize(3);
142+
assertThat(tree.getChildren().get(0).getChildren().get(0).getChildren()).anyMatch(treeItem -> treeItem.getId()
143+
.equals(SysMLv2Identifiers.SIMPLE_PROJECT_PART_DEF.toString()));
144+
}, () -> fail("Missing tree"));
145+
146+
StepVerifier.create(flux)
147+
.consumeNextWith(initialTreeContentConsumer)
148+
.then(dropItemMutation)
149+
.consumeNextWith(updateTreeContentConsumer)
150+
.thenCancel()
151+
.verify(Duration.ofSeconds(10));
152+
153+
}
154+
155+
@DisplayName("Given a SySML project, when drag an dropping an element into one of its descendant, then the selected element should not be moved")
156+
@Sql(scripts = { "/scripts/syson-test-database.sql" }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
157+
@Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED))
158+
@Test
159+
public void checkForbiddenDropOnDescendant() {
160+
161+
var expandedIds = List.of(
162+
SysMLv2Identifiers.SIMPLE_PROJECT_PACKAGE1.toString(),
163+
SysMLv2Identifiers.SIMPLE_PROJECT_PACKAGE2.toString(),
164+
SysMLv2Identifiers.SIMPLE_PROJECT.toString(),
165+
SysMLv2Identifiers.SIMPLE_PROJECT_DOCUMENT.toString());
166+
167+
var explorerRepresentationId = this.representationIdBuilder.buildExplorerRepresentationId(this.treeProvider.getDescriptionId(), expandedIds,
168+
List.of(SysONTreeFilterProvider.HIDE_MEMBERSHIPS_TREE_ITEM_FILTER_ID, SysONTreeFilterProvider.HIDE_ROOT_NAMESPACES_ID));
169+
var input = new ExplorerEventInput(UUID.randomUUID(), SysMLv2Identifiers.SIMPLE_PROJECT.toString(), explorerRepresentationId);
170+
var flux = this.treeEventSubscriptionRunner.run(input);
171+
172+
Consumer<Object> initialTreeContentConsumer = object -> Optional.of(object)
173+
.filter(DataFetcherResult.class::isInstance)
174+
.map(DataFetcherResult.class::cast)
175+
.map(DataFetcherResult::getData)
176+
.filter(TreeRefreshedEventPayload.class::isInstance)
177+
.map(TreeRefreshedEventPayload.class::cast)
178+
.map(TreeRefreshedEventPayload::tree)
179+
.ifPresentOrElse(tree -> {
180+
assertThat(tree).isNotNull();
181+
assertThat(tree.getChildren().get(0).getChildren().get(0).getChildren()).hasSize(2);
182+
assertThat(tree.getChildren().get(0).getChildren().get(0).getChildren()).anyMatch(treeItem -> treeItem.getId()
183+
.equals(SysMLv2Identifiers.SIMPLE_PROJECT_PART.toString()));
184+
assertThat(tree.getChildren().get(0).getChildren().get(1).getChildren()).hasSize(1);
185+
assertThat(tree.getChildren().get(0).getChildren().get(1).getChildren()).anyMatch(treeItem -> treeItem.getId()
186+
.equals(SysMLv2Identifiers.SIMPLE_PROJECT_PART_DEF.toString()));
187+
}, () -> fail("Missing tree"));
188+
189+
Runnable dropItemMutation = () -> {
190+
DropTreeItemInput dropTreeItemInput = new DropTreeItemInput(
191+
UUID.randomUUID(), SysMLv2Identifiers.SIMPLE_PROJECT.toString(),
192+
explorerRepresentationId,
193+
List.of(SysMLv2Identifiers.SIMPLE_PROJECT_PACKAGE1.toString()),
194+
SysMLv2Identifiers.SIMPLE_PROJECT_PART.toString(),
195+
-1);
196+
var result = this.dropTreeItemMutationRunner.run(dropTreeItemInput);
197+
String typename = JsonPath.read(result, "$.data.dropTreeItem.__typename");
198+
assertThat(typename).isEqualTo(ErrorPayload.class.getSimpleName());
199+
200+
TestTransaction.flagForCommit();
201+
TestTransaction.end();
202+
TestTransaction.start();
203+
};
204+
205+
206+
StepVerifier.create(flux)
207+
.consumeNextWith(initialTreeContentConsumer)
208+
.then(dropItemMutation)
209+
.thenCancel()
210+
.verify(Duration.ofSeconds(10));
211+
212+
}
213+
214+
}

0 commit comments

Comments
 (0)