diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index d66ad0b53c..b2b0792dd3 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -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 diff --git a/packages/core/backend/sirius-components-collaborative/src/main/resources/schema/core.graphqls b/packages/core/backend/sirius-components-collaborative/src/main/resources/schema/core.graphqls index 5cfc39e512..458e414d30 100644 --- a/packages/core/backend/sirius-components-collaborative/src/main/resources/schema/core.graphqls +++ b/packages/core/backend/sirius-components-collaborative/src/main/resources/schema/core.graphqls @@ -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 { @@ -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 { diff --git a/packages/sirius-web/backend/sirius-web-application/pom.xml b/packages/sirius-web/backend/sirius-web-application/pom.xml index b39e588dd4..3906148bd2 100644 --- a/packages/sirius-web/backend/sirius-web-application/pom.xml +++ b/packages/sirius-web/backend/sirius-web-application/pom.xml @@ -204,6 +204,12 @@ sirius-components-view-emf 2024.9.4 + + org.eclipse.emf + org.eclipse.emf.ecore.change + 2.17.0 + compile + diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/EditingContext.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/EditingContext.java index 6ebef30f0c..9fc721b85d 100644 --- a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/EditingContext.java +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/editingcontext/EditingContext.java @@ -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; @@ -36,9 +39,14 @@ public class EditingContext implements IEMFEditingContext { private final List views; + private final Map changesDescription = new HashMap<>(); + + private final ChangeRecorder changeRecorder; + public EditingContext(String id, AdapterFactoryEditingDomain editingDomain, Map representationDescriptions, List 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); } @@ -61,4 +69,12 @@ public List getViews() { return this.views; } + public ChangeRecorder getChangeRecorder() { + return changeRecorder; + } + + public Map getChangesDescription() { + return changesDescription; + } + } diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/controller/RedoDataFetcher.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/controller/RedoDataFetcher.java new file mode 100644 index 0000000000..10b9379f7c --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/controller/RedoDataFetcher.java @@ -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> { + + 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 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(); + } +} \ No newline at end of file diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/controller/UndoDataFetcher.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/controller/UndoDataFetcher.java new file mode 100644 index 0000000000..c76566d4d0 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/controller/UndoDataFetcher.java @@ -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> { + + 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 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(); + } +} \ No newline at end of file diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/dto/RedoInput.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/dto/RedoInput.java new file mode 100644 index 0000000000..26d8c9ddad --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/dto/RedoInput.java @@ -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 { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/dto/UndoInput.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/dto/UndoInput.java new file mode 100644 index 0000000000..990c0f3974 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/dto/UndoInput.java @@ -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 { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/handlers/RedoEventHandler.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/handlers/RedoEventHandler.java new file mode 100644 index 0000000000..da278577d6 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/handlers/RedoEventHandler.java @@ -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 payloadSink, Many 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); + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/handlers/UndoEventHandler.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/handlers/UndoEventHandler.java new file mode 100644 index 0000000000..f33d3c3b7b --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/handlers/UndoEventHandler.java @@ -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 payloadSink, Many 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); + } + +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/services/UndoRedoMutationsPrePostProcess.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/services/UndoRedoMutationsPrePostProcess.java new file mode 100644 index 0000000000..df4d5fa831 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/undo/services/UndoRedoMutationsPrePostProcess.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * 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.services; + +import org.eclipse.sirius.components.collaborative.api.ChangeDescription; +import org.eclipse.sirius.components.collaborative.api.IInputPostProcessor; +import org.eclipse.sirius.components.collaborative.api.IInputPreProcessor; +import org.eclipse.sirius.components.collaborative.diagrams.dto.LayoutDiagramInput; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IInput; +import org.eclipse.sirius.web.application.editingcontext.EditingContext; +import org.eclipse.sirius.web.application.undo.dto.RedoInput; +import org.eclipse.sirius.web.application.undo.dto.UndoInput; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Sinks; + +/** + * Used to save mutations id. + * + * @author mcharfadi + */ +@Service +public class UndoRedoMutationsPrePostProcess implements IInputPreProcessor, IInputPostProcessor { + + private boolean canHandle(IInput input) { + return !(input instanceof UndoInput || input instanceof RedoInput || input instanceof LayoutDiagramInput); + } + + @Override + public IInput preProcess(IEditingContext editingContext, IInput input, Sinks.Many changeDescriptionSink) { + if (editingContext instanceof EditingContext siriusEditingContext && canHandle(input)) { + siriusEditingContext.getChangeRecorder().beginRecording(siriusEditingContext.getDomain().getResourceSet().getResources()); + } + return input; + } + + @Override + public void postProcess(IEditingContext editingContext, IInput input, Sinks.Many changeDescriptionSink) { + if (editingContext instanceof EditingContext siriusEditingContext && canHandle(input)) { + var changeDescription = siriusEditingContext.getChangeRecorder().summarize(); + siriusEditingContext.getChangesDescription().put(input.id().toString(), changeDescription); + siriusEditingContext.getChangeRecorder().endRecording(); + } + + } +} 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 7c0c7a9ee1..5319822cba 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 @@ -51,6 +51,7 @@ import LinkIcon from '@mui/icons-material/Link'; import MenuIcon from '@mui/icons-material/Menu'; import WarningIcon from '@mui/icons-material/Warning'; import { DiagramFilter } from '../diagrams/DiagramFilter'; +import { OperationCountLink } from '../graphql/ApolloLinkMutationsStack'; import { ApolloClientOptionsConfigurer } from '../graphql/useCreateApolloClient.types'; import { apolloClientOptionsConfigurersExtensionPoint } from '../graphql/useCreateApolloClientExtensionPoints'; import { OnboardArea } from '../onboarding/OnboardArea'; @@ -251,6 +252,7 @@ const nodesApolloClientOptionsConfigurer: ApolloClientOptionsConfigurer = (curre return { ...currentOptions, documentTransform: newDocumentTransform, + link: new OperationCountLink().concat(currentOptions.link), }; }; diff --git a/packages/sirius-web/frontend/sirius-web-application/src/graphql/ApolloLinkMutationsStack.tsx b/packages/sirius-web/frontend/sirius-web-application/src/graphql/ApolloLinkMutationsStack.tsx new file mode 100644 index 0000000000..e160aaee98 --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/graphql/ApolloLinkMutationsStack.tsx @@ -0,0 +1,40 @@ +/******************************************************************************* + * 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 { ApolloLink, Operation } from '@apollo/client'; +import { Kind, OperationTypeNode } from 'graphql/language'; + +export class OperationCountLink extends ApolloLink { + constructor() { + super(); + } + override request(operation: Operation, forward) { + if ( + operation.query.definitions[0].kind === Kind.OPERATION_DEFINITION && + operation.query.definitions[0].operation === OperationTypeNode.MUTATION && + operation.variables.input.id && + !( + operation.operationName === 'undo' || + operation.operationName === 'redo' || + operation.operationName === 'layoutDiagram' + ) + ) { + var storedUndoStack = sessionStorage.getItem('undoStack'); + var undoStack = JSON.parse(storedUndoStack); + + sessionStorage.setItem('undoStack', JSON.stringify([operation.variables.input.id, ...undoStack])); + sessionStorage.setItem('redoStack', JSON.stringify([])); + } + + return forward(operation); + } +} diff --git a/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/EditProjectView.tsx b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/EditProjectView.tsx index c8a90a90eb..a91238db46 100644 --- a/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/EditProjectView.tsx +++ b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/EditProjectView.tsx @@ -42,6 +42,7 @@ import { import { ProjectContext } from './ProjectContext'; import { NewDocumentModalContribution } from './TreeToolBarContributions/NewDocumentModalContribution'; import { UploadDocumentModalContribution } from './TreeToolBarContributions/UploadDocumentModalContribution'; +import { UndoRedo } from './UndoRedo'; import { useProjectAndRepresentationMetadata } from './useProjectAndRepresentationMetadata'; const useEditProjectViewStyles = makeStyles()((_) => ({ @@ -123,12 +124,14 @@ export const EditProjectView = () => { - + + + diff --git a/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/UndoRedo.tsx b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/UndoRedo.tsx new file mode 100644 index 0000000000..64214a41d8 --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/UndoRedo.tsx @@ -0,0 +1,153 @@ +/******************************************************************************* + * 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 { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { EditProjectViewParams } from './EditProjectView.types'; +import { + GQLRedoData, + GQLSuccessPayload, + GQLUndoData, + GQLUndoRedoInput, + GQLUndoRedoItemPayload, + GQLUndoVariables, +} from './UndoRedo.types'; + +const undoMutation = gql` + mutation undo($input: UndoRedoInput!) { + undo(input: $input) { + __typename + ... on ErrorPayload { + message + } + } + } +`; + +const redoMutation = gql` + mutation redo($input: UndoRedoInput!) { + redo(input: $input) { + __typename + ... on ErrorPayload { + message + } + } + } +`; + +const isSuccessPayload = (payload: GQLUndoRedoItemPayload): payload is GQLSuccessPayload => + payload.__typename === 'SuccessPayload'; + +export const UndoRedo = ({ children }: { children: React.ReactNode }) => { + const [undo, { data: undoData }] = useMutation(undoMutation); + const [redo, { data: redoData }] = useMutation(redoMutation); + const { projectId } = useParams(); + + useEffect(() => { + sessionStorage.setItem('undoStack', JSON.stringify([])); + sessionStorage.setItem('redoStack', JSON.stringify([])); + }, []); + + const undoLastAction = () => { + var storedArray = sessionStorage.getItem('undoStack'); + if (storedArray) { + var arr = JSON.parse(storedArray); + if (arr[0]) { + const input: GQLUndoRedoInput = { + id: crypto.randomUUID(), + editingContextId: projectId, + mutationId: arr[0], + }; + undo({ variables: { input } }); + } + } + }; + + const redoLastAction = () => { + var storedArray = sessionStorage.getItem('redoStack'); + if (storedArray) { + var arr = JSON.parse(storedArray); + if (arr[0]) { + const input: GQLUndoRedoInput = { + id: crypto.randomUUID(), + editingContextId: projectId, + mutationId: arr[0], + }; + redo({ variables: { input } }); + } + } + }; + + useEffect(() => { + if (undoData) { + const { undo } = undoData; + if (isSuccessPayload(undo)) { + var storedUndoStack = sessionStorage.getItem('undoStack'); + var storedRedoStack = sessionStorage.getItem('redoStack'); + + //Remove first element of undo stack + var undoStack = JSON.parse(storedUndoStack); + var lastElement = undoStack.shift(); + sessionStorage.setItem('undoStack', JSON.stringify(undoStack)); + + //Put the element in the 1st position of the redo stack + var redoStack = JSON.parse(storedRedoStack); + sessionStorage.setItem('redoStack', JSON.stringify([lastElement, ...redoStack])); + } + } + }, [undoData]); + + useEffect(() => { + if (redoData) { + const { redo } = redoData; + if (isSuccessPayload(redo)) { + var storedUndoStack = sessionStorage.getItem('undoStack'); + var storedRedoStack = sessionStorage.getItem('redoStack'); + + //Remove first element of redo stack + var redoStack = JSON.parse(storedRedoStack); + var lastElement = redoStack.shift(); + sessionStorage.setItem('redoStack', JSON.stringify(redoStack)); + + //Put the element in the 1st position of the undo stack + var undoStack = JSON.parse(storedUndoStack); + sessionStorage.setItem('undoStack', JSON.stringify([lastElement, ...undoStack])); + } + } + }, [redoData]); + + const undoKeyPressHandler = (e) => { + if (e.ctrlKey && e.key === 'z') { + undoLastAction(); + } + }; + + const redoKeyPressHandler = (e) => { + if (e.ctrlKey && e.key === 'y') { + redoLastAction(); + } + }; + + useEffect(() => { + window.addEventListener('keydown', undoKeyPressHandler); + return () => window.removeEventListener('keydown', undoKeyPressHandler); + }, []); + + useEffect(() => { + window.addEventListener('keydown', redoKeyPressHandler); + return () => window.removeEventListener('keydown', redoKeyPressHandler); + }, []); + + return children; +}; diff --git a/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/UndoRedo.types.ts b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/UndoRedo.types.ts new file mode 100644 index 0000000000..6cf5bc7d30 --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/UndoRedo.types.ts @@ -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 + *******************************************************************************/ +export interface GQLUndoVariables { + input: GQLUndoRedoInput; +} + +export interface GQLUndoRedoInput { + id: string; + editingContextId: string; + mutationId: string; +} +export interface GQLUndoData { + undo: GQLUndoRedoItemPayload; +} + +export interface GQLRedoData { + redo: GQLUndoRedoItemPayload; +} + +export interface GQLUndoRedoItemPayload { + __typename: string; +} + +export interface GQLSuccessPayload { + __typename: 'SuccessPayload'; +}