diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 42831acfba..9c7e12937d 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -31,6 +31,7 @@ Both `IInput` and `IDomainEvent` implement `ICause` and will thus be used to ind - https://github.com/eclipse-sirius/sirius-web/issues/3972[#3972] [diagram] The `InsideLabelStyle#displayHeaderSeparator` has been renamed to `headerSeparatorDisplayMode` and is no longer a boolean but an enum with three possible values: NEVER, ALWAYS, and IF-CHILDREN. Previously, the value false was equivalent to NEVER, and true to IF-CHILDREN. The new option ALWAYS allows the separator to be displayed in every case. +- https://github.com/eclipse-sirius/sirius-web/issues/3983[#3983] [trees] The `TreeDescription` must now implement a `dropTreeItemHandler`. === Dependency update @@ -51,12 +52,14 @@ The new option ALWAYS allows the separator to be displayed in every case. - https://github.com/eclipse-sirius/sirius-web/issues/3763[#3763] [diagram] Make it possible to display semantic candidates in the selection dialog using a tree - https://github.com/eclipse-sirius/sirius-web/issues/3979[#3979] [core] Add Project related REST APIs. The new endpoints are: -** getProjects (`GET /api/rest/projects`): Get all projects. +** getProjects (`GET /api/rest/projects`): Get all projects. ** getProjectById (`GET /api/rest/projects/{projectId}`): Get project with the given id (projectId). ** createProject (`POST /projects`): Create a new project with the given name and description (optional). ** deleteProject (`POST /api/rest/projects/{projectId}`): Delete the project with the given id (projectId). ** updateProject (`PUT /projects/{projectId}`): Update the project with the given id (projectId). +- https://github.com/eclipse-sirius/sirius-web/issues/3983[#3983] [trees] Add drag and drop support for explorer + === Improvements diff --git a/packages/forms/backend/sirius-components-collaborative-widget-reference/src/main/java/org/eclipse/sirius/components/collaborative/widget/reference/browser/ModelBrowsersDescriptionProvider.java b/packages/forms/backend/sirius-components-collaborative-widget-reference/src/main/java/org/eclipse/sirius/components/collaborative/widget/reference/browser/ModelBrowsersDescriptionProvider.java index bb159a01bf..c432d351d5 100644 --- a/packages/forms/backend/sirius-components-collaborative-widget-reference/src/main/java/org/eclipse/sirius/components/collaborative/widget/reference/browser/ModelBrowsersDescriptionProvider.java +++ b/packages/forms/backend/sirius-components-collaborative-widget-reference/src/main/java/org/eclipse/sirius/components/collaborative/widget/reference/browser/ModelBrowsersDescriptionProvider.java @@ -50,6 +50,7 @@ import org.eclipse.sirius.components.representations.GetOrCreateRandomIdProvider; import org.eclipse.sirius.components.representations.IRepresentationDescription; import org.eclipse.sirius.components.representations.IStatus; +import org.eclipse.sirius.components.representations.Success; import org.eclipse.sirius.components.representations.VariableManager; import org.eclipse.sirius.components.trees.description.TreeDescription; import org.eclipse.sirius.components.trees.renderer.TreeRenderer; @@ -139,6 +140,7 @@ private TreeDescription getModelBrowserDescription(String descriptionId, Functio .renameHandler(this::getRenameHandler) .treeItemObjectProvider(this::getTreeItemObject) .parentObjectProvider(this::getParentObject) + .dropTreeItemHandler(variableManager -> new Success()) .build(); } @@ -278,8 +280,7 @@ private StyledString getLabel(VariableManager variableManager) { StyledString styledString = this.objectService.getStyledLabel(self); if (!styledString.toString().isBlank()) { return styledString; - } - else { + } else { var kind = this.objectService.getKind(self); label = this.urlParser.getParameterValues(kind).get(SemanticKindConstants.ENTITY_ARGUMENT).get(0); } diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/configuration/SiriusWebMessageServiceConfiguration.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/configuration/SiriusWebMessageServiceConfiguration.java new file mode 100644 index 0000000000..50e799c231 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/configuration/SiriusWebMessageServiceConfiguration.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * 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.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.context.support.ResourceBundleMessageSource; + +/** + * Configuration used to retrieve the message source accessor for the project. + * + * @author frouene + */ +@Configuration +public class SiriusWebMessageServiceConfiguration { + + private static final String PATH = "messages/sirius-web-application"; + + @Bean + public MessageSourceAccessor siriusWebApplicationMessageSourceAccessor() { + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + messageSource.addBasenames(PATH); + return new MessageSourceAccessor(messageSource); + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/messages/ISiriusWebMessageService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/messages/ISiriusWebMessageService.java new file mode 100644 index 0000000000..f9c89a6d97 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/messages/ISiriusWebMessageService.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * 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.messages; + +/** + * Interface of the sirius web message service. + * + * @author frouene + */ +public interface ISiriusWebMessageService { + + String unavailableFeature(); + + String alreadySetFeature(); + + /** + * Implementation which does nothing, used for mocks in unit tests. + * + * @author frouene + */ + class NoOp implements ISiriusWebMessageService { + + @Override + public String unavailableFeature() { + return ""; + } + + @Override + public String alreadySetFeature() { + return ""; + } + + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/messages/MessageConstants.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/messages/MessageConstants.java new file mode 100644 index 0000000000..a19265678c --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/messages/MessageConstants.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * 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.messages; + +/** + * This class is used to hold all the keys of the internationalization messages. + * + * @author frouene + */ +public final class MessageConstants { + + public static final String UNAVAILABLE_FEATURE = "UNAVAILABLE_FEATURE"; + + public static final String ALREADY_SET_FEATURE = "ALREADY_SET_FEATURE"; + + private MessageConstants() { + // Prevent instantiation + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/messages/SiriusWebMessageService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/messages/SiriusWebMessageService.java new file mode 100644 index 0000000000..beaa956e6c --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/messages/SiriusWebMessageService.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * 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.messages; + +import java.util.Objects; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.stereotype.Service; + +/** + * Implementation of the sirius web message service. + * + * @author frouene + */ +@Service +public class SiriusWebMessageService implements ISiriusWebMessageService { + + private final MessageSourceAccessor messageSourceAccessor; + + public SiriusWebMessageService(@Qualifier("siriusWebApplicationMessageSourceAccessor") MessageSourceAccessor messageSourceAccessor) { + this.messageSourceAccessor = Objects.requireNonNull(messageSourceAccessor); + } + + @Override + public String unavailableFeature() { + return this.messageSourceAccessor.getMessage(MessageConstants.UNAVAILABLE_FEATURE, new Object[] {}); + } + + @Override + public String alreadySetFeature() { + return this.messageSourceAccessor.getMessage(MessageConstants.ALREADY_SET_FEATURE, new Object[] {}); + } +} 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 68c8bec8eb..433a66ca33 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,9 +12,11 @@ *******************************************************************************/ package org.eclipse.sirius.web.application.views.explorer.services; + import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -24,6 +26,7 @@ import org.eclipse.emf.ecore.InternalEObject; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.sirius.components.collaborative.api.ChangeKind; import org.eclipse.sirius.components.collaborative.api.IRepresentationImageProvider; import org.eclipse.sirius.components.core.CoreImageConstants; import org.eclipse.sirius.components.core.RepresentationMetadata; @@ -39,6 +42,7 @@ import org.eclipse.sirius.components.representations.Failure; import org.eclipse.sirius.components.representations.IRepresentationDescription; import org.eclipse.sirius.components.representations.IStatus; +import org.eclipse.sirius.components.representations.Success; import org.eclipse.sirius.components.representations.VariableManager; import org.eclipse.sirius.components.trees.Tree; import org.eclipse.sirius.components.trees.TreeItem; @@ -48,6 +52,7 @@ import org.eclipse.sirius.web.application.views.explorer.services.api.IDeleteTreeItemHandler; import org.eclipse.sirius.web.application.views.explorer.services.api.IExplorerChildrenProvider; import org.eclipse.sirius.web.application.views.explorer.services.api.IExplorerElementsProvider; +import org.eclipse.sirius.web.application.views.explorer.services.api.IExplorerModificationService; import org.eclipse.sirius.web.application.views.explorer.services.api.IRenameTreeItemHandler; import org.eclipse.sirius.web.application.views.explorer.services.configuration.ExplorerDescriptionProviderConfiguration; import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.projections.RepresentationDataMetadataOnly; @@ -90,7 +95,9 @@ public class ExplorerDescriptionProvider implements IEditingContextRepresentatio private final IRepresentationDataSearchService representationDataSearchService; - public ExplorerDescriptionProvider(ExplorerDescriptionProviderConfiguration explorerDescriptionProviderConfiguration, List representationImageProviders, IExplorerElementsProvider explorerElementsProvider, IExplorerChildrenProvider explorerChildrenProvider, List renameTreeItemHandlers, List deleteTreeItemHandlers) { + private final IExplorerModificationService explorerModificationService; + + public ExplorerDescriptionProvider(ExplorerDescriptionProviderConfiguration explorerDescriptionProviderConfiguration, List representationImageProviders, IExplorerElementsProvider explorerElementsProvider, IExplorerChildrenProvider explorerChildrenProvider, List renameTreeItemHandlers, List deleteTreeItemHandlers, IExplorerModificationService explorerModificationService) { this.objectService = explorerDescriptionProviderConfiguration.getObjectService(); this.urlParser = explorerDescriptionProviderConfiguration.getUrlParser(); this.representationDataSearchService = explorerDescriptionProviderConfiguration.getRepresentationDataSearchService(); @@ -99,6 +106,7 @@ public ExplorerDescriptionProvider(ExplorerDescriptionProviderConfiguration expl this.explorerChildrenProvider = Objects.requireNonNull(explorerChildrenProvider); this.renameTreeItemHandlers = Objects.requireNonNull(renameTreeItemHandlers); this.deleteTreeItemHandlers = Objects.requireNonNull(deleteTreeItemHandlers); + this.explorerModificationService = Objects.requireNonNull(explorerModificationService); } @Override @@ -124,6 +132,7 @@ public List getRepresentationDescriptions(IEditingCo .deleteHandler(this::getDeleteHandler) .renameHandler(this::getRenameHandler) .treeItemObjectProvider(this::getTreeItemObject) + .dropTreeItemHandler(this::getDropTreeItemProvider) .build(); return List.of(explorerTreeDescription); } @@ -197,7 +206,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); @@ -360,4 +369,16 @@ private Object getParentObject(VariableManager variableManager) { } return result; } + + private IStatus getDropTreeItemProvider(VariableManager variableManager) { + var optionalEditingContext = variableManager.get(IEditingContext.EDITING_CONTEXT, IEditingContext.class); + var optionalTreeItemId = variableManager.get("targetTreeItem", String.class); + List droppedTreeItems = variableManager.get("droppedTreeItems", List.class).orElse(List.of()); + var optionalIndex = variableManager.get("index", Integer.class); + if (optionalTreeItemId.isPresent() && optionalEditingContext.isPresent()) { + this.explorerModificationService.moveObject(optionalEditingContext.get(), droppedTreeItems, optionalTreeItemId.get(), optionalIndex.orElse(-1)); + return new Success(ChangeKind.SEMANTIC_CHANGE, Map.of()); + } + return new Failure(""); + } } diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerModificationService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerModificationService.java new file mode 100644 index 0000000000..049cee3d59 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/ExplorerModificationService.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * 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.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.util.EcoreUtil; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IFeedbackMessageService; +import org.eclipse.sirius.components.core.api.IObjectService; +import org.eclipse.sirius.components.representations.Message; +import org.eclipse.sirius.components.representations.MessageLevel; +import org.eclipse.sirius.web.application.messages.ISiriusWebMessageService; +import org.eclipse.sirius.web.application.views.explorer.services.api.IExplorerModificationService; +import org.springframework.stereotype.Service; + +/** + * Service related to modification from Explorer. + * + * @author frouene + */ +@Service +public class ExplorerModificationService implements IExplorerModificationService { + + private final IObjectService objectService; + + private final ISiriusWebMessageService siriusWebMessageService; + + private final IFeedbackMessageService feedbackMessageService; + + public ExplorerModificationService(IObjectService objectService, ISiriusWebMessageService siriusWebMessageService, IFeedbackMessageService feedbackMessageService) { + this.objectService = Objects.requireNonNull(objectService); + this.siriusWebMessageService = Objects.requireNonNull(siriusWebMessageService); + this.feedbackMessageService = Objects.requireNonNull(feedbackMessageService); + } + + @Override + public void moveObject(IEditingContext editingContext, List objectToMoveIds, String containerId, int index) { + + List objectsToMove = objectToMoveIds.stream() + .map(objectToMoveId -> this.objectService.getObject(editingContext, objectToMoveId).filter(EObject.class::isInstance).map(EObject.class::cast)) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + Optional containerEObjectOpt = this.objectService.getObject(editingContext, containerId) + .filter(EObject.class::isInstance) + .map(EObject.class::cast); + + EObject targetContainer; + + if (index >= 0) { + targetContainer = containerEObjectOpt.map(EObject::eContainer).orElse(null); + } else { + targetContainer = containerEObjectOpt.orElse(null); + } + + if (targetContainer != null) { + objectsToMove.forEach(source -> this.getContainmentFeatureName(source, targetContainer) + .map(featureName -> targetContainer.eClass().getEStructuralFeature(featureName)) + .ifPresentOrElse(feature -> { + if (feature.isMany()) { + EList list = (EList) targetContainer.eGet(feature); + EcoreUtil.remove(source); + if (index >= 0 && index < list.size()) { + list.add(index, source); + } else { + list.add(source); + } + } else { + if (targetContainer.eGet(feature) == null) { + EcoreUtil.remove(source); + targetContainer.eSet(feature, source); + } else { + this.feedbackMessageService.addFeedbackMessage(new Message(this.siriusWebMessageService.alreadySetFeature(), MessageLevel.ERROR)); + } + } + }, () -> this.feedbackMessageService.addFeedbackMessage(new Message(this.siriusWebMessageService.unavailableFeature(), MessageLevel.ERROR)))); + } + } + + 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(); + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/api/IExplorerModificationService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/api/IExplorerModificationService.java new file mode 100644 index 0000000000..0aafe7e769 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/api/IExplorerModificationService.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * 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.api; + +import java.util.List; + +import org.eclipse.sirius.components.core.api.IEditingContext; + +/** + * Service related to modification from Explorer. + * + * @author frouene + */ +public interface IExplorerModificationService { + + void moveObject(IEditingContext editingContext, List objectToMoveIds, String containerId, int index); +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/resources/messages/sirius-web-application.properties b/packages/sirius-web/backend/sirius-web-application/src/main/resources/messages/sirius-web-application.properties new file mode 100644 index 0000000000..4cc25c237e --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/resources/messages/sirius-web-application.properties @@ -0,0 +1,14 @@ +################################################################################ +# 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 +################################################################################# +UNAVAILABLE_FEATURE=No contenance feature found for this element +ALREADY_SET_FEATURE=An element is already present for this mono-valued feature 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..009bf55724 --- /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,102 @@ +/******************************************************************************* + * 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.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.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.IFeedbackMessageService; +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.representations.VariableManager; +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 static final String DROPPED_TREE_ITEMS = "droppedTreeItems"; + + private static final String TARGET_TREE_ITEM = "targetTreeItem"; + + private static final String INDEX = "index"; + + private final ICollaborativeTreeMessageService messageService; + + private final IFeedbackMessageService feedbackMessageService; + + private final Counter counter; + + public DropTreeItemEventHandler(ICollaborativeTreeMessageService messageService, IFeedbackMessageService feedbackMessageService, MeterRegistry meterRegistry) { + this.messageService = Objects.requireNonNull(messageService); + this.feedbackMessageService = Objects.requireNonNull(feedbackMessageService); + 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) { + VariableManager variableManager = new VariableManager(); + variableManager.put(IEditingContext.EDITING_CONTEXT, editingContext); + variableManager.put(DROPPED_TREE_ITEMS, input.droppedElementIds()); + variableManager.put(TARGET_TREE_ITEM, input.targetElementId()); + variableManager.put(INDEX, input.index()); + + var status = treeDescription.getDropTreeItemHandler().apply(variableManager); + if (status instanceof Success success) { + changeDescription = new ChangeDescription(success.getChangeKind(), treeInput.representationId(), treeInput, success.getParameters()); + payload = new SuccessPayload(treeInput.id(), this.feedbackMessageService.getFeedbackMessages()); + } 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/resources/schema/tree.graphqls b/packages/trees/backend/sirius-components-collaborative-trees/src/main/resources/schema/tree.graphqls index d231858d23..86023b6602 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 @@ -42,7 +42,7 @@ type StyledStringFragment { text: String! styledStringFragmentStyle : StyledStringFragmentStyle } - + type StyledStringFragmentStyle { isStruckOut: Boolean underlineStyle: UnderLineStyle @@ -96,6 +96,18 @@ type TreeFilter { extend type Mutation { deleteTreeItem(input: DeleteTreeItemInput!): DeleteTreeItemPayload renameTreeItem(input: RenameTreeItemInput!): RenameTreeItemPayload + 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/src/main/java/org/eclipse/sirius/components/trees/description/TreeDescription.java b/packages/trees/backend/sirius-components-trees/src/main/java/org/eclipse/sirius/components/trees/description/TreeDescription.java index 73e1c3c76c..f50ad6b576 100644 --- a/packages/trees/backend/sirius-components-trees/src/main/java/org/eclipse/sirius/components/trees/description/TreeDescription.java +++ b/packages/trees/backend/sirius-components-trees/src/main/java/org/eclipse/sirius/components/trees/description/TreeDescription.java @@ -81,6 +81,8 @@ public final class TreeDescription implements IRepresentationDescription { private Function treeItemObjectProvider; + private Function dropTreeItemHandler; + private TreeDescription() { // Prevent instantiation } @@ -168,6 +170,10 @@ public Function getTreeItemObjectProvider() { return this.treeItemObjectProvider; } + public Function getDropTreeItemHandler() { + return this.dropTreeItemHandler; + } + @Override public String toString() { String pattern = "{0} '{'id: {1}, label: {2}'}'"; @@ -220,6 +226,8 @@ public static final class Builder { private Function treeItemObjectProvider; + private Function dropTreeItemHandler; + private Builder(String id) { this.id = Objects.requireNonNull(id); } @@ -314,6 +322,11 @@ public Builder treeItemObjectProvider(Function treeItem return this; } + public Builder dropTreeItemHandler(Function dropTreeItemHandler) { + this.dropTreeItemHandler = Objects.requireNonNull(dropTreeItemHandler); + return this; + } + public TreeDescription build() { TreeDescription treeDescription = new TreeDescription(); treeDescription.id = Objects.requireNonNull(this.id); @@ -335,6 +348,7 @@ public TreeDescription build() { treeDescription.deleteHandler = Objects.requireNonNull(this.deleteHandler); treeDescription.renameHandler = Objects.requireNonNull(this.renameHandler); treeDescription.treeItemObjectProvider = Objects.requireNonNull(this.treeItemObjectProvider); + treeDescription.dropTreeItemHandler = Objects.requireNonNull(this.dropTreeItemHandler); return treeDescription; } } 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 6b1fc32620..c3ef0fcb86 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..f58f3fda3a --- /dev/null +++ b/packages/trees/frontend/sirius-components-trees/src/treeitems/useDropTreeItem.ts @@ -0,0 +1,83 @@ +/******************************************************************************* + * 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, +} 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) => { + 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]); + + return useCallback((droppedElementIds: string[], targetElementId: string, index: number): void => { + const input: GQLDropTreeItemInput = { + id: crypto.randomUUID(), + editingContextId, + representationId: treeId, + droppedElementIds, + targetElementId, + index, + }; + dropMutation({ variables: { input } }); + }, []); +}; 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..137686056e --- /dev/null +++ b/packages/trees/frontend/sirius-components-trees/src/treeitems/useDropTreeItem.types.ts @@ -0,0 +1,42 @@ +/******************************************************************************* + * 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 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 | null; + 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<>(); @@ -112,9 +114,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) { @@ -200,6 +202,7 @@ private TreeDescription createTreeDescription(SelectionDialogDescription selecti .selectableProvider(isSelectableProvider) .treeItemObjectProvider(this::getTreeItemObject) .parentObjectProvider(this::getParentObject) + .dropTreeItemHandler(variableManager -> new Success()) .build(); } @@ -292,8 +295,8 @@ private Function> getElementProvider(AQLInterpreter int //Set the targetObject as the SELF value. //The targetObjectId is provided by the frontend in the treeId. this.getTargetObjectId(variableManager) - .flatMap(targetObjectId -> this.objectService.getObject(optionalEditingContext.get(), targetObjectId)) - .ifPresent(targetObject -> variableManager.put(VariableManager.SELF, targetObject)); + .flatMap(targetObjectId -> this.objectService.getObject(optionalEditingContext.get(), targetObjectId)) + .ifPresent(targetObject -> variableManager.put(VariableManager.SELF, targetObject)); String elementsExpression = selectionDialogTreeDescription.getElementsExpression(); Result result = interpreter.evaluateExpression(variableManager.getVariables(), elementsExpression);