Skip to content

Commit

Permalink
[4048] Add the ability to Undo or Redo mutations
Browse files Browse the repository at this point in the history
Bug: #4048
Signed-off-by: Michaël Charfadi <[email protected]>
  • Loading branch information
mcharfadi committed Oct 4, 2024
1 parent 52b5b3c commit 5ddbdb7
Show file tree
Hide file tree
Showing 16 changed files with 611 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ The new endpoints are:
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/4048[#4048] [core] Add the ability to undo/redo a mutation on the the edit project view with ctrl+z or ctrl+y (restoring a deleted resource is not yet supported).

=== Improvements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ type Mutation {
deleteObject(input: DeleteObjectInput!): DeleteObjectPayload!
renameObject(input: RenameObjectInput!): RenameObjectPayload!
invokeEditingContextAction(input: InvokeEditingContextActionInput!): InvokeEditingContextActionPayload!
undo(input : UndoRedoInput!) : UndoPayload!
redo(input : UndoRedoInput!) : UndoPayload!
}

type Object {
Expand Down Expand Up @@ -229,6 +231,14 @@ input DeleteObjectInput {
objectId: ID!
}

input UndoRedoInput {
id: ID!
editingContextId: ID!
mutationId: ID!
}

union UndoPayload = ErrorPayload | SuccessPayload

union DeleteObjectPayload = ErrorPayload | SuccessPayload

input RenameObjectInput {
Expand Down
6 changes: 6 additions & 0 deletions packages/sirius-web/backend/sirius-web-application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@
<artifactId>sirius-components-view-emf</artifactId>
<version>2024.9.4</version>
</dependency>
<dependency>
<groupId>org.eclipse.emf</groupId>
<artifactId>org.eclipse.emf.ecore.change</artifactId>
<version>2.17.0</version>
<scope>compile</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@
*******************************************************************************/
package org.eclipse.sirius.web.application.editingcontext;

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

import org.eclipse.emf.ecore.change.ChangeDescription;
import org.eclipse.emf.ecore.change.util.ChangeRecorder;
import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain;
import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext;
import org.eclipse.sirius.components.representations.IRepresentationDescription;
Expand All @@ -36,9 +39,14 @@ public class EditingContext implements IEMFEditingContext {

private final List<View> views;

private final Map<String, ChangeDescription> changesDescription = new HashMap<>();

private final ChangeRecorder changeRecorder;

public EditingContext(String id, AdapterFactoryEditingDomain editingDomain, Map<String, IRepresentationDescription> representationDescriptions, List<View> views) {
this.id = Objects.requireNonNull(id);
this.editingDomain = Objects.requireNonNull(editingDomain);
this.changeRecorder = new ChangeRecorder(this.editingDomain.getResourceSet());
this.representationDescriptions = Objects.requireNonNull(representationDescriptions);
this.views = Objects.requireNonNull(views);
}
Expand All @@ -61,4 +69,12 @@ public List<View> getViews() {
return this.views;
}

public ChangeRecorder getChangeRecorder() {
return changeRecorder;
}

public Map<String, ChangeDescription> getChangesDescription() {
return changesDescription;
}

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

import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.schema.DataFetchingEnvironment;
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.components.graphql.api.IExceptionWrapper;
import org.eclipse.sirius.web.application.undo.dto.RedoInput;


import java.util.Objects;
import java.util.concurrent.CompletableFuture;

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

private static final String INPUT_ARGUMENT = "input";

private final ObjectMapper objectMapper;

private final IExceptionWrapper exceptionWrapper;

private final IEditingContextDispatcher editingContextDispatcher;

public RedoDataFetcher(ObjectMapper objectMapper, IExceptionWrapper exceptionWrapper, IEditingContextDispatcher editingContextDispatcher) {
this.objectMapper = Objects.requireNonNull(objectMapper);
this.exceptionWrapper = Objects.requireNonNull(exceptionWrapper);
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, RedoInput.class);

return this.exceptionWrapper.wrapMono(() -> this.editingContextDispatcher.dispatchMutation(input.editingContextId(), input), input).toFuture();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*******************************************************************************
* 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.undo.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.schema.DataFetchingEnvironment;
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.components.graphql.api.IExceptionWrapper;
import org.eclipse.sirius.web.application.undo.dto.UndoInput;


import java.util.Objects;
import java.util.concurrent.CompletableFuture;

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

private static final String INPUT_ARGUMENT = "input";

private final ObjectMapper objectMapper;

private final IExceptionWrapper exceptionWrapper;

private final IEditingContextDispatcher editingContextDispatcher;

public UndoDataFetcher(ObjectMapper objectMapper, IExceptionWrapper exceptionWrapper, IEditingContextDispatcher editingContextDispatcher) {
this.objectMapper = Objects.requireNonNull(objectMapper);
this.exceptionWrapper = Objects.requireNonNull(exceptionWrapper);
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, UndoInput.class);

return this.exceptionWrapper.wrapMono(() -> this.editingContextDispatcher.dispatchMutation(input.editingContextId(), input), input).toFuture();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*******************************************************************************
* 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.undo.dto;

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

import java.util.UUID;

/**
* The input for redo mutation.
*
* @author mcharfadi
*/
public record RedoInput(UUID id, String editingContextId, String mutationId) implements IInput {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*******************************************************************************
* 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.undo.dto;

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

import java.util.UUID;

/**
* The input for undo mutation.
*
* @author mcharfadi
*/
public record UndoInput(UUID id, String editingContextId, String mutationId) implements IInput {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*******************************************************************************
* 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.undo.handlers;

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.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.core.api.SuccessPayload;
import org.eclipse.sirius.web.application.editingcontext.EditingContext;
import org.eclipse.sirius.web.application.undo.dto.RedoInput;
import org.springframework.stereotype.Service;

import reactor.core.publisher.Sinks.Many;
import reactor.core.publisher.Sinks.One;

/**
* Handler used to redo mutations.
*
* @author mcharfadi
*/
@Service
public class RedoEventHandler implements IEditingContextEventHandler {

@Override
public boolean canHandle(IEditingContext editingContext, IInput input) {
return input instanceof RedoInput;
}

@Override
public void handle(One<IPayload> payloadSink, Many<ChangeDescription> changeDescriptionSink, IEditingContext editingContext, IInput input) {
IPayload payload = new ErrorPayload(input.id(), "Error ");
ChangeDescription changeDescription = new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, editingContext.getId(), input);
if (editingContext instanceof EditingContext siriusEditingContext && input instanceof RedoInput redoInput) {
var emfChangeDescription = siriusEditingContext.getChangesDescription().get(redoInput.mutationId());
if (emfChangeDescription != null) {
emfChangeDescription.applyAndReverse();
}
payload = new SuccessPayload(input.id());
}
payloadSink.tryEmitValue(payload);
changeDescriptionSink.tryEmitNext(changeDescription);
}

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

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.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.core.api.SuccessPayload;
import org.eclipse.sirius.web.application.editingcontext.EditingContext;
import org.eclipse.sirius.web.application.undo.dto.UndoInput;
import org.springframework.stereotype.Service;

import reactor.core.publisher.Sinks.Many;
import reactor.core.publisher.Sinks.One;

/**
* Handler used to undo mutations.
*
* @author mcharfadi
*/
@Service
public class UndoEventHandler implements IEditingContextEventHandler {

@Override
public boolean canHandle(IEditingContext editingContext, IInput input) {
return input instanceof UndoInput;
}

@Override
public void handle(One<IPayload> payloadSink, Many<ChangeDescription> changeDescriptionSink, IEditingContext editingContext, IInput input) {
IPayload payload = new ErrorPayload(input.id(), "Error ");
ChangeDescription changeDescription = new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, editingContext.getId(), input);
if (editingContext instanceof EditingContext siriusEditingContext && input instanceof UndoInput undoInput) {
var emfChangeDescription = siriusEditingContext.getChangesDescription().get(undoInput.mutationId());
if (emfChangeDescription != null) {
emfChangeDescription.applyAndReverse();
}
payload = new SuccessPayload(input.id());
}
payloadSink.tryEmitValue(payload);
changeDescriptionSink.tryEmitNext(changeDescription);
}

}
Loading

0 comments on commit 5ddbdb7

Please sign in to comment.