From 3fa9b6c3caa91d8a37ac3f6110e54c413fa6001d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20ROU=C3=8BN=C3=89?= Date: Thu, 19 Sep 2024 17:27:02 +0200 Subject: [PATCH] [3983] Add drag and drop support for explorer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: https://github.com/eclipse-sirius/sirius-web/issues/3983 Signed-off-by: Florian ROUËNÉ --- CHANGELOG.adoc | 34 +-- .../e2e/project/explorer/explorer-dnd.cy.ts | 49 ++++ .../cypress/workbench/Explorer.ts | 4 + .../services/ExplorerDescriptionProvider.java | 2 + .../services/ExplorerDropTreeItemHandler.java | 185 ++++++++++++++ ...TreeRepresentationDescriptionProvider.java | 2 +- .../domain/services/api/IMessageService.java | 21 ++ .../infrastructure/i18n/MessageService.java | 15 ++ .../main/resources/i18n/messages.properties | 5 +- ...ectDownloadControllerIntegrationTests.java | 9 +- .../ExplorerDropTreeItemControllerTests.java | 227 ++++++++++++++++++ ...TreeNonSemanticElementControllerTests.java | 2 +- .../services/MessageServiceTests.java | 3 + .../sirius/web/data/StudioIdentifiers.java | 4 + .../src/test/resources/scripts/studio.sql | 7 + .../trees/api/IDropTreeItemHandler.java | 31 +++ .../trees/dto/DropTreeItemInput.java | 35 +++ .../handlers/DropTreeItemEventHandler.java | 94 ++++++++ .../CollaborativeTreeMessageService.java | 5 + .../api/ICollaborativeTreeMessageService.java | 7 + ...-web-spring-collaborative-trees.properties | 1 + .../src/main/resources/schema/tree.graphqls | 15 +- .../MutationDropTreeItemDataFetcher.java | 57 +++++ .../graphql/DropTreeItemMutationRunner.java | 49 ++++ .../src/treeitems/TreeItem.tsx | 60 +++-- .../src/treeitems/TreeItem.types.ts | 5 +- .../src/treeitems/useDropTreeItem.ts | 86 +++++++ .../src/treeitems/useDropTreeItem.types.ts | 46 ++++ .../src/trees/Tree.tsx | 3 +- .../SelectionDialogDescriptionConverter.java | 5 +- 30 files changed, 1026 insertions(+), 42 deletions(-) create mode 100644 integration-tests/cypress/e2e/project/explorer/explorer-dnd.cy.ts create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerDropTreeItemHandler.java create mode 100644 packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerDropTreeItemControllerTests.java create mode 100644 packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/IDropTreeItemHandler.java create mode 100644 packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/dto/DropTreeItemInput.java create mode 100644 packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/handlers/DropTreeItemEventHandler.java create mode 100644 packages/trees/backend/sirius-components-trees-graphql/src/main/java/org/eclipse/sirius/components/trees/graphql/datafetchers/mutation/MutationDropTreeItemDataFetcher.java create mode 100644 packages/trees/backend/sirius-components-trees-tests/src/main/java/org/eclipse/sirius/components/trees/tests/graphql/DropTreeItemMutationRunner.java create mode 100644 packages/trees/frontend/sirius-components-trees/src/treeitems/useDropTreeItem.ts create mode 100644 packages/trees/frontend/sirius-components-trees/src/treeitems/useDropTreeItem.types.ts diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index a165fa80f1..60114332b5 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -42,7 +42,7 @@ The new option ALWAYS allows the separator to be displayed in every case. - https://github.com/eclipse-sirius/sirius-web/issues/3678[#3678] [core] Remove `IRepresentationMetadataSearchService#findByRepresentation`, use `IRepresentationMetadataSearchService#findByRepresentationId` instead. - https://github.com/eclipse-sirius/sirius-web/issues/4050[#4050] [sirius-web] `IUploadFileLoader` must now return an `IResult`. This allows an error to be displayed when there is a problem during uploading -- https://github.com/eclipse-sirius/sirius-web/issues/4037[#4037] [trees] The tree item id passed to `ITreeQueryService.findTreeItem` is no longer a UUID but is now a String. +- https://github.com/eclipse-sirius/sirius-web/issues/4037[#4037] [tree] The tree item id passed to `ITreeQueryService.findTreeItem` is no longer a UUID but is now a String. - https://github.com/eclipse-sirius/sirius-web/issues/4077[#4077] [charts] Remove `interface RepresentationMetadata` from `BarChart.types.ts`. - https://github.com/eclipse-sirius/sirius-web/issues/3840[#3840] [diagram] Migrate to ReactFlow 12, if you have components that uses the library then you'll need to update them. - https://github.com/eclipse-sirius/sirius-web/issues/2399[#2399] [core] When executing successive model operations inside a tool, the execution context (variables, in particular `self`) of all sibling operations now stays the same. @@ -51,6 +51,7 @@ This is much more natural and closer to the behavior of usual languages, but cha + Remove `org.eclipse.sirius.components.core.api.IRepresentationMetadataSearchService` and its implementation. + Use `org.eclipse.sirius.web.domain.boundedcontexts.representationdata.services.api.IRepresentationMetadataSearchService#findAllMetadataByTargetObjectId` instead of `org.eclipse.sirius.components.core.api.IRepresentationMetadataSearchService#findAllByTargetObjectId`. - https://github.com/eclipse-sirius/sirius-web/issues/3679[#3679] [sirius-web] The service `hasChildren` requires the editing context like the aql service `getChildren`. +- https://github.com/eclipse-sirius/sirius-web/issues/3983[#3983] [tree] The `TreeDescription` must now implement a `dropTreeItemHandler`. === Dependency update @@ -70,12 +71,12 @@ This is much more natural and closer to the behavior of usual languages, but cha - https://github.com/eclipse-sirius/sirius-web/issues/4005[#4005] [diagram] Fix a problem synchronizing explorer with current selection - https://github.com/eclipse-sirius/sirius-web/issues/4008[#4008] [diagram] Prevent `detailsEvent` to be triggered twice when selecting a multi-represented element in the diagram - https://github.com/eclipse-sirius/sirius-web/issues/4032[#4032] [diagram] Fix an issue where separator list compartment is missing -- https://github.com/eclipse-sirius/sirius-web/issues/4028[#4028] [trees] Fixed an issue where special characters (e.g. #) could not be used in the treeId due to URL parsing limitations. +- https://github.com/eclipse-sirius/sirius-web/issues/4028[#4028] [tree] Fixed an issue where special characters (e.g. #) could not be used in the treeId due to URL parsing limitations. - https://github.com/eclipse-sirius/sirius-web/issues/3958[#3958] [tree] Partial fix of a StackOverflow when expanding an item contained in rendering loop cycle - https://github.com/eclipse-sirius/sirius-web/issues/4050[#4050] [sirius-web] Display error message on failed upload document action - https://github.com/eclipse-sirius/sirius-web/issues/4051[#4051] Error when deleting Resource that use EReference.isContainer feature -- https://github.com/eclipse-sirius/sirius-web/issues/3958[#3958] [trees] Prevent expand all action to loop indefinitely. -- https://github.com/eclipse-sirius/sirius-web/issues/4073[#4073] [trees] Add missing variables `ancestorIds` and `index` to variable manager used in the default expand all handler. +- https://github.com/eclipse-sirius/sirius-web/issues/3958[#3958] [tree] Prevent expand all action to loop indefinitely. +- https://github.com/eclipse-sirius/sirius-web/issues/4073[#4073] [tree] Add missing variables `ancestorIds` and `index` to variable manager used in the default expand all handler. - https://github.com/eclipse-sirius/sirius-web/issues/4077[#4077] [charts] Remove metadata from Charts widgets. Charts' metadata is only relevant when Charts are used as Representations. - https://github.com/eclipse-sirius/sirius-web/issues/3904[#3904] [form] Fix an issue where textfield did not update to reflect backend-set value when it matched the initial value. @@ -97,6 +98,7 @@ description (optional). - https://github.com/eclipse-sirius/sirius-web/issues/3873[#3873] [diagram] Make the Selection Dialog available for the EdgeTool - https://github.com/eclipse-sirius/sirius-web/issues/3950[#3950] [diagram] Have the multiple selection on the Selection Dialog - https://github.com/eclipse-sirius/sirius-web/issues/3982[#3982] [core] Add support for the command palette. +- https://github.com/eclipse-sirius/sirius-web/issues/3983[#3983] [tree] Add drag and drop support for explorer === Improvements @@ -121,9 +123,9 @@ never, always and if_children (to display the separator only if children exist). + To activate the test profile add the value _test_ to the `SPRING_PROFILES_ACTIVE` environment variable. - https://github.com/eclipse-sirius/sirius-web/issues/3882[#3882] [sirius-web] Add a new tree event to handle tree description which are not explorer-related - https://github.com/eclipse-sirius/sirius-web/issues/3961[#3961] [sirius-web] Extend the explorer to support multiple presentations -- https://github.com/eclipse-sirius/sirius-web/issues/3856[#3856] [trees] Add tree representation in the view DSL -- https://github.com/eclipse-sirius/sirius-web/issues/4000[#4000] [trees] Add support for styled labels in view model for trees -- https://github.com/eclipse-sirius/sirius-web/issues/4037[#4037] [trees] Add mechanism to define actions in tree item context menu +- https://github.com/eclipse-sirius/sirius-web/issues/3856[#3856] [tree] Add tree representation in the view DSL +- https://github.com/eclipse-sirius/sirius-web/issues/4000[#4000] [tree] Add support for styled labels in view model for trees +- https://github.com/eclipse-sirius/sirius-web/issues/4037[#4037] [tree] Add mechanism to define actions in tree item context menu - https://github.com/eclipse-sirius/sirius-web/issues/4064[#4064] [sirius-web] Ensure that project template images fill the whole content part of the card. Project template images will now have a predictable size of 150px by 110px. This will allow specifier to create images that fir perfectly in the project template card. @@ -211,11 +213,11 @@ The ids used to create a subscription is also changed, refer to [ADR-159] for mo - https://github.com/eclipse-sirius/sirius-web/issues/3784[#3784] [sirius-web] Fix the click on a representation which is already selected in the explorer. - https://github.com/eclipse-sirius/sirius-web/issues/2759[#2759] [diagram] Reactivate the Selection Dialog - https://github.com/eclipse-sirius/sirius-web/issues/3766[#3766] [sirius-web] Fix a potential NPE in RewriteProxiesEventHandler#rewriteProxyURIs -- https://github.com/eclipse-sirius/sirius-web/issues/3808[#3808] [trees] Fix interactions with `TreeItemContextMenu` modal +- https://github.com/eclipse-sirius/sirius-web/issues/3808[#3808] [tree] Fix interactions with `TreeItemContextMenu` modal - https://github.com/eclipse-sirius/sirius-web/issues/3811[#3811] [forms] Fix issue with Reference widget not working when placed in a flexbox container -- https://github.com/eclipse-sirius/sirius-web/issues/3776[#3776] [trees] Restore expand all functionality for ref widget trees +- https://github.com/eclipse-sirius/sirius-web/issues/3776[#3776] [tree] Restore expand all functionality for ref widget trees - https://github.com/eclipse-sirius/sirius-web/issues/3824[#3824] [core] Fix representation migration participant execution which was failing when there were many representation migration participant to apply. -- https://github.com/eclipse-sirius/sirius-web/issues/3851[#3851] [trees] Fix an issue where the tree filters menu items were not displayed anymore after a click on the filters menu. +- https://github.com/eclipse-sirius/sirius-web/issues/3851[#3851] [tree] Fix an issue where the tree filters menu items were not displayed anymore after a click on the filters menu. - https://github.com/eclipse-sirius/sirius-web/issues/3849[#3849] [form] Fix the tree representation in form support the display of the same referenced element many times in the tree. - https://github.com/eclipse-sirius/sirius-web/issues/3878[#3878] [diagram] Unmount ReactFlowProvider after layout - https://github.com/eclipse-sirius/sirius-web/issues/3869[#3869] [form] Close form-based views when the underlying element no longer exists. @@ -261,7 +263,7 @@ A migration participant has been added to automatically keep compatible all diag - https://github.com/eclipse-sirius/sirius-web/issues/3782[#3782] [sirius-web] Add an extension point to contribute custom widgets - https://github.com/eclipse-sirius/sirius-web/issues/3801[#3801] Add an equivalent of IViewDiagramDescriptionSearchService for Form descriptions - https://github.com/eclipse-sirius/sirius-web/issues/3557[#3550] [sirius-web] Add an extension point to contribute setting pages -- https://github.com/eclipse-sirius/sirius-web/issues/3776[#3776] [trees] Remove unwanted dependency from the reference widget in the explorer +- https://github.com/eclipse-sirius/sirius-web/issues/3776[#3776] [tree] Remove unwanted dependency from the reference widget in the explorer - https://github.com/eclipse-sirius/sirius-web/issues/3777[#3777] [sirius-web] Add support for any kind of object as semantic element in the tree representation - https://github.com/eclipse-sirius/sirius-web/issues/3392[#3392] [diagram] Prevent edge from passing through another node - https://github.com/eclipse-sirius/sirius-web/issues/3763[#3763] [diagram] Split the SelectionDialogDescription to prepare the Tree support @@ -550,7 +552,7 @@ image:doc/screenshots/inside_outside_labels.png[Isinde outside label example, 70 - https://github.com/eclipse-sirius/sirius-web/issues/3354[#3354] [diagram] Add support for overlapping nodes in arrangeAll and distribute elements - https://github.com/eclipse-sirius/sirius-web/issues/3389[#3389] [form] Add support for DateTime widget - https://github.com/eclipse-sirius/sirius-web/issues/3307[#3307] [representation] Add support for the migration of representation -- https://github.com/eclipse-sirius/sirius-web/issues/3503[#3503] [trees] Add tree item renaming support to the new architecture +- https://github.com/eclipse-sirius/sirius-web/issues/3503[#3503] [tree] Add tree item renaming support to the new architecture === Improvements @@ -771,7 +773,7 @@ Form variables declared in a form description will thus be added in the FormDesc - https://github.com/eclipse-sirius/sirius-web/issues/3050[#3050] [sirius-web] Contributes a first version of the new Sirius Web architecture - https://github.com/eclipse-sirius/sirius-web/issues/1844[#1844] [emf] Unload resources by default when the EMF based editing context is disposed - https://github.com/eclipse-sirius/sirius-web/issues/3020[#3020] [sirius-web] Improve the creation of the Apollo GraphQL client. -- https://github.com/eclipse-sirius/sirius-web/issues/3056[#3056] [trees] Expand the clickable/draggable zone for tree items +- https://github.com/eclipse-sirius/sirius-web/issues/3056[#3056] [tree] Expand the clickable/draggable zone for tree items - [forms] Avoid creating a new subscription when re-selecting the same element - [tree] Avoid sending treePath query if the selection does not actually change - [core] Ignore empty changes of kind ChangeKind.NOTHING @@ -897,7 +899,7 @@ It allows to execute operations before/after the load of an `IEditingContext` in === Bug fixes -- https://github.com/eclipse-sirius/sirius-web/issues/2812[#2812] [trees] Fix an issue that prevents icon to be displayed in treeNodes. +- https://github.com/eclipse-sirius/sirius-web/issues/2812[#2812] [tree] Fix an issue that prevents icon to be displayed in treeNodes. - https://github.com/eclipse-sirius/sirius-web/issues/2580[#2580] [diagram] Cancel direct edit on 'Esc'. - https://github.com/eclipse-sirius/sirius-web/issues/2778[#2778] [diagram] Authorize empty string for direct edit. - https://github.com/eclipse-sirius/sirius-web/issues/2579[#2579] [diagram] Fix the invalid text cursor position when editing a label by typing text directly. @@ -1154,7 +1156,7 @@ That includes the support during the direct edit. The user can use 'shift+enter' to insert a line break. - https://github.com/eclipse-sirius/sirius-web/issues/2522[#2522] [diagram] Add the support for rotatable border node. - https://github.com/eclipse-sirius/sirius-web/issues/2255[#2255] [diagram] Add the possibility to specify a ratio on node to guarantee its appearance. -- https://github.com/eclipse-sirius/sirius-web/issues/2568[#2568] [trees] Allow developers to modify the content of the explorer view +- https://github.com/eclipse-sirius/sirius-web/issues/2568[#2568] [tree] Allow developers to modify the content of the explorer view - https://github.com/eclipse-sirius/sirius-web/issues/2570[#2570] [form] Allow developers to modify the content of the details view - https://github.com/eclipse-sirius/sirius-web/issues/2618[#2618] [core] Add an API to provide pre-process and post-process around input handling. @@ -1941,7 +1943,7 @@ By default, new nodes, edges and tools are explicitly configured to invoke the d - https://github.com/eclipse-sirius/sirius-components/issues/1231[#1231] [form] The components related to forms have been extracted to `@eclipse-sirius/sirius-components-forms`. The views and representations have adopted the suffix `View` and `Representation` respectively. As an example, the component `FormWebSocketContainer` is now named `FormRepresentation` - https://github.com/eclipse-sirius/sirius-components/issues/1231[#1231] [formdescriptioneditors] The components related to formdescriptioneditors have been extracted to `@eclipse-sirius/sirius-components-formdescriptioneditors`. The representation has been renamed `FormDescriptionEditorRepresentation`. - https://github.com/eclipse-sirius/sirius-components/issues/1231[#1231] [core] Since the core parts of Sirius Components cannot possibly known the components of the representations installed in a project, the context `RepresentationContext` will not provide a default value anymore. In a similar fashion, the `ServerContext` from `@eclipse-sirius/sirius-components-core` will now be required in order to let Sirius Components retrieve the URL of the backend. -- https://github.com/eclipse-sirius/sirius-components/issues/1231[#1231] [trees] The trees related components and the explorer have been extracted to `@eclipse-sirius/sirius-components-trees`. The `ExplorerWebSocketContainer` has also been renamed `ExplorerView` to match the naming convention of the other views. The components migrated have also been fully converted to XState and MaterialUI +- https://github.com/eclipse-sirius/sirius-components/issues/1231[#1231] [tree] The trees related components and the explorer have been extracted to `@eclipse-sirius/sirius-components-trees`. The `ExplorerWebSocketContainer` has also been renamed `ExplorerView` to match the naming convention of the other views. The components migrated have also been fully converted to XState and MaterialUI - https://github.com/eclipse-sirius/sirius-components/issues/1231[#1231] [workbench] The workbench was depending on some tree components in order to compute the list of `TreeItemContextMenuContribution` to use in the explorer. This dependency has been removed and as a result, users of the workbench component will have to rely on the `TreeItemContextMenuContext` to provide the contributions - https://github.com/eclipse-sirius/sirius-components/issues/1231[#1231] [workbench] Move the workbench related code to `@eclipse-sirius/sirius-components-core`. This move includes components such as `Workbench`, `RepresentationContext`, `WorkbenchViewContribution` for example - https://github.com/eclipse-sirius/sirius-components/issues/1231[#1231] [diagram] Move the code of the diagram and the selection wizard in the packages `@eclipse-sirius/sirius-components-diagrams` and `@eclipse-sirius/sirius-components-selection` diff --git a/integration-tests/cypress/e2e/project/explorer/explorer-dnd.cy.ts b/integration-tests/cypress/e2e/project/explorer/explorer-dnd.cy.ts new file mode 100644 index 0000000000..937f3c38cf --- /dev/null +++ b/integration-tests/cypress/e2e/project/explorer/explorer-dnd.cy.ts @@ -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'); + }); + }); + }); +}); diff --git a/integration-tests/cypress/workbench/Explorer.ts b/integration-tests/cypress/workbench/Explorer.ts index 0551745d02..1a7883aacd 100644 --- a/integration-tests/cypress/workbench/Explorer.ts +++ b/integration-tests/cypress/workbench/Explorer.ts @@ -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 diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerDescriptionProvider.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerDescriptionProvider.java index 4e3d803490..9cfedf6406 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerDescriptionProvider.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerDescriptionProvider.java @@ -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; @@ -336,4 +337,5 @@ private Object getParentObject(VariableManager variableManager) { } return result; } + } diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerDropTreeItemHandler.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerDropTreeItemHandler.java new file mode 100644 index 0000000000..f297881a97 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerDropTreeItemHandler.java @@ -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 objectsToMove = this.getObjectsToMove(editingContext, input.droppedElementIds()); + Optional 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 getObjectsToMove(IEditingContext editingContext, List 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 getTargetContainer(IEditingContext editingContext, String containerId, int index) { + Optional 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 objectsToMove, EObject targetContainer, Tree tree, int index) { + List targetSiblings = tree.getChildren() + .stream() + .flatMap(children -> this.getContainerChild(children, this.objectService.getId(targetContainer)).stream()) + .toList(); + return objectsToMove.stream() + .map(source -> { + Optional 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(); + 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 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 targetSiblings, int index) { + EList featureListEObject = (EList) 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 featureListEObject, List 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 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 getContainerChild(TreeItem treeItem, String containerId) { + if (treeItem.getId().equals(containerId)) { + return treeItem.getChildren(); + } + + List result = new ArrayList<>(); + if (!treeItem.getChildren().isEmpty()) { + for (TreeItem child : treeItem.getChildren()) { + result.addAll(this.getContainerChild(child, containerId)); + } + } + return result; + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreeRepresentationDescriptionProvider.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreeRepresentationDescriptionProvider.java index ca0b5004ed..e00a44f88d 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreeRepresentationDescriptionProvider.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/tree/DomainTreeRepresentationDescriptionProvider.java @@ -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); diff --git a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/services/api/IMessageService.java b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/services/api/IMessageService.java index 40571bbad3..eefa1bcc73 100644 --- a/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/services/api/IMessageService.java +++ b/packages/sirius-web/backend/sirius-web-domain/src/main/java/org/eclipse/sirius/web/domain/services/api/IMessageService.java @@ -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. * @@ -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 ""; + } } } diff --git a/packages/sirius-web/backend/sirius-web-infrastructure/src/main/java/org/eclipse/sirius/web/infrastructure/i18n/MessageService.java b/packages/sirius-web/backend/sirius-web-infrastructure/src/main/java/org/eclipse/sirius/web/infrastructure/i18n/MessageService.java index 6ad34eeb18..e269220dd1 100644 --- a/packages/sirius-web/backend/sirius-web-infrastructure/src/main/java/org/eclipse/sirius/web/infrastructure/i18n/MessageService.java +++ b/packages/sirius-web/backend/sirius-web-infrastructure/src/main/java/org/eclipse/sirius/web/infrastructure/i18n/MessageService.java @@ -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"); + } } diff --git a/packages/sirius-web/backend/sirius-web-infrastructure/src/main/resources/i18n/messages.properties b/packages/sirius-web/backend/sirius-web-infrastructure/src/main/resources/i18n/messages.properties index 3e52c8a1ba..d40359ab69 100644 --- a/packages/sirius-web/backend/sirius-web-infrastructure/src/main/resources/i18n/messages.properties +++ b/packages/sirius-web/backend/sirius-web-infrastructure/src/main/resources/i18n/messages.properties @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/projects/ProjectDownloadControllerIntegrationTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/projects/ProjectDownloadControllerIntegrationTests.java index b77627542d..8792348270 100644 --- a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/projects/ProjectDownloadControllerIntegrationTests.java +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/projects/ProjectDownloadControllerIntegrationTests.java @@ -244,7 +244,14 @@ private String getExpectedStudioDomainDocumentDataContent() { "data":{ "name":"label" } - } + }, + { + "id":"d51d676c-0cb7-414b-8358-bacbc5d33942", + "eClass":"domain:Attribute", + "data":{ + "name":"description" + } + } ], "relations":[ { diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerDropTreeItemControllerTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerDropTreeItemControllerTests.java new file mode 100644 index 0000000000..e0c9dfd2f4 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/ExplorerDropTreeItemControllerTests.java @@ -0,0 +1,227 @@ +/******************************************************************************* + * 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.controllers.trees; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.jayway.jsonpath.JsonPath; + +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Consumer; + +import org.eclipse.sirius.components.collaborative.trees.dto.DropTreeItemInput; +import org.eclipse.sirius.components.collaborative.trees.dto.TreeRefreshedEventPayload; +import org.eclipse.sirius.components.core.api.SuccessPayload; +import org.eclipse.sirius.components.trees.tests.graphql.DropTreeItemMutationRunner; +import org.eclipse.sirius.web.AbstractIntegrationTests; +import org.eclipse.sirius.web.application.views.explorer.ExplorerEventInput; +import org.eclipse.sirius.web.application.views.explorer.services.ExplorerDescriptionProvider; +import org.eclipse.sirius.web.data.StudioIdentifiers; +import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState; +import org.eclipse.sirius.web.tests.services.explorer.ExplorerEventSubscriptionRunner; +import org.eclipse.sirius.web.tests.services.representation.RepresentationIdBuilder; +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.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.transaction.TestTransaction; +import org.springframework.transaction.annotation.Transactional; + +import graphql.execution.DataFetcherResult; +import reactor.test.StepVerifier; + +/** + * Integration tests of the drop tree item mutation in the explorer. + * + * @author frouene + */ +@Transactional +@SuppressWarnings("checkstyle:MultipleStringLiterals") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class ExplorerDropTreeItemControllerTests extends AbstractIntegrationTests { + + @Autowired + private IGivenInitialServerState givenInitialServerState; + + @Autowired + private ExplorerEventSubscriptionRunner treeEventSubscriptionRunner; + + @Autowired + private DropTreeItemMutationRunner dropTreeItemMutationRunner; + + @Autowired + private RepresentationIdBuilder representationIdBuilder; + + @BeforeEach + public void beforeEach() { + this.givenInitialServerState.initialize(); + } + + @Test + @DisplayName("Given a studio, when we drag and drop an item in the explorer, then the object is moved") + @Sql(scripts = { "/scripts/studio.sql" }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + public void givenStudioWhenWeDragAndDropAnItemThenTheObjectIsMoved() { + var expandedIds = List.of( + StudioIdentifiers.DOMAIN_DOCUMENT.toString(), + StudioIdentifiers.DOMAIN_OBJECT.toString(), + StudioIdentifiers.ROOT_ENTITY_OBJECT.toString(), + StudioIdentifiers.NAMED_ELEMENT_ENTITY_OBJECT.toString() + ); + var explorerRepresentationId = this.representationIdBuilder.buildExplorerRepresentationId(ExplorerDescriptionProvider.DESCRIPTION_ID, expandedIds, List.of()); + var input = new ExplorerEventInput(UUID.randomUUID(), StudioIdentifiers.SAMPLE_STUDIO_PROJECT.toString(), explorerRepresentationId); + var flux = this.treeEventSubscriptionRunner.run(input); + + + Consumer initialTreeContentConsumer = object -> Optional.of(object) + .filter(DataFetcherResult.class::isInstance) + .map(DataFetcherResult.class::cast) + .map(DataFetcherResult::getData) + .filter(TreeRefreshedEventPayload.class::isInstance) + .map(TreeRefreshedEventPayload.class::cast) + .map(TreeRefreshedEventPayload::tree) + .ifPresentOrElse(tree -> { + assertThat(tree).isNotNull(); + assertThat(tree.getChildren().get(1).getChildren().get(0).getChildren().get(0).getChildren()).hasSize(3); + assertThat(tree.getChildren().get(1).getChildren().get(0).getChildren().get(1).getChildren()).hasSize(1); + assertThat(tree.getChildren().get(1).getChildren().get(0).getChildren().get(1).getChildren()).anyMatch(treeItem -> treeItem.getId() + .equals(StudioIdentifiers.NAME_ATTRIBUTE_OBJECT.toString())); + }, () -> fail("Missing tree")); + + Runnable dropItemMutation = () -> { + DropTreeItemInput dropTreeItemInput = new DropTreeItemInput( + UUID.randomUUID(), StudioIdentifiers.SAMPLE_STUDIO_PROJECT.toString(), + explorerRepresentationId, + List.of(StudioIdentifiers.NAME_ATTRIBUTE_OBJECT.toString()), + StudioIdentifiers.ROOT_ENTITY_OBJECT.toString(), + -1 + ); + var result = this.dropTreeItemMutationRunner.run(dropTreeItemInput); + String typename = JsonPath.read(result, "$.data.dropTreeItem.__typename"); + assertThat(typename).isEqualTo(SuccessPayload.class.getSimpleName()); + + TestTransaction.flagForCommit(); + TestTransaction.end(); + TestTransaction.start(); + }; + + + Consumer updateTreeContentConsumer = object -> { + Optional.of(object) + .filter(DataFetcherResult.class::isInstance) + .map(DataFetcherResult.class::cast) + .map(DataFetcherResult::getData) + .filter(TreeRefreshedEventPayload.class::isInstance) + .map(TreeRefreshedEventPayload.class::cast) + .map(TreeRefreshedEventPayload::tree) + .ifPresentOrElse(tree -> { + assertThat(tree).isNotNull(); + assertThat(tree.getChildren().get(1).getChildren().get(0).getChildren().get(0).getChildren()).hasSize(4); + assertThat(tree.getChildren().get(1).getChildren().get(0).getChildren().get(1).getChildren()).hasSize(0); + assertThat(tree.getChildren().get(1).getChildren().get(0).getChildren().get(0).getChildren()).anyMatch(treeItem -> treeItem.getId() + .equals(StudioIdentifiers.NAME_ATTRIBUTE_OBJECT.toString())); + }, () -> fail("Missing tree")); + }; + + + StepVerifier.create(flux) + .consumeNextWith(initialTreeContentConsumer) + .then(dropItemMutation) + .consumeNextWith(updateTreeContentConsumer) + .thenCancel() + .verify(Duration.ofSeconds(10)); + } + + @Test + @DisplayName("Given a studio, when we drag and drop an item to a specific position in the explorer, then the object is moved") + @Sql(scripts = { "/scripts/studio.sql" }, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = { "/scripts/cleanup.sql" }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + public void givenStudioWhenWeDragAndDropAnItemToSpecificPositionThenTheObjectIsMoved() { + var expandedIds = List.of( + StudioIdentifiers.DOMAIN_DOCUMENT.toString(), + StudioIdentifiers.DOMAIN_OBJECT.toString(), + StudioIdentifiers.ROOT_ENTITY_OBJECT.toString(), + StudioIdentifiers.NAMED_ELEMENT_ENTITY_OBJECT.toString() + ); + var explorerRepresentationId = this.representationIdBuilder.buildExplorerRepresentationId(ExplorerDescriptionProvider.DESCRIPTION_ID, expandedIds, List.of()); + var input = new ExplorerEventInput(UUID.randomUUID(), StudioIdentifiers.SAMPLE_STUDIO_PROJECT.toString(), explorerRepresentationId); + var flux = this.treeEventSubscriptionRunner.run(input); + + + Consumer initialTreeContentConsumer = object -> Optional.of(object) + .filter(DataFetcherResult.class::isInstance) + .map(DataFetcherResult.class::cast) + .map(DataFetcherResult::getData) + .filter(TreeRefreshedEventPayload.class::isInstance) + .map(TreeRefreshedEventPayload.class::cast) + .map(TreeRefreshedEventPayload::tree) + .ifPresentOrElse(tree -> { + assertThat(tree).isNotNull(); + assertThat(tree.getChildren().get(1).getChildren().get(0).getChildren().get(0).getChildren()).hasSize(3); + assertThat(tree.getChildren().get(1).getChildren().get(0).getChildren().get(1).getChildren()).hasSize(1); + assertThat(tree.getChildren().get(1).getChildren().get(0).getChildren().get(1).getChildren()).anyMatch(treeItem -> treeItem.getId() + .equals(StudioIdentifiers.NAME_ATTRIBUTE_OBJECT.toString())); + }, () -> fail("Missing tree")); + + Runnable dropItemMutation = () -> { + DropTreeItemInput dropTreeItemInput = new DropTreeItemInput( + UUID.randomUUID(), StudioIdentifiers.SAMPLE_STUDIO_PROJECT.toString(), + explorerRepresentationId, + List.of(StudioIdentifiers.NAME_ATTRIBUTE_OBJECT.toString()), + StudioIdentifiers.DESCRIPTION_ATTRIBUTE_OBJECT.toString(), + 1 + ); + var result = this.dropTreeItemMutationRunner.run(dropTreeItemInput); + String typename = JsonPath.read(result, "$.data.dropTreeItem.__typename"); + assertThat(typename).isEqualTo(SuccessPayload.class.getSimpleName()); + + TestTransaction.flagForCommit(); + TestTransaction.end(); + TestTransaction.start(); + }; + + + Consumer updateTreeContentConsumer = object -> { + Optional.of(object) + .filter(DataFetcherResult.class::isInstance) + .map(DataFetcherResult.class::cast) + .map(DataFetcherResult::getData) + .filter(TreeRefreshedEventPayload.class::isInstance) + .map(TreeRefreshedEventPayload.class::cast) + .map(TreeRefreshedEventPayload::tree) + .ifPresentOrElse(tree -> { + assertThat(tree).isNotNull(); + assertThat(tree.getChildren().get(1).getChildren().get(0).getChildren().get(0).getChildren()).hasSize(4); + assertThat(tree.getChildren().get(1).getChildren().get(0).getChildren().get(1).getChildren()).hasSize(0); + assertThat(tree.getChildren().get(1).getChildren().get(0).getChildren().get(0).getChildren()).anyMatch(treeItem -> treeItem.getId() + .equals(StudioIdentifiers.NAME_ATTRIBUTE_OBJECT.toString())); + }, () -> fail("Missing tree")); + }; + + + StepVerifier.create(flux) + .consumeNextWith(initialTreeContentConsumer) + .then(dropItemMutation) + .consumeNextWith(updateTreeContentConsumer) + .thenCancel() + .verify(Duration.ofSeconds(10)); + } + +} diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/TreeNonSemanticElementControllerTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/TreeNonSemanticElementControllerTests.java index 6205c7aabf..4973ae797e 100644 --- a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/TreeNonSemanticElementControllerTests.java +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/trees/TreeNonSemanticElementControllerTests.java @@ -151,7 +151,7 @@ public void givenADomainTreeRepresentationWhenWeSubscribeToItsEventThenTheRepres assertThat(tree).isNotNull(); assertThat(tree.getChildren()).hasSize(1); assertThat(tree.getChildren().get(0).getChildren()).hasSize(4); - assertThat(tree.getChildren().get(0).getChildren().get(1).getChildren()).hasSize(3); + assertThat(tree.getChildren().get(0).getChildren().get(1).getChildren()).hasSize(4); assertThat(tree.getChildren().get(0).getChildren().get(1).getChildren().get(0).getId()).startsWith(DomainTreeRepresentationDescriptionProvider.SETTING); }); diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/services/MessageServiceTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/services/MessageServiceTests.java index abf1ae3097..95233dbda5 100644 --- a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/services/MessageServiceTests.java +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/services/MessageServiceTests.java @@ -48,6 +48,9 @@ public void givenTheMessageServiceWhenAskedForValueThenTheEnglishValueIsReturned assertThat(this.messageService.showSelectedElements()).isNotBlank(); assertThat(this.messageService.unexpectedError()).isNotBlank(); assertThat(this.messageService.unpinSelectedElements()).isNotBlank(); + assertThat(this.messageService.unavailableFeature()).isNotBlank(); + assertThat(this.messageService.alreadySetFeature()).isNotBlank(); + assertThat(this.messageService.invalidDroppedObject()).isNotBlank(); } diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/data/StudioIdentifiers.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/data/StudioIdentifiers.java index 136845d0f1..95ca1ad6fc 100644 --- a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/data/StudioIdentifiers.java +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/data/StudioIdentifiers.java @@ -33,6 +33,10 @@ public final class StudioIdentifiers { public static final UUID NAMED_ELEMENT_ENTITY_OBJECT = UUID.fromString("c6fdba07-dea5-4a53-99c7-7eefc1bfdfcc"); + public static final UUID DESCRIPTION_ATTRIBUTE_OBJECT = UUID.fromString("d51d676c-0cb7-414b-8358-bacbc5d33942"); + + public static final UUID NAME_ATTRIBUTE_OBJECT = UUID.fromString("520bb7c9-5f28-40f7-bda0-b35dd593876d"); + public static final UUID HUMAN_ENTITY_OBJECT = UUID.fromString("1731ffb5-bfb0-46f3-a23d-0c0650300005"); public static final UUID VIEW_DOCUMENT = UUID.fromString("ed2a5355-991d-458f-87f1-ea3a18b1f104"); diff --git a/packages/sirius-web/backend/sirius-web/src/test/resources/scripts/studio.sql b/packages/sirius-web/backend/sirius-web/src/test/resources/scripts/studio.sql index 6dad144e68..9fff89547e 100644 --- a/packages/sirius-web/backend/sirius-web/src/test/resources/scripts/studio.sql +++ b/packages/sirius-web/backend/sirius-web/src/test/resources/scripts/studio.sql @@ -119,6 +119,13 @@ INSERT INTO document ( "data":{ "name":"label" } + }, + { + "id":"d51d676c-0cb7-414b-8358-bacbc5d33942", + "eClass":"domain:Attribute", + "data":{ + "name":"description" + } } ], "relations":[ diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/IDropTreeItemHandler.java b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/IDropTreeItemHandler.java new file mode 100644 index 0000000000..21f9708013 --- /dev/null +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/api/IDropTreeItemHandler.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * 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.components.collaborative.trees.api; + +import org.eclipse.sirius.components.collaborative.trees.dto.DropTreeItemInput; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.representations.IStatus; +import org.eclipse.sirius.components.trees.Tree; + +/** + * Interface for the drop tree item provider. + * + * @author frouene + */ +public interface IDropTreeItemHandler { + + boolean canHandle(Tree tree); + + IStatus handle(IEditingContext editingContext, Tree tree, DropTreeItemInput input); + +} diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/dto/DropTreeItemInput.java b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/dto/DropTreeItemInput.java new file mode 100644 index 0000000000..21dfb632e5 --- /dev/null +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/dto/DropTreeItemInput.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * 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.components.collaborative.trees.dto; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import org.eclipse.sirius.components.collaborative.trees.api.ITreeInput; + +/** + * The input for the "drop item" mutation. + * + * @author frouene + */ +public record DropTreeItemInput(UUID id, String editingContextId, String representationId, List droppedElementIds, String targetElementId, int index) implements ITreeInput { + + public DropTreeItemInput { + Objects.requireNonNull(id); + Objects.requireNonNull(editingContextId); + Objects.requireNonNull(representationId); + Objects.requireNonNull(droppedElementIds); + Objects.requireNonNull(targetElementId); + } +} diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/handlers/DropTreeItemEventHandler.java b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/handlers/DropTreeItemEventHandler.java new file mode 100644 index 0000000000..227657dc85 --- /dev/null +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/handlers/DropTreeItemEventHandler.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * 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.components.collaborative.trees.handlers; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.sirius.components.collaborative.api.ChangeDescription; +import org.eclipse.sirius.components.collaborative.api.ChangeKind; +import org.eclipse.sirius.components.collaborative.api.Monitoring; +import org.eclipse.sirius.components.collaborative.trees.api.IDropTreeItemHandler; +import org.eclipse.sirius.components.collaborative.trees.api.ITreeEventHandler; +import org.eclipse.sirius.components.collaborative.trees.api.ITreeInput; +import org.eclipse.sirius.components.collaborative.trees.dto.DropTreeItemInput; +import org.eclipse.sirius.components.collaborative.trees.services.api.ICollaborativeTreeMessageService; +import org.eclipse.sirius.components.core.api.ErrorPayload; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.core.api.SuccessPayload; +import org.eclipse.sirius.components.representations.Failure; +import org.eclipse.sirius.components.representations.Success; +import org.eclipse.sirius.components.trees.Tree; +import org.eclipse.sirius.components.trees.description.TreeDescription; +import org.springframework.stereotype.Service; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import reactor.core.publisher.Sinks.Many; +import reactor.core.publisher.Sinks.One; + +/** + * Used to drop tree items. + * + * @author frouene + */ +@Service +public class DropTreeItemEventHandler implements ITreeEventHandler { + + private final ICollaborativeTreeMessageService messageService; + + private final List dropTreeItemHandlers; + + private final Counter counter; + + public DropTreeItemEventHandler(ICollaborativeTreeMessageService messageService, List dropTreeItemHandlers, MeterRegistry meterRegistry) { + this.messageService = Objects.requireNonNull(messageService); + this.dropTreeItemHandlers = Objects.requireNonNull(dropTreeItemHandlers); + this.counter = Counter.builder(Monitoring.EVENT_HANDLER) + .tag(Monitoring.NAME, this.getClass().getSimpleName()) + .register(meterRegistry); + } + + @Override + public boolean canHandle(ITreeInput treeInput) { + return treeInput instanceof DropTreeItemInput; + } + + @Override + public void handle(One payloadSink, Many changeDescriptionSink, IEditingContext editingContext, TreeDescription treeDescription, Tree tree, ITreeInput treeInput) { + this.counter.increment(); + + String message = this.messageService.invalidInput(treeInput.getClass().getSimpleName(), DropTreeItemInput.class.getSimpleName()); + IPayload payload = new ErrorPayload(treeInput.id(), message); + ChangeDescription changeDescription = new ChangeDescription(ChangeKind.NOTHING, treeInput.representationId(), treeInput); + + if (treeInput instanceof DropTreeItemInput input) { + var optionalDropHandler = this.dropTreeItemHandlers.stream().filter(provider -> provider.canHandle(tree)).findFirst(); + + var status = optionalDropHandler + .map(provider -> provider.handle(editingContext, tree, input)) + .orElseGet(() -> new Failure(this.messageService.noDropHandler())); + if (status instanceof Success success) { + changeDescription = new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, treeInput.representationId(), treeInput, success.getParameters()); + payload = new SuccessPayload(treeInput.id(), success.getMessages()); + } else if (status instanceof Failure failure) { + payload = new ErrorPayload(treeInput.id(), failure.getMessages()); + } + } + + changeDescriptionSink.tryEmitNext(changeDescription); + payloadSink.tryEmitValue(payload); + } + +} diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/CollaborativeTreeMessageService.java b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/CollaborativeTreeMessageService.java index 6d008215b7..c4ea769de3 100644 --- a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/CollaborativeTreeMessageService.java +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/CollaborativeTreeMessageService.java @@ -42,4 +42,9 @@ public String invalidInput(String expectedInputTypeName, String receivedInputTyp public String noSingleClickTreeItemExecutor() { return this.messageSourceAccessor.getMessage("NO_SINGLE_CLICK_TREE_ITEM_EXECUTOR"); } + + @Override + public String noDropHandler() { + return this.messageSourceAccessor.getMessage("NO_DROP_HANDLER"); + } } diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/api/ICollaborativeTreeMessageService.java b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/api/ICollaborativeTreeMessageService.java index 3f98563d41..72328cc9e9 100644 --- a/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/api/ICollaborativeTreeMessageService.java +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/java/org/eclipse/sirius/components/collaborative/trees/services/api/ICollaborativeTreeMessageService.java @@ -23,6 +23,8 @@ public interface ICollaborativeTreeMessageService { String noSingleClickTreeItemExecutor(); + String noDropHandler(); + /** * Implementation which does nothing, used for mocks in unit tests. * @@ -40,5 +42,10 @@ public String noSingleClickTreeItemExecutor() { return ""; } + @Override + public String noDropHandler() { + return ""; + } + } } diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/resources/messages/sirius-web-spring-collaborative-trees.properties b/packages/trees/backend/sirius-components-collaborative-trees/src/main/resources/messages/sirius-web-spring-collaborative-trees.properties index 37e76430f2..e78e00cafb 100644 --- a/packages/trees/backend/sirius-components-collaborative-trees/src/main/resources/messages/sirius-web-spring-collaborative-trees.properties +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/resources/messages/sirius-web-spring-collaborative-trees.properties @@ -12,3 +12,4 @@ ################################################################################# INVALID_INPUT=Invalid input type, "{0}" has been received while "{1}" was expected NO_SINGLE_CLICK_TREE_ITEM_EXECUTOR=No existing action for this entry menu +NO_DROP_HANDLER=No drop handler provided diff --git a/packages/trees/backend/sirius-components-collaborative-trees/src/main/resources/schema/tree.graphqls b/packages/trees/backend/sirius-components-collaborative-trees/src/main/resources/schema/tree.graphqls index ced82ba83f..db3cfbe886 100644 --- a/packages/trees/backend/sirius-components-collaborative-trees/src/main/resources/schema/tree.graphqls +++ b/packages/trees/backend/sirius-components-collaborative-trees/src/main/resources/schema/tree.graphqls @@ -52,7 +52,7 @@ type StyledStringFragment { text: String! styledStringFragmentStyle : StyledStringFragmentStyle } - + type StyledStringFragmentStyle { isStruckOut: Boolean underlineStyle: UnderLineStyle @@ -117,7 +117,18 @@ extend type Mutation { deleteTreeItem(input: DeleteTreeItemInput!): DeleteTreeItemPayload renameTreeItem(input: RenameTreeItemInput!): RenameTreeItemPayload invokeSingleClickTreeItemContextMenuEntry(input: InvokeSingleClickTreeItemContextMenuEntryInput!): InvokeSingleClickTreeItemContextMenuEntryPayload! - + dropTreeItem(input: DropTreeItemInput!): DropTreeItemPayload +} + +union DropTreeItemPayload = ErrorPayload | SuccessPayload + +input DropTreeItemInput { + id: ID! + editingContextId: ID! + representationId: ID! + droppedElementIds: [ID!]! + targetElementId: ID! + index: Int! } input DeleteTreeItemInput { diff --git a/packages/trees/backend/sirius-components-trees-graphql/src/main/java/org/eclipse/sirius/components/trees/graphql/datafetchers/mutation/MutationDropTreeItemDataFetcher.java b/packages/trees/backend/sirius-components-trees-graphql/src/main/java/org/eclipse/sirius/components/trees/graphql/datafetchers/mutation/MutationDropTreeItemDataFetcher.java new file mode 100644 index 0000000000..00819ccaff --- /dev/null +++ b/packages/trees/backend/sirius-components-trees-graphql/src/main/java/org/eclipse/sirius/components/trees/graphql/datafetchers/mutation/MutationDropTreeItemDataFetcher.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * 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.components.trees.graphql.datafetchers.mutation; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.sirius.components.annotations.spring.graphql.MutationDataFetcher; +import org.eclipse.sirius.components.collaborative.trees.dto.DropTreeItemInput; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.graphql.api.IDataFetcherWithFieldCoordinates; +import org.eclipse.sirius.components.graphql.api.IEditingContextDispatcher; +import org.eclipse.sirius.components.graphql.api.IExceptionWrapper; + +import graphql.schema.DataFetchingEnvironment; + +/** + * The data fetcher used to drop tree items. + * + * @author frouene + */ +@MutationDataFetcher(type = "Mutation", field = "dropTreeItem") +public class MutationDropTreeItemDataFetcher implements IDataFetcherWithFieldCoordinates> { + + private static final String INPUT_ARGUMENT = "input"; + + private final ObjectMapper objectMapper; + + private final IExceptionWrapper exceptionWrapper; + + private final IEditingContextDispatcher editingContextDispatcher; + + public MutationDropTreeItemDataFetcher(ObjectMapper objectMapper, IExceptionWrapper exceptionWrapper, IEditingContextDispatcher editingContextDispatcher) { + this.objectMapper = Objects.requireNonNull(objectMapper); + this.exceptionWrapper = Objects.requireNonNull(exceptionWrapper); + this.editingContextDispatcher = Objects.requireNonNull(editingContextDispatcher); + } + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + Object argument = environment.getArgument(INPUT_ARGUMENT); + var input = this.objectMapper.convertValue(argument, DropTreeItemInput.class); + return this.exceptionWrapper.wrapMono(() -> this.editingContextDispatcher.dispatchMutation(input.editingContextId(), input), input).toFuture(); + } +} diff --git a/packages/trees/backend/sirius-components-trees-tests/src/main/java/org/eclipse/sirius/components/trees/tests/graphql/DropTreeItemMutationRunner.java b/packages/trees/backend/sirius-components-trees-tests/src/main/java/org/eclipse/sirius/components/trees/tests/graphql/DropTreeItemMutationRunner.java new file mode 100644 index 0000000000..a0412ce1c7 --- /dev/null +++ b/packages/trees/backend/sirius-components-trees-tests/src/main/java/org/eclipse/sirius/components/trees/tests/graphql/DropTreeItemMutationRunner.java @@ -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 + *******************************************************************************/ +package org.eclipse.sirius.components.trees.tests.graphql; + +import java.util.Objects; + +import org.eclipse.sirius.components.collaborative.trees.dto.DropTreeItemInput; +import org.eclipse.sirius.components.graphql.tests.api.IGraphQLRequestor; +import org.eclipse.sirius.components.graphql.tests.api.IMutationRunner; +import org.springframework.stereotype.Service; + +/** + * Used to drop a tree item with the GraphQL API. + * + * @author frouene + */ +@Service +public class DropTreeItemMutationRunner implements IMutationRunner { + + private static final String DROP_TREE_ITEM_MUTATION = """ + mutation deleteTreeItem($input: DropTreeItemInput!) { + dropTreeItem(input: $input) { + __typename + } + } + """; + + private final IGraphQLRequestor graphQLRequestor; + + public DropTreeItemMutationRunner(IGraphQLRequestor graphQLRequestor) { + this.graphQLRequestor = Objects.requireNonNull(graphQLRequestor); + } + + @Override + public String run(DropTreeItemInput input) { + return this.graphQLRequestor.execute(DROP_TREE_ITEM_MUTATION, input); + } + +} diff --git a/packages/trees/frontend/sirius-components-trees/src/treeitems/TreeItem.tsx b/packages/trees/frontend/sirius-components-trees/src/treeitems/TreeItem.tsx index e58300ce02..22aa85432c 100644 --- a/packages/trees/frontend/sirius-components-trees/src/treeitems/TreeItem.tsx +++ b/packages/trees/frontend/sirius-components-trees/src/treeitems/TreeItem.tsx @@ -22,17 +22,21 @@ import { import CropDinIcon from '@mui/icons-material/CropDin'; import React, { useEffect, useRef, useState } from 'react'; import { makeStyles } from 'tss-react/mui'; -import { TreeItemProps, TreeItemState } from './TreeItem.types'; +import { TreeItemProps, TreeItemState, PartHovered } from './TreeItem.types'; import { TreeItemAction } from './TreeItemAction'; import { TreeItemArrow } from './TreeItemArrow'; import { TreeItemDirectEditInput } from './TreeItemDirectEditInput'; import { isFilterCandidate } from './filterTreeItem'; +import { useDropTreeItem } from './useDropTreeItem'; const useTreeItemStyle = makeStyles()((theme) => ({ + treeItemBefore: { + height: '2px', + }, treeItem: { display: 'flex', flexDirection: 'row', - height: '24px', + height: '22px', gap: theme.spacing(0.5), alignItems: 'center', userSelect: 'none', @@ -106,6 +110,7 @@ export const TreeItem = ({ editingContextId, treeId, item, + itemIndex, depth, onExpand, onExpandAll, @@ -121,7 +126,7 @@ export const TreeItem = ({ const initialState: TreeItemState = { editingMode: false, editingKey: null, - isHovered: false, + partHovered: null, }; const [state, setState] = useState(initialState); @@ -130,17 +135,14 @@ export const TreeItem = ({ const refDom = useRef() as any; const { selection, setSelection } = useSelection(); + const { onDropTreeItem } = useDropTreeItem(editingContextId, treeId); - const handleMouseEnter = () => { - setState((prevState) => { - return { ...prevState, isHovered: true }; - }); + const handleMouseEnter = (partHovered: PartHovered) => { + setState((prevState) => ({ ...prevState, partHovered })); }; const handleMouseLeave = () => { - setState((prevState) => { - return { ...prevState, isHovered: false }; - }); + setState((prevState) => ({ ...prevState, partHovered: null })); }; const onTreeItemAction = () => { @@ -161,13 +163,14 @@ export const TreeItem = ({ if (item.expanded && item.children) { content = (
    - {item.children.map((childItem) => { + {item.children.map((childItem, index) => { return (
  • { @@ -296,7 +299,21 @@ export const TreeItem = ({ }; const dragOver: React.DragEventHandler = (event) => { - event.stopPropagation(); + event.preventDefault(); + }; + + const onDropItem: React.DragEventHandler = (event) => { + const dragSourcesStringified = event.dataTransfer.getData(DRAG_SOURCES_TYPE); + const selectedIds = JSON.parse(dragSourcesStringified).map((entry: SelectionEntry) => entry.id); + onDropTreeItem(selectedIds, item.id, -1); + event.preventDefault(); + }; + + const onDropBefore: React.DragEventHandler = (event) => { + const dragSourcesStringified = event.dataTransfer.getData(DRAG_SOURCES_TYPE); + const selectedIds = JSON.parse(dragSourcesStringified).map((entry: SelectionEntry) => entry.id); + onDropTreeItem(selectedIds, item.id, itemIndex); + event.preventDefault(); }; let tooltipText = ''; @@ -322,13 +339,24 @@ export const TreeItem = ({ /* ref, tabindex and onFocus are used to set the React component focusabled and to set the focus to the corresponding DOM part */ currentTreeItem = ( <> +
    handleMouseEnter('before')} + onDragExit={handleMouseLeave} + onDragOver={dragOver} + data-testid={`${dataTestid}-drop-before`} + />
    handleMouseEnter('item')} + onDragExit={handleMouseLeave} + onDrop={onDropItem} + onMouseEnter={() => handleMouseEnter('item')} onMouseLeave={handleMouseLeave}>
    )}
    diff --git a/packages/trees/frontend/sirius-components-trees/src/treeitems/TreeItem.types.ts b/packages/trees/frontend/sirius-components-trees/src/treeitems/TreeItem.types.ts index 40346ec187..16998f43ab 100644 --- a/packages/trees/frontend/sirius-components-trees/src/treeitems/TreeItem.types.ts +++ b/packages/trees/frontend/sirius-components-trees/src/treeitems/TreeItem.types.ts @@ -17,6 +17,7 @@ export interface TreeItemProps { editingContextId: string; treeId: string; item: GQLTreeItem; + itemIndex: number; depth: number; onExpand: (id: string, depth: number) => void; onExpandAll: (treeItem: GQLTreeItem) => void; @@ -31,5 +32,7 @@ export interface TreeItemProps { export interface TreeItemState { editingMode: boolean; editingKey: string | null; - isHovered: boolean; + partHovered: PartHovered | null; } + +export type PartHovered = 'before' | 'item'; diff --git a/packages/trees/frontend/sirius-components-trees/src/treeitems/useDropTreeItem.ts b/packages/trees/frontend/sirius-components-trees/src/treeitems/useDropTreeItem.ts new file mode 100644 index 0000000000..37bb907074 --- /dev/null +++ b/packages/trees/frontend/sirius-components-trees/src/treeitems/useDropTreeItem.ts @@ -0,0 +1,86 @@ +/******************************************************************************* + * 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 { gql, useMutation } from '@apollo/client'; +import { useMultiToast } from '@eclipse-sirius/sirius-components-core'; +import { useCallback, useEffect } from 'react'; +import { + GQLDropTreeItemData, + GQLDropTreeItemPayload, + GQLErrorPayload, + GQLSuccessPayload, + GQLDropTreeItemInput, + GQLDropTreeItemVariables, + UseDropTreeItemValue, +} from './useDropTreeItem.types'; + +const dropTreeItemMutation = gql` + mutation dropTreeItem($input: DropTreeItemInput!) { + dropTreeItem(input: $input) { + __typename + ... on ErrorPayload { + messages { + body + level + } + } + ... on SuccessPayload { + messages { + body + level + } + } + } + } +`; + +const isErrorPayload = (payload: GQLDropTreeItemPayload): payload is GQLErrorPayload => + payload.__typename === 'ErrorPayload'; +const isSuccessPayload = (payload: GQLDropTreeItemPayload): payload is GQLSuccessPayload => + payload.__typename === 'SuccessPayload'; + +export const useDropTreeItem = (editingContextId: string, treeId: string): UseDropTreeItemValue => { + const { addErrorMessage, addMessages } = useMultiToast(); + const [dropMutation, { data: dropTreeItemData, error: dropTreeItemError }] = useMutation< + GQLDropTreeItemData, + GQLDropTreeItemVariables + >(dropTreeItemMutation); + + useEffect(() => { + if (dropTreeItemError) { + addErrorMessage('An unexpected error has occurred, please refresh the page'); + } + if (dropTreeItemData) { + const { dropTreeItem } = dropTreeItemData; + if (isSuccessPayload(dropTreeItem)) { + addMessages(dropTreeItem.messages); + } + if (isErrorPayload(dropTreeItem)) { + addMessages(dropTreeItem.messages); + } + } + }, [dropTreeItemData, dropTreeItemError]); + + const onDropTreeItem = useCallback((droppedElementIds: string[], targetElementId: string, index: number): void => { + const input: GQLDropTreeItemInput = { + id: crypto.randomUUID(), + editingContextId, + representationId: treeId, + droppedElementIds, + targetElementId, + index, + }; + dropMutation({ variables: { input } }); + }, []); + + return { onDropTreeItem }; +}; diff --git a/packages/trees/frontend/sirius-components-trees/src/treeitems/useDropTreeItem.types.ts b/packages/trees/frontend/sirius-components-trees/src/treeitems/useDropTreeItem.types.ts new file mode 100644 index 0000000000..8f49da5976 --- /dev/null +++ b/packages/trees/frontend/sirius-components-trees/src/treeitems/useDropTreeItem.types.ts @@ -0,0 +1,46 @@ +/******************************************************************************* + * 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 { GQLMessage } from '@eclipse-sirius/sirius-components-core'; + +export interface UseDropTreeItemValue { + onDropTreeItem: (droppedElementIds: string[], targetElementId: string, index: number) => void; +} + +export interface GQLDropTreeItemPayload { + __typename: string; +} + +export interface GQLDropTreeItemData { + dropTreeItem: GQLDropTreeItemPayload; +} + +export interface GQLDropTreeItemVariables { + input: GQLDropTreeItemInput; +} + +export interface GQLDropTreeItemInput { + id: string; + editingContextId: string; + representationId: string; + droppedElementIds: string[]; + targetElementId: string; + index: number; +} + +export interface GQLErrorPayload extends GQLDropTreeItemPayload { + messages: GQLMessage[]; +} + +export interface GQLSuccessPayload extends GQLDropTreeItemPayload { + messages: GQLMessage[]; +} diff --git a/packages/trees/frontend/sirius-components-trees/src/trees/Tree.tsx b/packages/trees/frontend/sirius-components-trees/src/trees/Tree.tsx index 08e79e2091..cdce98dd75 100644 --- a/packages/trees/frontend/sirius-components-trees/src/trees/Tree.tsx +++ b/packages/trees/frontend/sirius-components-trees/src/trees/Tree.tsx @@ -109,12 +109,13 @@ export const Tree = ({ <>
      - {tree.children.map((item) => ( + {tree.children.map((item, index) => (
    • convert(DialogDescription dialogDescription, AQLInterpreter interpreter) { List representationDescriptions = new ArrayList<>(); @@ -128,9 +129,9 @@ private SelectionDescription convertSelectionDialog(org.eclipse.sirius.component * Create the TreeDescription attached to the SelectionDescription. * * @param selectionDescription - * the SelectionDialogDescription. + * the SelectionDialogDescription. * @param interpreter - * the AQL interpreter. + * the AQL interpreter. * @return The {@link TreeDescription} */ private TreeDescription createTreeDescription(SelectionDialogDescription selectionDescription, AQLInterpreter interpreter) {