Skip to content

Commit

Permalink
[3983] Add drag and drop support for explorer
Browse files Browse the repository at this point in the history
Bug: #3983
Signed-off-by: Florian ROUËNÉ <florian.rouene@obeosoft.com>
  • Loading branch information
frouene authored and sbegaudeau committed Oct 26, 2024
1 parent b4bfc23 commit 3fa9b6c
Show file tree
Hide file tree
Showing 30 changed files with 1,026 additions and 42 deletions.
34 changes: 18 additions & 16 deletions CHANGELOG.adoc

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions integration-tests/cypress/e2e/project/explorer/explorer-dnd.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*******************************************************************************
* Copyright (c) 2024 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 { Project } from '../../../pages/Project';
import { Flow } from '../../../usecases/Flow';
import { Explorer } from '../../../workbench/Explorer';

const projectName = 'Cypress - explorer-dnd';

describe('Explorer', () => {
context('Given a flow project with a robot document', () => {
let projectId: string = '';
beforeEach(() =>
new Flow().createRobotProject(projectName).then((createdProjectData) => {
projectId = createdProjectData.projectId;
new Project().visit(projectId);
})
);

afterEach(() => cy.deleteProject(projectId));

context('When we drop tree item in the explorer', () => {
it('Then the object are moved', () => {
const explorer = new Explorer();
explorer.expand('robot');
explorer.expand('System');
explorer.expand('Central_Unit');
explorer.getTreeItemByLabel('Radar').should('not.exist');

explorer.expand('CompositeProcessor');
const dataTransfer = new DataTransfer();
explorer.dragTreeItem('Radar', dataTransfer);
explorer.dopOnTreeItem('Central_Unit', dataTransfer);
explorer.collapse('CompositeProcessor');

explorer.getTreeItemByLabel('Radar').should('exist');
});
});
});
});
4 changes: 4 additions & 0 deletions integration-tests/cypress/workbench/Explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ export class Explorer {
this.getTreeItemByLabel(treeItemLabel).trigger('dragstart', { dataTransfer });
}

public dopOnTreeItem(treeItemLabel: string, dataTransfer: DataTransfer): void {
this.getTreeItemByLabel(treeItemLabel).trigger('drop', { dataTransfer });
}

public createNewModel(modelName: string, modelType: string): void {
cy.getByTestId('new-model').should('exist');
cy.getByTestId('tree-filter-menu-icon').should('exist'); // trick to avoid error if this menu is not render yet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*******************************************************************************/
package org.eclipse.sirius.web.application.views.explorer.services;


import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
Expand Down Expand Up @@ -336,4 +337,5 @@ private Object getParentObject(VariableManager variableManager) {
}
return result;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*******************************************************************************
* Copyright (c) 2024 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.sirius.web.application.views.explorer.services;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.ENamedElement;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.sirius.components.collaborative.trees.api.IDropTreeItemHandler;
import org.eclipse.sirius.components.collaborative.trees.dto.DropTreeItemInput;
import org.eclipse.sirius.components.core.api.IEditingContext;
import org.eclipse.sirius.components.core.api.IObjectService;
import org.eclipse.sirius.components.representations.Failure;
import org.eclipse.sirius.components.representations.IStatus;
import org.eclipse.sirius.components.representations.Message;
import org.eclipse.sirius.components.representations.Success;
import org.eclipse.sirius.components.trees.Tree;
import org.eclipse.sirius.components.trees.TreeItem;
import org.eclipse.sirius.web.domain.services.api.IMessageService;
import org.springframework.stereotype.Service;

/**
* This class is used to provide the drop tree item of the explorer.
*
* @author frouene
*/
@Service
public class ExplorerDropTreeItemHandler implements IDropTreeItemHandler {

private final IObjectService objectService;

private final IMessageService messageService;

public ExplorerDropTreeItemHandler(IObjectService objectService, IMessageService messageService) {
this.objectService = Objects.requireNonNull(objectService);
this.messageService = Objects.requireNonNull(messageService);
}

@Override
public boolean canHandle(Tree tree) {
return tree.getId().startsWith(ExplorerDescriptionProvider.PREFIX);
}

@Override
public IStatus handle(IEditingContext editingContext, Tree tree, DropTreeItemInput input) {
List<EObject> objectsToMove = this.getObjectsToMove(editingContext, input.droppedElementIds());
Optional<EObject> targetContainer = this.getTargetContainer(editingContext, input.targetElementId(), input.index());

return targetContainer.map(eObject -> this.moveObjects(objectsToMove, eObject, tree, input.index())).orElse(new Failure(this.messageService.unavailableFeature()));
}

private List<EObject> getObjectsToMove(IEditingContext editingContext, List<String> objectToMoveIds) {
return objectToMoveIds.stream()
.map(objectToMoveId -> this.objectService.getObject(editingContext, objectToMoveId))
.filter(Optional::isPresent)
.map(Optional::get)
.filter(EObject.class::isInstance)
.map(EObject.class::cast)
.toList();
}

private Optional<EObject> getTargetContainer(IEditingContext editingContext, String containerId, int index) {
Optional<EObject> containerEObjectOpt = this.objectService.getObject(editingContext, containerId)
.filter(EObject.class::isInstance)
.map(EObject.class::cast);
if (index >= 0) {
return containerEObjectOpt.map(EObject::eContainer);
}
return containerEObjectOpt;
}

private IStatus moveObjects(List<EObject> objectsToMove, EObject targetContainer, Tree tree, int index) {
List<TreeItem> targetSiblings = tree.getChildren()
.stream()
.flatMap(children -> this.getContainerChild(children, this.objectService.getId(targetContainer)).stream())
.toList();
return objectsToMove.stream()
.map(source -> {
Optional<EStructuralFeature> optionalFeature = this.getContainmentFeatureName(source, targetContainer)
.map(featureName -> targetContainer.eClass().getEStructuralFeature(featureName));
return optionalFeature
.map(feature -> this.moveObjectToFeature(source, targetContainer, feature, targetSiblings, index))
.orElseGet(() -> new Failure(this.messageService.unavailableFeature()));
})
.reduce((accStatus, currentStatus) -> {
var messages = new ArrayList<Message>();
if (accStatus instanceof Success accSuccess) {
messages.addAll(accSuccess.getMessages());
}
if (currentStatus instanceof Failure currentFailure) {
messages.addAll(currentFailure.getMessages());
}
return new Success(messages);
})
.orElse(new Failure(this.messageService.invalidDroppedObject()));
}

private IStatus moveObjectToFeature(EObject source, EObject targetContainer, EStructuralFeature feature, List<TreeItem> targetSiblings, int index) {
if (feature.isMany()) {
return this.moveObjectToManyFeature(source, targetContainer, feature, targetSiblings, index);
}
return this.moveObjectToSingleFeature(source, targetContainer, feature);
}

private IStatus moveObjectToManyFeature(EObject source, EObject targetContainer, EStructuralFeature feature, List<TreeItem> targetSiblings, int index) {
EList<EObject> featureListEObject = (EList<EObject>) targetContainer.eGet(feature);
if (index >= 0) {
this.moveObjectToIndex(source, featureListEObject, targetSiblings, index);
} else {
EcoreUtil.remove(source);
featureListEObject.add(source);
}
return new Success();
}

private void moveObjectToIndex(EObject source, EList<EObject> featureListEObject, List<TreeItem> targetSiblings, int index) {
var firstListId = featureListEObject.stream().findFirst().map(this.objectService::getId).orElse(null);
var offset = targetSiblings.stream().map(TreeItem::getId).toList().indexOf(firstListId);
var featureIndex = index - offset;
var sourceId = this.objectService.getId(source);
var sourcePosBeforeMove = targetSiblings.stream().map(TreeItem::getId).toList().indexOf(sourceId);
if (sourcePosBeforeMove >= 0) {
if (sourcePosBeforeMove < index) {
featureListEObject.move(featureIndex - 1, source);
} else {
if (featureIndex < 0) {
featureIndex = 0;
}
featureListEObject.move(featureIndex, source);
}
} else {
EcoreUtil.remove(source);
featureListEObject.add(featureIndex, source);
}
}

private IStatus moveObjectToSingleFeature(EObject source, EObject targetContainer, EStructuralFeature feature) {
if (targetContainer.eGet(feature) == null) {
EcoreUtil.remove(source);
targetContainer.eSet(feature, source);
return new Success();
}
return new Failure(this.messageService.alreadySetFeature());
}

Optional<String> getContainmentFeatureName(EObject source, EObject target) {
EClass containedObjectEClass = source.eClass();
return target.eClass().getEAllContainments().stream()
.filter(eReference -> containedObjectEClass.equals(eReference.getEReferenceType()) || containedObjectEClass.getEAllSuperTypes().stream()
.anyMatch(superType -> superType.equals(eReference.getEReferenceType())))
.map(ENamedElement::getName)
.findFirst();
}

List<TreeItem> getContainerChild(TreeItem treeItem, String containerId) {
if (treeItem.getId().equals(containerId)) {
return treeItem.getChildren();
}

List<TreeItem> result = new ArrayList<>();
if (!treeItem.getChildren().isEmpty()) {
for (TreeItem child : treeItem.getChildren()) {
result.addAll(this.getContainerChild(child, containerId));
}
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ private StyledString getLabel(VariableManager variableManager) {
var kind = this.objectService.getKind(self);
label = this.urlParser.getParameterValues(kind).get(SemanticKindConstants.ENTITY_ARGUMENT).get(0);
}
} else if (self instanceof Setting setting) {
} else if (self instanceof Setting setting) {
label = setting.getEStructuralFeature().getName();
}
return StyledString.of(label);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ public interface IMessageService {

String unpinSelectedElements();

String unavailableFeature();

String alreadySetFeature();

String invalidDroppedObject();

/**
* Implementation which does nothing, used for mocks in unit tests.
*
Expand Down Expand Up @@ -102,5 +108,20 @@ public String unexpectedError() {
public String unpinSelectedElements() {
return "";
}

@Override
public String unavailableFeature() {
return "";
}

@Override
public String alreadySetFeature() {
return "";
}

@Override
public String invalidDroppedObject() {
return "";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,19 @@ public String unexpectedError() {
public String unpinSelectedElements() {
return this.messageSourceAccessor.getMessage("UNPIN_SELECTED_ELEMENTS");
}

@Override
public String unavailableFeature() {
return this.messageSourceAccessor.getMessage("UNAVAILABLE_FEATURE");
}

@Override
public String alreadySetFeature() {
return this.messageSourceAccessor.getMessage("ALREADY_SET_FEATURE");
}

@Override
public String invalidDroppedObject() {
return this.messageSourceAccessor.getMessage("INVALID_DROPPED_OBJECT");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ NOT_FOUND=Not found
PIN_SELECTED_ELEMENTS=Pin selected elements
SHOW_SELECTED_ELEMENTS=Show selected elements
UNEXPECTED_ERROR=An unexpected error has occurred, please contact the server administrator
UNPIN_SELECTED_ELEMENTS=Unpin selected elements
UNPIN_SELECTED_ELEMENTS=Unpin selected elements
UNAVAILABLE_FEATURE=No containment feature found for this element
ALREADY_SET_FEATURE=An element is already present for this mono-valued feature
INVALID_DROPPED_OBJECT=The dropped object is invalid
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,14 @@ private String getExpectedStudioDomainDocumentDataContent() {
"data":{
"name":"label"
}
}
},
{
"id":"d51d676c-0cb7-414b-8358-bacbc5d33942",
"eClass":"domain:Attribute",
"data":{
"name":"description"
}
}
],
"relations":[
{
Expand Down
Loading

0 comments on commit 3fa9b6c

Please sign in to comment.