Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3740] Add support for object duplication from Explorer #3798

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ 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

=== Improvements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ Currently, it is not possible to duplicate an object from the Explorer.
== Key Result

The user can duplicate a semantic object with contextual menu on tree items in the explorer.
The user can choose
The user can choose

* the target container where the duplicated object will be added
* the target containment feature
* options to define how is duplicated the content of the duplicated object
Expand Down Expand Up @@ -72,4 +73,4 @@ Nothing identified

== No-gos

Nothing identified
Nothing identified
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*******************************************************************************
* 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 { isCreateProjectFromTemplateSuccessPayload } from '../../../support/server/createProjectFromTemplateCommand';
import { Project } from '../../../pages/Project';
import { Explorer } from '../../../workbench/Explorer';

describe('Explorer - duplicate object', () => {
let studioProjectId: string = '';
let domainName: string = '';
context('Given a studio with a domain', () => {
beforeEach(() => {
cy.createProjectFromTemplate('studio-template').then((res) => {
const payload = res.body.data.createProjectFromTemplate;
if (isCreateProjectFromTemplateSuccessPayload(payload)) {
const projectId = payload.project.id;
studioProjectId = projectId;

const project = new Project();
project.visit(projectId);

const explorer = new Explorer();
explorer.expandWithDoubleClick('DomainNewModel');
cy.get('[title="domain::Domain"]').then(($div) => {
domainName = $div.data().testid;
explorer.expandWithDoubleClick(domainName);
explorer.expandWithDoubleClick('Entity1');
explorer.expandWithDoubleClick('Entity2');
});
}
});
});

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

context('When we duplicate an object using the contextual menu', () => {
it('Then the object is twice in the explorer', () => {
const explorer = new Explorer();
explorer.getTreeItemByLabel('attribute2').should('have.length', 1);
cy.getByTestId('attribute2-more').click();
cy.getByTestId('duplicate-object').click();
cy.getByTestId('duplicate-object-tree').find(`[data-treeitemlabel="DomainNewModel"]`).dblclick();
cy.getByTestId('duplicate-object-tree').find(`[data-treeitemlabel="${domainName}"]`).dblclick();
cy.getByTestId('duplicate-object-tree').find(`[data-treeitemlabel="Entity2"]`).click();
cy.getByTestId('containment-feature-name')
.children('[role="combobox"]')
.invoke('text')
.should('eq', 'attributes');
cy.getByTestId('duplicate-object-button').click();
explorer.getTreeItemByLabel('attribute2').should('have.length', 2);
});
});

context('When we duplicate an object using the shortcut', () => {
it('Then the object is twice in the explorer', () => {
const explorer = new Explorer();
explorer.getTreeItemByLabel('attribute3').should('have.length', 1);
explorer.select('attribute3');
cy.getByTestId('attribute3').type('{ctrl}d');
cy.getByTestId('duplicate-object-tree').find(`[data-treeitemlabel="DomainNewModel"]`).dblclick();
cy.getByTestId('duplicate-object-tree').find(`[data-treeitemlabel="${domainName}"]`).dblclick();
cy.getByTestId('duplicate-object-tree').find(`[data-treeitemlabel="Entity2"]`).click();
cy.getByTestId('containment-feature-name')
.children('[role="combobox"]')
.invoke('text')
.should('eq', 'attributes');
cy.getByTestId('duplicate-object-button').click();
explorer.getTreeItemByLabel('attribute3').should('have.length', 2);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*******************************************************************************
* 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.components.emf.utils;

import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.util.EcoreUtil;

/**
* This class inherits from org.eclipse.emf.ecore.util.EcoreUtil.Copier to leverage protected methods.
*
* @author lfasani
*/
public class SiriusEMFCopier extends EcoreUtil.Copier {

/*
* This method copies the EObject with all its attributes but not its cross references nor its content.
* This code is partially copied from org.eclipse.emf.ecore.util.EcoreUtil.Copier.copy
* */
public EObject copyWithoutContent(EObject eObject) {
if (eObject == null) {
return null;
} else {
EObject copyEObject = this.createCopy(eObject);
if (copyEObject != null) {
this.put(eObject, copyEObject);
EClass eClass = eObject.eClass();
for (int i = 0, size = eClass.getFeatureCount(); i < size; ++i) {
EStructuralFeature eStructuralFeature = eClass.getEStructuralFeature(i);
if (eStructuralFeature.isChangeable() && !eStructuralFeature.isDerived()) {
if (eStructuralFeature instanceof EAttribute) {
this.copyAttribute((EAttribute) eStructuralFeature, eObject, copyEObject);
}
}
}

this.copyProxyURI(eObject, copyEObject);
}

return copyEObject;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*******************************************************************************
* 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.object;

import java.util.UUID;

import org.eclipse.sirius.components.core.api.IInput;

/**
* The input of the duplicate dialog tree event subscription.
*
* @author frouene
*/
public record DuplicateDialogTreeEventInput(UUID id, String editingContextId, String representationId) implements IInput {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*******************************************************************************
* 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.object;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import org.eclipse.sirius.components.annotations.spring.graphql.SubscriptionDataFetcher;
import org.eclipse.sirius.components.core.api.IPayload;
import org.eclipse.sirius.components.graphql.api.IDataFetcherWithFieldCoordinates;
import org.eclipse.sirius.components.graphql.api.IEventProcessorSubscriptionProvider;
import org.eclipse.sirius.components.graphql.api.IExceptionWrapper;
import org.eclipse.sirius.components.graphql.api.LocalContextConstants;
import org.reactivestreams.Publisher;

import graphql.execution.DataFetcherResult;
import graphql.schema.DataFetchingEnvironment;

/**
* The data fetcher used to send the refreshed tree to a duplicate dialog subscription .
*
* @author frouene
*/
@SubscriptionDataFetcher(type = "Subscription", field = "duplicateDialogTreeEvent")
public class SubscriptionDuplicateDialogTreeEventDataFetcher implements IDataFetcherWithFieldCoordinates<Publisher<DataFetcherResult<IPayload>>> {

private static final String INPUT_ARGUMENT = "input";

private final ObjectMapper objectMapper;

private final IExceptionWrapper exceptionWrapper;

private final IEventProcessorSubscriptionProvider eventProcessorSubscriptionProvider;

public SubscriptionDuplicateDialogTreeEventDataFetcher(ObjectMapper objectMapper, IExceptionWrapper exceptionWrapper, IEventProcessorSubscriptionProvider eventProcessorSubscriptionProvider) {
this.objectMapper = Objects.requireNonNull(objectMapper);
this.exceptionWrapper = Objects.requireNonNull(exceptionWrapper);
this.eventProcessorSubscriptionProvider = Objects.requireNonNull(eventProcessorSubscriptionProvider);
}

@Override
public Publisher<DataFetcherResult<IPayload>> get(DataFetchingEnvironment environment) throws Exception {
Object argument = environment.getArgument(INPUT_ARGUMENT);
var input = this.objectMapper.convertValue(argument, DuplicateDialogTreeEventInput.class);

Map<String, Object> localContext = new HashMap<>();
localContext.put(LocalContextConstants.EDITING_CONTEXT_ID, input.editingContextId());
localContext.put(LocalContextConstants.REPRESENTATION_ID, input.representationId());

return this.exceptionWrapper.wrapFlux(() -> this.eventProcessorSubscriptionProvider.getSubscription(input.editingContextId(), input.representationId(), input), input)
.map(payload -> DataFetcherResult.<IPayload>newResult()
.data(payload)
.localContext(localContext)
.build());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*******************************************************************************
* 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 java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

import org.eclipse.sirius.components.annotations.spring.graphql.QueryDataFetcher;
import org.eclipse.sirius.components.collaborative.api.IEditingContextEventProcessorRegistry;
import org.eclipse.sirius.components.core.api.IPayload;
import org.eclipse.sirius.components.graphql.api.IDataFetcherWithFieldCoordinates;
import org.eclipse.sirius.web.application.views.explorer.dto.EditingContextContainmentFeatureNamesInput;

import graphql.schema.DataFetchingEnvironment;

/**
* Data fetcher for the field EditingContext#containmentFeatureNames.
* It is used to find the containment feature names of a container object given the candidate contained object.
*
* @author lfasani
*/
@QueryDataFetcher(type = "EditingContext", field = "containmentFeatureNames")
public class EditingContextContainmentFeatureNamesDataFetcher implements IDataFetcherWithFieldCoordinates<CompletableFuture<IPayload>> {
private static final String CONTAINER_ID = "containerId";

private static final String CONTAINED_OBJECT_ID = "containedObjectId";

private final IEditingContextEventProcessorRegistry editingContextEventProcessorRegistry;

public EditingContextContainmentFeatureNamesDataFetcher(IEditingContextEventProcessorRegistry editingContextEventProcessorRegistry) {
this.editingContextEventProcessorRegistry = Objects.requireNonNull(editingContextEventProcessorRegistry);
}

@Override
public CompletableFuture<IPayload> get(DataFetchingEnvironment environment) throws Exception {
String editingContextId = environment.getSource();
String containerId = environment.getArgument(CONTAINER_ID);
String containedObjectId = environment.getArgument(CONTAINED_OBJECT_ID);

EditingContextContainmentFeatureNamesInput input = new EditingContextContainmentFeatureNamesInput(UUID.randomUUID(), editingContextId, containerId, containedObjectId);
return this.editingContextEventProcessorRegistry.dispatchEvent(editingContextId, input)
.toFuture();
}
}
Original file line number Diff line number Diff line change
@@ -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.DuplicateObjectInput;

import graphql.schema.DataFetchingEnvironment;

/**
* Data fetcher for the field Mutation#duplicateObject.
*
* @author lfasani
*/
@MutationDataFetcher(type = "Mutation", field = "duplicateObject")
public class MutationDuplicateObjectDataFetcher implements IDataFetcherWithFieldCoordinates<CompletableFuture<IPayload>> {

private static final String INPUT_ARGUMENT = "input";

private final ObjectMapper objectMapper;

private final IEditingContextDispatcher editingContextDispatcher;

public MutationDuplicateObjectDataFetcher(ObjectMapper objectMapper, IEditingContextDispatcher editingContextDispatcher) {
this.objectMapper = Objects.requireNonNull(objectMapper);
this.editingContextDispatcher = Objects.requireNonNull(editingContextDispatcher);
}

@Override
public CompletableFuture<IPayload> get(DataFetchingEnvironment environment) throws Exception {
Object argument = environment.getArgument(INPUT_ARGUMENT);
var input = this.objectMapper.convertValue(argument, DuplicateObjectInput.class);

return this.editingContextDispatcher.dispatchMutation(input.editingContextId(), input).toFuture();
}
}
Loading
Loading