From e6102f841fe3292f77680188414fb7c4b5a994da Mon Sep 17 00:00:00 2001 From: Haydar Metin Date: Mon, 29 Jun 2026 14:32:16 +0200 Subject: [PATCH] Add change container behavior --- .../change-container-workflow-handler.ts | 45 +++++++++++ .../src/common/workflow-diagram-module.ts | 3 + .../change-container-operation-handler.ts | 81 +++++++++++++++++++ .../common/gmodel/gmodel-diagram-module.ts | 5 +- packages/server/src/common/index.ts | 1 + .../server/src/common/protocol/glsp-server.ts | 12 ++- 6 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 examples/workflow-server/src/common/handler/change-container-workflow-handler.ts create mode 100644 packages/server/src/common/gmodel/change-container-operation-handler.ts diff --git a/examples/workflow-server/src/common/handler/change-container-workflow-handler.ts b/examples/workflow-server/src/common/handler/change-container-workflow-handler.ts new file mode 100644 index 00000000..880fea9e --- /dev/null +++ b/examples/workflow-server/src/common/handler/change-container-workflow-handler.ts @@ -0,0 +1,45 @@ +/******************************************************************************** + * Copyright (c) 2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { ChangeContainerOperation, GCompartment, GModelChangeContainerOperationHandler, GModelElement } from '@eclipse-glsp/server'; +import { injectable } from 'inversify'; +import { Category } from '../graph-extension'; +import { ModelTypes } from '../util/model-types'; + +/** + * Workflow-specific {@link GModelChangeContainerOperationHandler} that redirects reparenting into a + * {@link Category}'s structure compartment. The client references the outer {@link Category} node as + * container, so the actual child container (the `struct` compartment) has to be resolved on the server side. + */ +@injectable() +export class ChangeContainerWorkflowHandler extends GModelChangeContainerOperationHandler { + override getContainer(operation: ChangeContainerOperation): GModelElement | undefined { + const container = super.getContainer(operation); + if (container instanceof Category) { + const structComp = this.getCategoryCompartment(container); + if (structComp) { + return structComp; + } + } + return container; + } + + getCategoryCompartment(category: Category): GCompartment | undefined { + return category.children + .filter(child => child instanceof GCompartment) + .map(child => child as GCompartment) + .find(comp => ModelTypes.STRUCTURE === comp.type); + } +} diff --git a/examples/workflow-server/src/common/workflow-diagram-module.ts b/examples/workflow-server/src/common/workflow-diagram-module.ts index 06d50581..5cad3122 100644 --- a/examples/workflow-server/src/common/workflow-diagram-module.ts +++ b/examples/workflow-server/src/common/workflow-diagram-module.ts @@ -22,6 +22,7 @@ import { DiagramConfiguration, EdgeCreationChecker, GLSPServerInitializer, + GModelChangeContainerOperationHandler, GModelDiagramModule, InstanceMultiBinding, LabelEditValidator, @@ -35,6 +36,7 @@ import { SourceModelStorage } from '@eclipse-glsp/server'; import { injectable } from 'inversify'; +import { ChangeContainerWorkflowHandler } from './handler/change-container-workflow-handler'; import { CreateAutomatedTaskHandler } from './handler/create-automated-task-handler'; import { CreateCategoryHandler } from './handler/create-category-handler'; import { CreateDecisionNodeHandler } from './handler/create-decision-node-handler'; @@ -89,6 +91,7 @@ export class WorkflowDiagramModule extends GModelDiagramModule { binding.add(CreateDecisionNodeHandler); binding.add(CreateCategoryHandler); binding.add(EditTaskOperationHandler); + binding.rebind(GModelChangeContainerOperationHandler, ChangeContainerWorkflowHandler); } protected bindDiagramConfiguration(): BindingTarget { diff --git a/packages/server/src/common/gmodel/change-container-operation-handler.ts b/packages/server/src/common/gmodel/change-container-operation-handler.ts new file mode 100644 index 00000000..fc416736 --- /dev/null +++ b/packages/server/src/common/gmodel/change-container-operation-handler.ts @@ -0,0 +1,81 @@ +/******************************************************************************** + * Copyright (c) 2025-2026 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { GModelElement, isGBoundsAware } from '@eclipse-glsp/graph'; +import { ChangeContainerOperation, MaybePromise, Point } from '@eclipse-glsp/protocol'; +import { injectable } from 'inversify'; +import { Command } from '../command/command'; +import { getRelativeLocation } from '../utils/layout-util'; +import { GModelOperationHandler } from './gmodel-operation-handler'; + +/** + * Applies {@link ChangeContainerOperation} directly to the GModel by re-parenting the element + * from its current container to the target container. + */ +@injectable() +export class GModelChangeContainerOperationHandler extends GModelOperationHandler { + operationType = ChangeContainerOperation.KIND; + + createCommand(operation: ChangeContainerOperation): MaybePromise { + const element = this.modelState.index.find(operation.elementId); + const targetContainer = this.getContainer(operation); + if (!element || !targetContainer) { + return undefined; + } + // resolve elements lazily inside the closure so that references remain valid + // even when preceding commands in a CompoundCommand rebuild the model tree + return this.commandOf(() => { + const currentElement = this.modelState.index.find(operation.elementId); + const currentContainer = this.getContainer(operation); + if (currentElement && currentContainer) { + this.executeChangeContainer(currentElement, currentContainer, operation.location); + } + }); + } + + /** + * Returns the {@link GModelElement} that will receive the reparented element. By default this is the + * target element referenced by {@link ChangeContainerOperation.targetContainerId}, but it could also + * be e.g. an intermediate compartment or a different node. Subclasses can override this to redirect the + * reparented element into another container. + * + * @param operation the operation to resolve the container for. + * @returns the {@link GModelElement} that will contain the reparented element, or `undefined` if it cannot be resolved. + */ + getContainer(operation: ChangeContainerOperation): GModelElement | undefined { + return this.modelState.index.find(operation.targetContainerId); + } + + protected executeChangeContainer(element: GModelElement, targetContainer: GModelElement, location?: Point): void { + // remove from current parent + const currentParent = element.parent; + if (currentParent) { + const index = currentParent.children.indexOf(element); + if (index >= 0) { + currentParent.children.splice(index, 1); + } + } + // add to new parent + targetContainer.children.push(element); + element.parent = targetContainer; + // update position relative to the new container + if (location && isGBoundsAware(element)) { + const relativeLocation = getRelativeLocation(location, targetContainer); + if (relativeLocation) { + element.position = relativeLocation; + } + } + } +} diff --git a/packages/server/src/common/gmodel/gmodel-diagram-module.ts b/packages/server/src/common/gmodel/gmodel-diagram-module.ts index 2d448cbf..0f6ec62e 100644 --- a/packages/server/src/common/gmodel/gmodel-diagram-module.ts +++ b/packages/server/src/common/gmodel/gmodel-diagram-module.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2022-2023 STMicroelectronics and others. + * Copyright (c) 2022-2026 STMicroelectronics and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -25,6 +25,7 @@ import { DefaultModelState, ModelState } from '../features/model/model-state'; import { OperationHandler, OperationHandlerConstructor } from '../operations/operation-handler'; import { GModelApplyLabelEditOperationHandler } from './apply-label-edit-operation-handler'; import { GModelChangeBoundsOperationHandler } from './change-bounds-operation-handler'; +import { GModelChangeContainerOperationHandler } from './change-container-operation-handler'; import { GModelChangeRoutingPointsOperationHandler } from './change-routing-points-operation-handler'; import { GModelCutOperationHandler } from './cut-operation-handler'; import { GModelDeleteOperationHandler } from './delete-operation-handler'; @@ -41,6 +42,7 @@ import { GModelReconnectEdgeOperationHandler } from './reconnect-edge-operation- * - {@link RequestClipboardDataActionHandler} to {@link ActionHandler} * - {@link GModelApplyLabelEditOperationHandler} to {@link OperationHandler} * - {@link GModelChangeBoundsOperationHandler} to {@link OperationHandler} + * - {@link GModelChangeContainerOperationHandler} to {@link OperationHandler} * - {@link GModelCutOperationHandler} to {@link OperationHandler} * - {@link GModelDeleteOperationHandler} to {@link OperationHandler} * - {@link GModelPasteOperationHandler} to {@link OperationHandler} @@ -67,6 +69,7 @@ export abstract class GModelDiagramModule extends DiagramModule { super.configureOperationHandlers(binding); binding.add(GModelApplyLabelEditOperationHandler); binding.add(GModelChangeBoundsOperationHandler); + binding.add(GModelChangeContainerOperationHandler); binding.add(GModelCutOperationHandler); binding.add(GModelDeleteOperationHandler); binding.add(GModelPasteOperationHandler); diff --git a/packages/server/src/common/index.ts b/packages/server/src/common/index.ts index e850e0a2..86f63072 100644 --- a/packages/server/src/common/index.ts +++ b/packages/server/src/common/index.ts @@ -71,6 +71,7 @@ export * from './features/validation/model-validator'; export * from './features/validation/request-markers-handler'; export * from './gmodel/apply-label-edit-operation-handler'; export * from './gmodel/change-bounds-operation-handler'; +export * from './gmodel/change-container-operation-handler'; export * from './gmodel/change-routing-points-operation-handler'; export * from './gmodel/cut-operation-handler'; export * from './gmodel/delete-operation-handler'; diff --git a/packages/server/src/common/protocol/glsp-server.ts b/packages/server/src/common/protocol/glsp-server.ts index e20432c8..302b0749 100644 --- a/packages/server/src/common/protocol/glsp-server.ts +++ b/packages/server/src/common/protocol/glsp-server.ts @@ -14,8 +14,10 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { + Action, ActionMessage, Args, + CompoundOperation, DisposeClientSessionParameters, GLSPClientProxy, GLSPServer, @@ -167,7 +169,7 @@ export class DefaultGLSPServer implements GLSPServer { process(message: ActionMessage): void { this.validateServerInitialized(); - this.logger.info(`process [action=${message.action.kind}, clientId=${message.clientId}]`); + this.logger.info(`process [action=${this.formatAction(message.action)}, clientId=${message.clientId}]`); const clientSessionId = message.clientId; const clientSession = this.clientSessions.get(clientSessionId); if (!clientSession) { @@ -228,6 +230,14 @@ export class DefaultGLSPServer implements GLSPServer { this.sendToClient({ clientId: message.clientId, action: errorAction }); } + protected formatAction(action: Action): string { + if (CompoundOperation.is(action)) { + const children = action.operationList.map(op => op.kind).join(', '); + return `${action.kind}(${children})`; + } + return action.kind; + } + protected sendToClient(message: ActionMessage): void { this.glspClientProxy.process(message); }