-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[3983] Add drag and drop support for explorer
Bug: #3983 Signed-off-by: Florian ROUËNÉ <florian.rouene@obeosoft.com>
- Loading branch information
1 parent
b4bfc23
commit 3fa9b6c
Showing
30 changed files
with
1,026 additions
and
42 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
49 changes: 49 additions & 0 deletions
49
integration-tests/cypress/e2e/project/explorer/explorer-dnd.cy.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
185 changes: 185 additions & 0 deletions
185
...g/eclipse/sirius/web/application/views/explorer/services/ExplorerDropTreeItemHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.