From 7dca70efa79c112035e2478b414c596c18bc9751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20ROU=C3=8BN=C3=89?= Date: Mon, 6 Jan 2025 16:02:56 +0100 Subject: [PATCH] [4383] Add support for representation duplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: https://github.com/eclipse-sirius/sirius-web/issues/4383 Signed-off-by: Florian ROUËNÉ --- CHANGELOG.adoc | 3 +- .../IRepresentationPersistenceService.java | 10 +- .../api/IRepresentationSearchService.java | 10 +- .../portals/services/PortalServicesTests.java | 17 +- .../RepresentationPersistenceService.java | 11 +- .../services/RepresentationSearchService.java | 11 +- ...ionDuplicateRepresentationDataFetcher.java | 54 ++++++ .../dto/DuplicateRepresentationInput.java | 28 +++ ...DuplicateRepresentationSuccessPayload.java | 31 ++++ .../DuplicateRepresentationEventHandler.java | 161 ++++++++++++++++++ .../schema/duplicaterepresentation.graphqls | 17 ++ ...DuplicateRepresentationMutationRunner.java | 62 +++++++ ...DuplicationControllerIntegrationTests.java | 90 ++++++++++ .../extension/DefaultExtensionRegistry.tsx | 5 + .../useDuplicateObject.types.ts | 4 +- .../ObjectTreeItemContextMenuContribution.tsx | 2 +- ...ntationTreeItemContextMenuContribution.tsx | 61 +++++++ .../useDuplicateRepresentation.ts | 80 +++++++++ .../useDuplicateRepresentation.types.ts | 47 +++++ 19 files changed, 695 insertions(+), 9 deletions(-) create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/controllers/MutationDuplicateRepresentationDataFetcher.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/dto/DuplicateRepresentationInput.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/dto/DuplicateRepresentationSuccessPayload.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/DuplicateRepresentationEventHandler.java create mode 100644 packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/duplicaterepresentation.graphqls create mode 100644 packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/graphql/DuplicateRepresentationMutationRunner.java create mode 100644 packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/representations/RepresentationDuplicationControllerIntegrationTests.java create mode 100644 packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/RepresentationTreeItemContextMenuContribution.tsx create mode 100644 packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/useDuplicateRepresentation.ts create mode 100644 packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/useDuplicateRepresentation.types.ts diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index db8747fa0b..e6bfd05410 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -36,7 +36,8 @@ This may have some consequences for downstream applications which are embedding + Added by default to all sirius-web diagrams - https://github.com/eclipse-sirius/sirius-web/issues/4346[#4346] [query] Add support for a query view. Specifiers can contribute dedicated AQL services for this feature using implementations of `IInterpreterJavaServiceProvider`. -- https://github.com/eclipse-sirius/sirius-web/issues/3740[#3740] [sirius-web] Add support for object duplication from explorer +- https://github.com/eclipse-sirius/sirius-web/issues/3740[#3740] [sirius-web] Add support for semantic object duplication from explorer +- https://github.com/eclipse-sirius/sirius-web/issues/4383[#4383] [sirius-web] Add support for representation duplication from explorer === Improvements diff --git a/packages/core/backend/sirius-components-collaborative/src/main/java/org/eclipse/sirius/components/collaborative/api/IRepresentationPersistenceService.java b/packages/core/backend/sirius-components-collaborative/src/main/java/org/eclipse/sirius/components/collaborative/api/IRepresentationPersistenceService.java index 01463a66e1..b5b87e438a 100644 --- a/packages/core/backend/sirius-components-collaborative/src/main/java/org/eclipse/sirius/components/collaborative/api/IRepresentationPersistenceService.java +++ b/packages/core/backend/sirius-components-collaborative/src/main/java/org/eclipse/sirius/components/collaborative/api/IRepresentationPersistenceService.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021, 2024 Obeo. + * Copyright (c) 2021, 2025 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 @@ -25,6 +25,8 @@ public interface IRepresentationPersistenceService { void save(ICause cause, IEditingContext editingContext, IRepresentation representation); + void save(ICause cause, IEditingContext editingContext, String representationId, String representationContent, String representationKind); + /** * Empty implementation, used for mocks in unit tests. * @@ -37,6 +39,12 @@ public void save(ICause cause, IEditingContext editingContext, IRepresentation r // Do nothing } + @Override + public void save(ICause cause, IEditingContext editingContext, String representationId, String representationContent, String representationKind) { + //Do nothing + } + + } } diff --git a/packages/core/backend/sirius-components-collaborative/src/main/java/org/eclipse/sirius/components/collaborative/api/IRepresentationSearchService.java b/packages/core/backend/sirius-components-collaborative/src/main/java/org/eclipse/sirius/components/collaborative/api/IRepresentationSearchService.java index c7f0bc13bc..871e6873cc 100644 --- a/packages/core/backend/sirius-components-collaborative/src/main/java/org/eclipse/sirius/components/collaborative/api/IRepresentationSearchService.java +++ b/packages/core/backend/sirius-components-collaborative/src/main/java/org/eclipse/sirius/components/collaborative/api/IRepresentationSearchService.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021, 2024 Obeo. + * Copyright (c) 2021, 2025 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 @@ -24,6 +24,9 @@ * @author sbegaudeau */ public interface IRepresentationSearchService { + + Optional findById(IEditingContext editingContext, String representationId); + Optional findById(IEditingContext editingContext, String representationId, Class representationClass); boolean existByIdAndKind(String representationId, List kinds); @@ -35,6 +38,11 @@ public interface IRepresentationSearchService { */ class NoOp implements IRepresentationSearchService { + @Override + public Optional findById(IEditingContext editingContext, String representationId) { + return Optional.empty(); + } + @Override public Optional findById(IEditingContext editingContext, String representationId, Class representationClass) { return Optional.empty(); diff --git a/packages/portals/backend/sirius-components-collaborative-portals/src/test/java/org/eclipse/sirius/components/collaborative/portals/services/PortalServicesTests.java b/packages/portals/backend/sirius-components-collaborative-portals/src/test/java/org/eclipse/sirius/components/collaborative/portals/services/PortalServicesTests.java index ca8baab9b4..a23d897da5 100644 --- a/packages/portals/backend/sirius-components-collaborative-portals/src/test/java/org/eclipse/sirius/components/collaborative/portals/services/PortalServicesTests.java +++ b/packages/portals/backend/sirius-components-collaborative-portals/src/test/java/org/eclipse/sirius/components/collaborative/portals/services/PortalServicesTests.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Obeo. + * Copyright (c) 2024, 2025 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 @@ -40,6 +40,11 @@ public class PortalServicesTests { private static final IEditingContext NOOP_EDITING_CONTEXT = new IEditingContext.NoOp(); private static final IRepresentationSearchService NOOP_SEARCH_SERVICE = new IRepresentationSearchService() { + @Override + public Optional findById(IEditingContext editingContext, String representationId) { + return Optional.empty(); + } + @Override public Optional findById(IEditingContext editingContext, String representationId, Class representationClass) { return Optional.empty(); @@ -136,6 +141,11 @@ public void testPreventDirectLoop() { .build(); IRepresentationSearchService mockSearchService = new IRepresentationSearchService() { + @Override + public Optional findById(IEditingContext editingContext, String representationId) { + return Optional.empty(); + } + @Override public Optional findById(IEditingContext editingContext, String representationId, Class representationClass) { return Optional.of(representationClass.cast(portal)); @@ -167,6 +177,11 @@ public void testPreventIndirectLoop() { portalsRepository.add(portal2); IRepresentationSearchService mockSearchService = new IRepresentationSearchService() { + @Override + public Optional findById(IEditingContext editingContext, String representationId) { + return Optional.empty(); + } + @Override public Optional findById(IEditingContext editingContext, String representationId, Class representationClass) { return portalsRepository.stream().filter(portal -> portal.getId().equals(representationId)).findFirst().map(representationClass::cast); diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/RepresentationPersistenceService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/RepresentationPersistenceService.java index ecba0fb106..bbb7500595 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/RepresentationPersistenceService.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/RepresentationPersistenceService.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Obeo. + * Copyright (c) 2024, 2025 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 @@ -85,6 +85,15 @@ public void save(ICause cause, IEditingContext editingContext, IRepresentation r } } + @Override + @Transactional + public void save(ICause cause, IEditingContext editingContext, String representationId, String representationContent, String representationKind) { + new UUIDParser().parse(representationId).ifPresent(representationUUID -> { + var migrationData = this.getInitialMigrationData(representationKind); + this.representationContentCreationService.create(cause, representationUUID, representationContent, migrationData.lastMigrationPerformed(), migrationData.migrationVersion()); + }); + } + private String toString(IRepresentation representation) { String content = ""; try { diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/RepresentationSearchService.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/RepresentationSearchService.java index 708b0e0f40..dd3317cb48 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/RepresentationSearchService.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/representation/services/RepresentationSearchService.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Obeo. + * Copyright (c) 2024, 2025 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 @@ -58,6 +58,13 @@ public RepresentationSearchService(IRepresentationMetadataSearchService represen this.objectMapper = Objects.requireNonNull(objectMapper); } + @Override + public Optional findById(IEditingContext editingContext, String representationId) { + return new UUIDParser().parse(representationId) + .flatMap(this.representationMetadataSearchService::findMetadataById) + .flatMap(this::getRepresentation); + } + @Override public Optional findById(IEditingContext editingContext, String representationId, Class representationClass) { return new UUIDParser().parse(representationId) @@ -75,7 +82,7 @@ private Optional getRepresentation(RepresentationMetadata repre @Override public boolean existByIdAndKind(String representationId, List kinds) { - Optional uuid = new UUIDParser().parse(representationId); + Optional uuid = new UUIDParser().parse(representationId); return uuid.filter(value -> this.representationMetadataSearchService.existsByIdAndKind(value, kinds)).isPresent(); } diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/controllers/MutationDuplicateRepresentationDataFetcher.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/controllers/MutationDuplicateRepresentationDataFetcher.java new file mode 100644 index 0000000000..ef5aefb06e --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/controllers/MutationDuplicateRepresentationDataFetcher.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2025 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.controllers; + +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.core.api.IPayload; +import org.eclipse.sirius.components.graphql.api.IDataFetcherWithFieldCoordinates; +import org.eclipse.sirius.components.graphql.api.IEditingContextDispatcher; +import org.eclipse.sirius.web.application.views.explorer.dto.DuplicateRepresentationInput; + +import graphql.schema.DataFetchingEnvironment; + +/** + * Data fetcher for the field Mutation#duplicateRepresentation. + * + * @author frouene + */ +@MutationDataFetcher(type = "Mutation", field = "duplicateRepresentation") +public class MutationDuplicateRepresentationDataFetcher implements IDataFetcherWithFieldCoordinates> { + + private static final String INPUT_ARGUMENT = "input"; + + private final ObjectMapper objectMapper; + + private final IEditingContextDispatcher editingContextDispatcher; + + public MutationDuplicateRepresentationDataFetcher(ObjectMapper objectMapper, IEditingContextDispatcher editingContextDispatcher) { + this.objectMapper = Objects.requireNonNull(objectMapper); + 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, DuplicateRepresentationInput.class); + + return this.editingContextDispatcher.dispatchMutation(input.editingContextId(), input).toFuture(); + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/dto/DuplicateRepresentationInput.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/dto/DuplicateRepresentationInput.java new file mode 100644 index 0000000000..c7999557d0 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/dto/DuplicateRepresentationInput.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2025 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.dto; + +import java.util.UUID; + +import org.eclipse.sirius.components.core.api.IInput; + +import jakarta.validation.constraints.NotNull; + +/** + * The input object of the duplicate representation mutation. + * + * @author frouene + */ +public record DuplicateRepresentationInput(@NotNull UUID id, @NotNull String editingContextId, @NotNull String representationId) implements IInput { + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/dto/DuplicateRepresentationSuccessPayload.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/dto/DuplicateRepresentationSuccessPayload.java new file mode 100644 index 0000000000..4cc4740a33 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/dto/DuplicateRepresentationSuccessPayload.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2025 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.dto; + +import java.util.List; +import java.util.UUID; + +import org.eclipse.sirius.components.core.RepresentationMetadata; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.representations.Message; + +import jakarta.validation.constraints.NotNull; + +/** + * The payload of the duplicate representation mutation. + * + * @author frouene + */ +public record DuplicateRepresentationSuccessPayload(@NotNull UUID id, @NotNull RepresentationMetadata representationMetadata, @NotNull List messages) implements IPayload { + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/DuplicateRepresentationEventHandler.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/DuplicateRepresentationEventHandler.java new file mode 100644 index 0000000000..5847c3a385 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/explorer/services/DuplicateRepresentationEventHandler.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * Copyright (c) 2025 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 com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +import org.eclipse.sirius.components.collaborative.api.ChangeDescription; +import org.eclipse.sirius.components.collaborative.api.ChangeKind; +import org.eclipse.sirius.components.collaborative.api.IEditingContextEventHandler; +import org.eclipse.sirius.components.collaborative.api.IRepresentationMetadataPersistenceService; +import org.eclipse.sirius.components.collaborative.api.IRepresentationPersistenceService; +import org.eclipse.sirius.components.collaborative.api.IRepresentationSearchService; +import org.eclipse.sirius.components.collaborative.api.Monitoring; +import org.eclipse.sirius.components.collaborative.messages.ICollaborativeMessageService; +import org.eclipse.sirius.components.core.api.ErrorPayload; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IInput; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.representations.Failure; +import org.eclipse.sirius.components.representations.IRepresentation; +import org.eclipse.sirius.components.representations.IStatus; +import org.eclipse.sirius.components.representations.Message; +import org.eclipse.sirius.components.representations.MessageLevel; +import org.eclipse.sirius.components.representations.Success; +import org.eclipse.sirius.web.application.UUIDParser; +import org.eclipse.sirius.web.application.views.explorer.dto.DuplicateObjectInput; +import org.eclipse.sirius.web.application.views.explorer.dto.DuplicateRepresentationInput; +import org.eclipse.sirius.web.application.views.explorer.dto.DuplicateRepresentationSuccessPayload; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.RepresentationIconURL; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.RepresentationMetadata; +import org.eclipse.sirius.web.domain.boundedcontexts.representationdata.services.api.IRepresentationMetadataSearchService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import reactor.core.publisher.Sinks; + +/** + * Handler used to duplicate a representation. + * + * @author frouene + */ +@Service +public class DuplicateRepresentationEventHandler implements IEditingContextEventHandler { + + private static final String NEW_REPRESENTATION_METADATA = "newRepresentationMetadata"; + private static final String COPY_SUFFIX = "_copy"; + + private final Logger logger = LoggerFactory.getLogger(DuplicateRepresentationEventHandler.class); + + private final ICollaborativeMessageService messageService; + + private final IRepresentationMetadataSearchService representationMetadataSearchService; + + private final IRepresentationSearchService representationSearchService; + + private final IRepresentationMetadataPersistenceService representationMetadataPersistenceService; + + private final IRepresentationPersistenceService representationPersistenceService; + + private final ObjectMapper objectMapper; + + private final Counter counter; + + public DuplicateRepresentationEventHandler(ICollaborativeMessageService messageService, IRepresentationMetadataSearchService representationMetadataSearchService, IRepresentationSearchService representationSearchService, + IRepresentationMetadataPersistenceService representationMetadataPersistenceService, IRepresentationPersistenceService representationPersistenceService, ObjectMapper objectMapper, + MeterRegistry meterRegistry) { + this.messageService = Objects.requireNonNull(messageService); + this.representationMetadataSearchService = Objects.requireNonNull(representationMetadataSearchService); + this.representationMetadataPersistenceService = Objects.requireNonNull(representationMetadataPersistenceService); + this.representationSearchService = Objects.requireNonNull(representationSearchService); + this.representationPersistenceService = Objects.requireNonNull(representationPersistenceService); + this.objectMapper = Objects.requireNonNull(objectMapper); + + this.counter = Counter.builder(Monitoring.EVENT_HANDLER).tag(Monitoring.NAME, this.getClass().getSimpleName()).register(meterRegistry); + } + + @Override + public boolean canHandle(IEditingContext editingContext, IInput input) { + return input instanceof DuplicateRepresentationInput; + } + + @Override + public void handle(Sinks.One payloadSink, Sinks.Many changeDescriptionSink, IEditingContext editingContext, IInput input) { + this.counter.increment(); + + List messages = List.of(new Message(this.messageService.invalidInput(input.getClass().getSimpleName(), DuplicateObjectInput.class.getSimpleName()), MessageLevel.ERROR)); + ChangeDescription changeDescription = new ChangeDescription(ChangeKind.NOTHING, editingContext.getId(), input); + IPayload payload = new ErrorPayload(input.id(), messages); + + if (input instanceof DuplicateRepresentationInput duplicateRepresentationInput) { + IStatus duplicationResult = this.duplicateRepresentation(editingContext, duplicateRepresentationInput.representationId(), duplicateRepresentationInput); + + if (duplicationResult instanceof Success success) { + payload = new DuplicateRepresentationSuccessPayload(input.id(), (org.eclipse.sirius.components.core.RepresentationMetadata) success.getParameters() + .get(NEW_REPRESENTATION_METADATA), success.getMessages()); + changeDescription = new ChangeDescription(success.getChangeKind(), editingContext.getId(), input); + } else if (duplicationResult instanceof Failure failure) { + payload = new ErrorPayload(input.id(), failure.getMessages()); + } + } + + payloadSink.tryEmitValue(payload); + changeDescriptionSink.tryEmitNext(changeDescription); + } + + private IStatus duplicateRepresentation(IEditingContext editingContext, String representationId, DuplicateRepresentationInput duplicateRepresentationInput) { + + Optional representationMetadataOptional = new UUIDParser().parse(representationId) + .flatMap(this.representationMetadataSearchService::findMetadataById); + Optional representationOptional = this.representationSearchService.findById(editingContext, representationId); + + if (representationMetadataOptional.isPresent() && representationOptional.isPresent()) { + String duplicatedRepresentationId = UUID.randomUUID().toString(); + var duplicatedRepresentationMetadata = org.eclipse.sirius.components.core.RepresentationMetadata.newRepresentationMetadata(duplicatedRepresentationId) + .kind(representationMetadataOptional.get().getKind()) + .label(representationMetadataOptional.get().getLabel() + COPY_SUFFIX) + .descriptionId(representationMetadataOptional.get().getDescriptionId()) + .iconURLs(representationMetadataOptional.get().getIconURLs().stream().map(RepresentationIconURL::url).toList()) + .build(); + + var content = this.getContent(representationOptional.get(), representationId, duplicatedRepresentationId); + + this.representationMetadataPersistenceService.save(duplicateRepresentationInput, editingContext, duplicatedRepresentationMetadata, representationMetadataOptional.get() + .getTargetObjectId()); + this.representationPersistenceService.save(duplicateRepresentationInput, editingContext, duplicatedRepresentationId, content, representationOptional.get().getKind()); + return new Success(ChangeKind.REPRESENTATION_CREATION, Map.of(NEW_REPRESENTATION_METADATA, duplicatedRepresentationMetadata)); + } + return new Failure("The duplication of the representation has failed"); + } + + private String getContent(IRepresentation representation, String oldRepresentationId, String newRepresentationId) { + String content = ""; + try { + content = this.objectMapper.writeValueAsString(representation); + content = content.replace(oldRepresentationId, newRepresentationId); + } catch (JsonProcessingException exception) { + this.logger.warn(exception.getMessage(), exception); + } + return content; + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/duplicaterepresentation.graphqls b/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/duplicaterepresentation.graphqls new file mode 100644 index 0000000000..fedf133328 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/duplicaterepresentation.graphqls @@ -0,0 +1,17 @@ +extend type Mutation { + duplicateRepresentation(input: DuplicateRepresentationInput!): DuplicateRepresentationPayload! +} + +input DuplicateRepresentationInput { + id: ID! + editingContextId: ID! + representationId: ID! +} + +type DuplicateRepresentationSuccessPayload { + id: ID! + representationMetadata: RepresentationMetadata + messages: [Message]! +} + +union DuplicateRepresentationPayload = ErrorPayload | DuplicateRepresentationSuccessPayload diff --git a/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/graphql/DuplicateRepresentationMutationRunner.java b/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/graphql/DuplicateRepresentationMutationRunner.java new file mode 100644 index 0000000000..98c8a7b0c6 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/graphql/DuplicateRepresentationMutationRunner.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2025 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.tests.graphql; + +import java.util.Objects; + +import org.eclipse.sirius.components.graphql.tests.api.IGraphQLRequestor; +import org.eclipse.sirius.components.graphql.tests.api.IMutationRunner; +import org.eclipse.sirius.web.application.views.explorer.dto.DuplicateRepresentationInput; +import org.springframework.stereotype.Service; + +/** + * Used to duplicate a representation with the GraphQL API. + * + * @author frouene + */ +@Service +public class DuplicateRepresentationMutationRunner implements IMutationRunner { + + private static final String DUPLICATE_REPRESENTATION = """ + mutation duplicateRepresentation($input: DuplicateRepresentationInput!) { + duplicateRepresentation(input: $input) { + __typename + ... on DuplicateRepresentationSuccessPayload { + representationMetadata { + id + label + kind + } + } + ... on ErrorPayload { + message + messages { + level + body + } + } + } + } + """; + + private final IGraphQLRequestor graphQLRequestor; + + public DuplicateRepresentationMutationRunner(IGraphQLRequestor graphQLRequestor) { + this.graphQLRequestor = Objects.requireNonNull(graphQLRequestor); + } + + @Override + public String run(DuplicateRepresentationInput input) { + return this.graphQLRequestor.execute(DUPLICATE_REPRESENTATION, input); + } +} diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/representations/RepresentationDuplicationControllerIntegrationTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/representations/RepresentationDuplicationControllerIntegrationTests.java new file mode 100644 index 0000000000..db8ef4c049 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/representations/RepresentationDuplicationControllerIntegrationTests.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) 2025 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.representations; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.jayway.jsonpath.JsonPath; + +import java.util.UUID; + +import org.eclipse.sirius.web.AbstractIntegrationTests; +import org.eclipse.sirius.web.application.views.explorer.dto.DuplicateRepresentationInput; +import org.eclipse.sirius.web.application.views.explorer.dto.DuplicateRepresentationSuccessPayload; +import org.eclipse.sirius.web.data.PapayaIdentifiers; +import org.eclipse.sirius.web.tests.graphql.DuplicateRepresentationMutationRunner; +import org.eclipse.sirius.web.tests.services.api.IGivenCommittedTransaction; +import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState; +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.transaction.annotation.Transactional; + +/** + * Integration tests of the representation duplicate controllers. + * + * @author frouene + */ +@Transactional +@SuppressWarnings("checkstyle:MultipleStringLiterals") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class RepresentationDuplicationControllerIntegrationTests extends AbstractIntegrationTests { + + @Autowired + private IGivenInitialServerState givenInitialServerState; + + @Autowired + private IGivenCommittedTransaction givenCommittedTransaction; + + @Autowired + private DuplicateRepresentationMutationRunner duplicateRepresentationMutationRunner; + + @BeforeEach + public void beforeEach() { + this.givenInitialServerState.initialize(); + } + + + @Test + @DisplayName("Given a representation, when this representation is duplicated, then it is duplicated properly") + @Sql(scripts = {"/scripts/papaya.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 givenRepresentationWhenRepresentationIsDuplicatedThenItIsDuplicatedProperly() { + this.givenCommittedTransaction.commit(); + + var input = new DuplicateRepresentationInput( + UUID.randomUUID(), + PapayaIdentifiers.PAPAYA_PROJECT.toString(), + PapayaIdentifiers.PAPAYA_PACKAGE_TABLE_REPRESENTATION.toString() + ); + var result = this.duplicateRepresentationMutationRunner.run(input); + + String typename = JsonPath.read(result, "$.data.duplicateRepresentation.__typename"); + assertThat(typename).isEqualTo(DuplicateRepresentationSuccessPayload.class.getSimpleName()); + + String objectId = JsonPath.read(result, "$.data.duplicateRepresentation.representationMetadata.id"); + assertThat(objectId).isNotBlank(); + + String objectLabel = JsonPath.read(result, "$.data.duplicateRepresentation.representationMetadata.label"); + assertThat(objectLabel).isNotBlank(); + assertThat(objectLabel).isEqualTo("_copy"); + + String objectKind = JsonPath.read(result, "$.data.duplicateRepresentation.representationMetadata.kind"); + assertThat(objectKind).isEqualTo("siriusComponents://representation?type=Table"); + } + +} diff --git a/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx b/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx index af13cf7359..a1e811ac5d 100644 --- a/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx +++ b/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx @@ -66,6 +66,7 @@ import { DocumentTreeItemContextMenuContribution } from '../views/edit-project/D import { DownloadProjectMenuEntryContribution } from '../views/edit-project/EditProjectNavbar/DownloadProjectMenuEntryContribution'; import { editProjectNavbarMenuEntryExtensionPoint } from '../views/edit-project/EditProjectNavbar/EditProjectNavbarMenuExtensionPoints'; import { ObjectTreeItemContextMenuContribution } from '../views/edit-project/ObjectTreeItemContextMenuContribution'; +import { RepresentationTreeItemContextMenuContribution } from '../views/edit-project/RepresentationTreeItemContextMenuContribution'; import { DetailsView } from '../views/edit-project/workbench-views/details/DetailsView'; import { ExplorerView } from '../views/edit-project/workbench-views/explorer/ExplorerView'; import { QueryView } from '../views/edit-project/workbench-views/query/QueryView'; @@ -274,6 +275,10 @@ defaultExtensionRegistry.addComponent(treeItemContextMenuEntryExtensionPoint, { identifier: `siriusweb_${treeItemContextMenuEntryExtensionPoint.identifier}_diagram`, Component: DiagramTreeItemContextMenuContribution, }); +defaultExtensionRegistry.addComponent(treeItemContextMenuEntryExtensionPoint, { + identifier: `siriusweb_${treeItemContextMenuEntryExtensionPoint.identifier}_representation`, + Component: RepresentationTreeItemContextMenuContribution, +}); /******************************************************************************* * diff --git a/packages/sirius-web/frontend/sirius-web-application/src/modals/duplicate-object/useDuplicateObject.types.ts b/packages/sirius-web/frontend/sirius-web-application/src/modals/duplicate-object/useDuplicateObject.types.ts index 784ed698ce..10d10c5bfa 100644 --- a/packages/sirius-web/frontend/sirius-web-application/src/modals/duplicate-object/useDuplicateObject.types.ts +++ b/packages/sirius-web/frontend/sirius-web-application/src/modals/duplicate-object/useDuplicateObject.types.ts @@ -23,12 +23,13 @@ export interface UseDuplicateObjectValue { copyOutgoingReferences: boolean, updateIncomingReferences: boolean ) => void; - duplicatedObject: GQLObject; + duplicatedObject: GQLObject | null; } export interface GQLDuplicateObjectVariables { input: GQLDuplicateObjectInput; } + export interface GQLDuplicateObjectInput { id: string; editingContextId: string; @@ -39,6 +40,7 @@ export interface GQLDuplicateObjectInput { copyOutgoingReferences: boolean; updateIncomingReferences: boolean; } + export interface GQLDuplicateObjectData { duplicateObject: GQLDuplicateObjectPayload; } diff --git a/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/ObjectTreeItemContextMenuContribution.tsx b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/ObjectTreeItemContextMenuContribution.tsx index e8811cad46..3edbf74d4d 100644 --- a/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/ObjectTreeItemContextMenuContribution.tsx +++ b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/ObjectTreeItemContextMenuContribution.tsx @@ -107,7 +107,7 @@ export const ObjectTreeItemContextMenuContribution = forwardRef( - + {modalElement} diff --git a/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/RepresentationTreeItemContextMenuContribution.tsx b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/RepresentationTreeItemContextMenuContribution.tsx new file mode 100644 index 0000000000..e744d5b06e --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/RepresentationTreeItemContextMenuContribution.tsx @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 2021, 2025 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 { useSelection } from '@eclipse-sirius/sirius-components-core'; +import { TreeItemContextMenuComponentProps } from '@eclipse-sirius/sirius-components-trees'; +import AddToPhotosIcon from '@mui/icons-material/AddToPhotos'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import MenuItem from '@mui/material/MenuItem'; +import { Fragment, forwardRef, useEffect } from 'react'; +import { useDuplicateRepresentation } from './useDuplicateRepresentation'; + +export const RepresentationTreeItemContextMenuContribution = forwardRef( + ( + { editingContextId, treeId, item, readOnly, onClose }: TreeItemContextMenuComponentProps, + ref: React.ForwardedRef + ) => { + const { setSelection } = useSelection(); + const { duplicateRepresentation, duplicatedRepresentationMetadata } = useDuplicateRepresentation(); + const onDuplicate = () => { + duplicateRepresentation(editingContextId, item.id); + }; + + useEffect(() => { + if (duplicatedRepresentationMetadata) { + setSelection({ entries: [duplicatedRepresentationMetadata] }); + onClose(); + } + }, [duplicatedRepresentationMetadata, setSelection]); + + if (!treeId.startsWith('explorer://') || !item.kind.startsWith('siriusComponents://representation')) { + return null; + } + + return ( + + + + + + + + + ); + } +); diff --git a/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/useDuplicateRepresentation.ts b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/useDuplicateRepresentation.ts new file mode 100644 index 0000000000..a5c00ffb6d --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/useDuplicateRepresentation.ts @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2025 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 { useReporting } from '@eclipse-sirius/sirius-components-core'; +import { + UseDuplicateRepresentationValue, + GQLDuplicateRepresentationVariables, + GQLDuplicateRepresentationData, + GQLDuplicateRepresentationInput, + GQLRepresentationMetadata, +} from './useDuplicateRepresentation.types'; + +const duplicateRepresentationMutation = gql` + mutation duplicateRepresentation($input: DuplicateRepresentationInput!) { + duplicateRepresentation(input: $input) { + __typename + ... on ErrorPayload { + messages { + body + level + } + } + ... on DuplicateRepresentationSuccessPayload { + representationMetadata { + id + kind + } + messages { + body + level + } + } + } + } +`; + +export const useDuplicateRepresentation = (): UseDuplicateRepresentationValue => { + const [mutationDuplicateRepresentation, mutationDuplicateRepresentationResult] = useMutation< + GQLDuplicateRepresentationData, + GQLDuplicateRepresentationVariables + >(duplicateRepresentationMutation); + useReporting(mutationDuplicateRepresentationResult, (data: GQLDuplicateRepresentationData) => { + const payload = data.duplicateRepresentation; + if (payload.__typename === 'DuplicateRepresentationSuccessPayload') { + return { __typename: 'SuccessPayload', id: payload.id, messages: payload.messages }; + } else { + return { __typename: 'ErrorPayload', id: payload.id, messages: payload.messages }; + } + }); + + const duplicateRepresentation = (editingContextId: string, representationId: string) => { + const input: GQLDuplicateRepresentationInput = { + id: crypto.randomUUID(), + editingContextId, + representationId, + }; + mutationDuplicateRepresentation({ variables: { input } }); + }; + + let duplicatedRepresentationMetadata: GQLRepresentationMetadata = null; + if ( + mutationDuplicateRepresentationResult?.data?.duplicateRepresentation?.__typename === + 'DuplicateRepresentationSuccessPayload' + ) { + duplicatedRepresentationMetadata = + mutationDuplicateRepresentationResult.data.duplicateRepresentation.representationMetadata; + } + + return { duplicateRepresentation, duplicatedRepresentationMetadata }; +}; diff --git a/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/useDuplicateRepresentation.types.ts b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/useDuplicateRepresentation.types.ts new file mode 100644 index 0000000000..1c0284335e --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/useDuplicateRepresentation.types.ts @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2025 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 { GQLErrorPayload, GQLMessage } from '@eclipse-sirius/sirius-components-core'; + +export interface UseDuplicateRepresentationValue { + duplicateRepresentation: (editingContextId: string, representationId: string) => void; + duplicatedRepresentationMetadata: GQLRepresentationMetadata | null; +} + +export interface GQLDuplicateRepresentationVariables { + input: GQLDuplicateRepresentationInput; +} + +export interface GQLDuplicateRepresentationInput { + id: string; + editingContextId: string; + representationId: string; +} + +export interface GQLDuplicateRepresentationData { + duplicateRepresentation: GQLDuplicateRepresentationPayload; +} + +export interface GQLDuplicateObjectSuccessPayload { + __typename: 'DuplicateRepresentationSuccessPayload'; + id: string | null; + representationMetadata: GQLRepresentationMetadata; + messages: GQLMessage[] | null; +} + +export interface GQLRepresentationMetadata { + id: string; + kind: string; +} + +export type GQLDuplicateRepresentationPayload = GQLErrorPayload | GQLDuplicateObjectSuccessPayload;