diff --git a/backend/plugins/com.eclipsesource.workflow.glsp.server/src/main/java/com/eclipsesource/workflow/glsp/server/handler/operation/DeleteOperationHandler.java b/backend/plugins/com.eclipsesource.workflow.glsp.server/src/main/java/com/eclipsesource/workflow/glsp/server/handler/operation/DeleteOperationHandler.java index d8512f75..721fbcb2 100644 --- a/backend/plugins/com.eclipsesource.workflow.glsp.server/src/main/java/com/eclipsesource/workflow/glsp/server/handler/operation/DeleteOperationHandler.java +++ b/backend/plugins/com.eclipsesource.workflow.glsp.server/src/main/java/com/eclipsesource/workflow/glsp/server/handler/operation/DeleteOperationHandler.java @@ -10,12 +10,22 @@ ******************************************************************************/ package com.eclipsesource.workflow.glsp.server.handler.operation; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; +import java.util.List; import java.util.Optional; import java.util.Set; +import org.eclipse.emf.common.command.Command; +import org.eclipse.emf.common.command.CompoundCommand; +import org.eclipse.emf.common.util.EList; import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.emf.edit.command.CommandParameter; +import org.eclipse.emf.edit.command.RemoveCommand; import org.eclipse.emfcloud.modelserver.coffee.model.coffee.Flow; import org.eclipse.emfcloud.modelserver.coffee.model.coffee.Node; import org.eclipse.glsp.api.model.GraphicalModelState; @@ -23,22 +33,73 @@ import org.eclipse.glsp.graph.GEdge; import org.eclipse.glsp.graph.GModelElement; import org.eclipse.glsp.graph.GNode; -import org.eclipse.glsp.server.operationhandler.BasicOperationHandler; import com.eclipsesource.workflow.glsp.server.model.WorkflowModelServerAccess; import com.eclipsesource.workflow.glsp.server.model.WorkflowModelState; import com.eclipsesource.workflow.glsp.server.wfnotation.DiagramElement; import com.eclipsesource.workflow.glsp.server.wfnotation.Shape; -public class DeleteOperationHandler extends BasicOperationHandler { +public class DeleteOperationHandler extends ModelServerAwareBasicOperationHandler { - private Set toDelete; + private Set toDeleteNodes; + private Set toDeleteEdges; + private Set toDeleteLocal; @Override - public void executeOperation(DeleteOperation operation, GraphicalModelState modelState) { - toDelete = new HashSet<>(); + public void executeOperation(DeleteOperation operation, GraphicalModelState modelState, + WorkflowModelServerAccess modelAccess) throws Exception { + toDeleteNodes = new HashSet<>(); + toDeleteEdges = new HashSet<>(); + toDeleteLocal = new HashSet<>(); operation.getElementIds().forEach(id -> collectElementsToDelete(id, modelState)); - toDelete.forEach(e -> EcoreUtil.delete(e, true)); + toDeleteLocal.forEach(e -> EcoreUtil.delete(e, true)); + + List deleteEdges = delete(toDeleteEdges, modelAccess); + List deleteNodes = delete(toDeleteNodes, modelAccess); + List unifiedToDelete = new ArrayList<>(); + Comparator sortByIndex = new Comparator() { + + @Override + public int compare(Command o1, Command o2) { + if (!(o1 instanceof RemoveCommand)) + return 1; + if (!(o2 instanceof RemoveCommand)) + return -1; + RemoveCommand rc1 = (RemoveCommand) o1; + RemoveCommand rc2 = (RemoveCommand) o2; + CommandParameter.Indices cp2 = (CommandParameter.Indices) rc2.getCollection().iterator().next(); + CommandParameter.Indices cp1 = (CommandParameter.Indices) rc1.getCollection().iterator().next(); + return cp2.getIndices()[0] - cp1.getIndices()[0]; + + } + }; + // need to sort as otherwise the index is not correct when commands are applied. + Collections.sort(deleteEdges, sortByIndex); + Collections.sort(deleteNodes, sortByIndex); + unifiedToDelete.addAll(deleteEdges); + unifiedToDelete.addAll(deleteNodes); + + CompoundCommand cc = new CompoundCommand(unifiedToDelete); + if (!modelAccess.edit(cc).thenApply(res -> res.body()).get()) { + throw new IllegalAccessError("Could not execute command: " + cc); + } + } + + @SuppressWarnings("unchecked") + private List delete(Set eObjects, final WorkflowModelServerAccess modelAccess) { + List result = new ArrayList<>(); + for (EObject e : eObjects) { + EObject container = e.eContainer(); + EStructuralFeature containingFeature = e.eContainingFeature(); + // use index as object id cannot be used due to glsp -> modelserver -> glsp + // communication + int index = ((EList) container.eGet(containingFeature)).indexOf(e); + Command removeEdgesCommand = RemoveCommand.create(modelAccess.getEditingDomain(), container, + containingFeature, index); + result.add(removeEdgesCommand); + + } + return result; } protected void collectElementsToDelete(String id, GraphicalModelState modelState) { @@ -55,11 +116,11 @@ protected void collectElementsToDelete(String id, GraphicalModelState modelState WorkflowModelServerAccess modelAccess = WorkflowModelState.getModelAccess(modelState); if (element instanceof GNode) { Node node = modelAccess.getNodeById(element.getId()); - toDelete.add(node); + toDeleteNodes.add(node); Optional diagramElement = modelAccess.getWorkflowFacade().findDiagramElement(node); if (!diagramElement.isEmpty() && diagramElement.get() instanceof Shape) { - toDelete.add(diagramElement.get()); + toDeleteLocal.add(diagramElement.get()); } modelState.getIndex().getIncomingEdges(element) @@ -74,14 +135,14 @@ protected void collectElementsToDelete(String id, GraphicalModelState modelState if (maybeFlow.isEmpty()) { return; } - toDelete.add(maybeFlow.get()); + toDeleteEdges.add(maybeFlow.get()); Optional edge = maybeFlow .flatMap(flow -> modelAccess.getWorkflowFacade().findDiagramElement(flow)); if (edge.isEmpty()) { return; } - toDelete.add(edge.get()); + toDeleteLocal.add(edge.get()); } } diff --git a/web/coffee-editor-extension/src/browser/coffee-tree/coffee-tree-editor-widget.tsx b/web/coffee-editor-extension/src/browser/coffee-tree/coffee-tree-editor-widget.tsx index 64edf068..ae90246d 100644 --- a/web/coffee-editor-extension/src/browser/coffee-tree/coffee-tree-editor-widget.tsx +++ b/web/coffee-editor-extension/src/browser/coffee-tree/coffee-tree-editor-widget.tsx @@ -13,7 +13,7 @@ import { ModelServerClient, ModelServerCommand, ModelServerCommandUtil, - ModelServerReferenceDescription + ModelServerReferenceDescription, } from '@eclipse-emfcloud/modelserver-theia/lib/common'; import { AddCommandProperty, @@ -22,7 +22,7 @@ import { MasterTreeWidget, NavigatableTreeEditorOptions, NavigatableTreeEditorWidget, - TreeEditor + TreeEditor, } from '@eclipse-emfcloud/theia-tree-editor'; import { Title, TreeNode, Widget } from '@theia/core/lib/browser'; import { ILogger } from '@theia/core/lib/common'; @@ -32,6 +32,9 @@ import { clone, isEqual } from 'lodash'; import { CoffeeModel } from './coffee-model'; +const sortByIndex = (a: ModelServerCommand, b: ModelServerCommand) => + b.indices[0] - a.indices[0]; + @injectable() export class CoffeeTreeEditorWidget extends NavigatableTreeEditorWidget { private delayedRefresh = false; @@ -75,111 +78,119 @@ export class CoffeeTreeEditorWidget extends NavigatableTreeEditorWidget { }); this.subscriptionService.onIncrementalUpdateListener(incrementalUpdate => { const command = incrementalUpdate as ModelServerCommand; - // the #/ marks the beginning of the actual path, but we also want the first slash removed so +3 - const ownerPropIndexPath = command.owner.$ref - .substring(command.owner.$ref.indexOf('#/') + 3) - .split('/') - .filter(v => v.length !== 0) - .map(path => { - const indexSplitPos = path.indexOf('.'); - // each property starts with an @ so we ignore it - return { - property: path.substring(1, indexSplitPos), - index: path.substring(indexSplitPos + 1) - }; - }); - let ownerNode; - if (ownerPropIndexPath.length !== 0) { - ownerNode = this.treeWidget.findNode(ownerPropIndexPath); - } else { - // TODO should be done in findNode - ownerNode = (this.treeWidget.model.root as TreeEditor.RootNode) - .children[0]; - } - const objectToModify = - ownerPropIndexPath.length === 0 - ? this.instanceData - : ownerPropIndexPath.reduce( - (data, path) => - path.index === undefined - ? data[path.property] - : data[path.property][path.index], - this.instanceData - ); - switch (command.type) { - case 'add': { - if (!objectToModify[command.feature]) { - objectToModify[command.feature] = []; - } - objectToModify[command.feature].push(...command.objectsToAdd); - this.treeWidget.addChildren( - ownerNode, - command.objectsToAdd, - command.feature - ); - if (!this.isVisible) { - this.delayedRefresh = true; - } - break; - } - case 'remove': { - command.indices.forEach(i => - objectToModify[command.feature].splice(i, 1) - ); - this.treeWidget.removeChildren( - ownerNode, - command.indices, - command.feature - ); + if (command.commands !== undefined) { + command.commands.forEach(c => this.handleCommand(c)); + } else { + this.handleCommand(command); + } + }); + this.modelServerApi.get(this.getModelIDToRequest()).then(response => { + if (response.statusCode === 200) { + if (isEqual(this.instanceData, response.body)) { + return; + } + this.instanceData = response.body; + this.treeWidget + .setData({ error: false, data: this.instanceData }) + .then(() => this.treeWidget.selectFirst()); + return; + } + this.treeWidget.setData({ error: !!response.statusMessage }); + this.renderError( + "An error occurred when requesting '" + + this.getModelIDToRequest() + + "' - Status " + + response.statusCode + + ' ' + + response.statusMessage + ); + this.instanceData = undefined; + return; + }); + this.modelServerApi.subscribe(this.getModelIDToRequest()); + // see https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload + window.onbeforeunload = () => this.dispose(); + } + private handleCommand(command: ModelServerCommand) { + // the #/ marks the beginning of the actual path, but we also want the first slash removed so +3 + const ownerPropIndexPath = command.owner.$ref + .substring(command.owner.$ref.indexOf('#/') + 3) + .split('/') + .filter(v => v.length !== 0) + .map(path => { + const indexSplitPos = path.indexOf('.'); + // each property starts with an @ so we ignore it + return { + property: path.substring(1, indexSplitPos), + index: path.substring(indexSplitPos + 1) + }; + }); + let ownerNode; + if (ownerPropIndexPath.length !== 0) { + ownerNode = this.treeWidget.findNode(ownerPropIndexPath); + } else { + // TODO should be done in findNode + ownerNode = (this.treeWidget.model.root as TreeEditor.RootNode) + .children[0]; + } + const objectToModify = + ownerPropIndexPath.length === 0 + ? this.instanceData + : ownerPropIndexPath.reduce( + (data, path) => + path.index === undefined + ? data[path.property] + : data[path.property][path.index], + this.instanceData + ); + switch (command.type) { + case 'add': { + if (!objectToModify[command.feature]) { + objectToModify[command.feature] = []; + } + objectToModify[command.feature].push(...command.objectsToAdd); + this.treeWidget.addChildren( + ownerNode, + command.objectsToAdd, + command.feature + ); if (!this.isVisible) { this.delayedRefresh = true; } - break; - } - case 'set': { - // maybe we can directly manipulate the data? - const data = clone(ownerNode.jsonforms.data); - // FIXME handle array changes - if (command.dataValues) { - data[command.feature] = command.dataValues[0]; - } else { - data[command.feature] = command.objectsToAdd[0]; - } - this.treeWidget.updateDataForNode(ownerNode, data); + break; + } + case 'remove': { + command.indices.forEach(i => + objectToModify[command.feature].splice(i, 1) + ); + this.treeWidget.removeChildren( + ownerNode, + command.indices, + command.feature + ); if (!this.isVisible) { this.delayedRefresh = true; } + break; + } + case 'set': { + // maybe we can directly manipulate the data? + const data = clone(ownerNode.jsonforms.data); + // FIXME handle array changes + if (command.dataValues) { + data[command.feature] = command.dataValues[0]; + } else { + data[command.feature] = command.objectsToAdd[0]; + } + this.treeWidget.updateDataForNode(ownerNode, data); + if (!this.isVisible) { + this.delayedRefresh = true; + } break; - } + } default: { /** */} - } - }); - this.modelServerApi.get(this.getModelIDToRequest()).then(response => { - if (response.statusCode === 200) { - if (isEqual(this.instanceData, response.body)) { - return; - } - this.instanceData = response.body; - this.treeWidget - .setData({ error: false, data: this.instanceData }) - .then(() => this.treeWidget.selectFirst()); - return; - } - this.treeWidget.setData({ error: !!response.statusMessage }); - this.renderError( - "An error occurred when requesting '" + - this.getModelIDToRequest() + - "' - Status " + - response.statusCode + - ' ' + - response.statusMessage - ); - this.instanceData = undefined; - return; - }); - this.modelServerApi.subscribe(this.getModelIDToRequest()); - // see https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload - window.onbeforeunload = () => this.dispose(); + } + } private getOldSelectedPath(): string[] { const paths: string[] = []; @@ -206,12 +217,68 @@ export class CoffeeTreeEditorWidget extends NavigatableTreeEditorWidget { } protected deleteNode(node: Readonly): void { + const elements = this.collectElementsToDelete(node); + const compoundCommand = { + eClass: + 'http://www.eclipsesource.com/schema/2019/modelserver/command#//CompoundCommand', + type: 'compound', + commands: [] + }; + let edges = []; + let nodes = []; + elements.edges.forEach(e => { const removeCommand = ModelServerCommandUtil.createRemoveCommand( - this.getNodeDescription(node.parent as TreeEditor.Node), - node.jsonforms.property, - node.jsonforms.index ? [Number(node.jsonforms.index)] : [] + this.getNodeDescription(e.parent as TreeEditor.Node), + e.jsonforms.property, + e.jsonforms.index ? [Number(e.jsonforms.index)] : [] + ); + edges.push(removeCommand); + }); + elements.nodes.forEach(e => { + const removeCommand = ModelServerCommandUtil.createRemoveCommand( + this.getNodeDescription(e.parent as TreeEditor.Node), + e.jsonforms.property, + e.jsonforms.index ? [Number(e.jsonforms.index)] : [] + ); + nodes.push(removeCommand); + }); + edges = edges.sort(sortByIndex); + nodes = nodes.sort(sortByIndex); + compoundCommand.commands.push(...edges); + compoundCommand.commands.push(...nodes); + this.modelServerApi.edit( + this.getModelIDToRequest(), + compoundCommand as ModelServerCommand + ); + } + private collectElementsToDelete( + node: Readonly + ): { nodes: TreeEditor.Node[]; edges: TreeEditor.Node[] } { + const result = { nodes: [], edges: [] }; + switch (node.jsonforms.type) { + case CoffeeModel.Type.AutomaticTask: + case CoffeeModel.Type.ManualTask: + result.nodes.push(node); + result.edges.push(...this.findEdges(node)); + break; + case CoffeeModel.Type.WeightedFlow: + case CoffeeModel.Type.Flow: + result.edges.push(node); + break; + } + return result; + } + private findEdges(node: Readonly): TreeEditor.Node[] { + const parent = node.parent as TreeEditor.Node; + const flows: any[] = parent.children.filter( + c => (c as TreeEditor.Node).jsonforms.property === 'flows' + ); + const ref = `//@workflows.0/@nodes.${node.jsonforms.index}`; + return flows.filter( + f => + f.jsonforms.data.source.$ref === ref || + f.jsonforms.data.target.$ref === ref ); - this.modelServerApi.edit(this.getModelIDToRequest(), removeCommand); } protected addNode({ node, type, property }: AddCommandProperty): void { const addCommand = ModelServerCommandUtil.createAddCommand(