Skip to content

Commit

Permalink
Provide command API (#38)
Browse files Browse the repository at this point in the history
Implement command API & Undo/Redo support

- implement a generic Command/Commandstack API similar to the EMF Api
- Provide default implementations for Commandstack & Compound command
- Provide a RecordingCommand API for arbitary JSON models using json patches
- Add testscases for the new command API
Fixes eclipse-glsp/glsp#791

- Refactor OperationHandler API to return optional executable commands (similar to how it has been done for the Java API)
- Unfortunately this introduces hard breaks and maintaining a compatibility/deprecation layer is not easily feasible.
- Most prominent changes: Refactor `OperationHandler` from interface to class and refactor `CreateOperationHandler` to interface to facilitate multi-inheritance
Fixes eclipse-glsp/glsp#889

Ensure that all components of the direct gmodel library are correctly prefixed.
Fixes eclipse-glsp/glsp#826
  • Loading branch information
tortmayr authored Feb 7, 2023
1 parent 3d7585d commit 44bd1e0
Show file tree
Hide file tree
Showing 44 changed files with 1,462 additions and 368 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
- New namespaces for environment specific code:
- `@eclipse-glsp/server/node`
- `@eclipse-glsp/server/browser`
- [operation] Implement Command API and rework OperationHandler to provide an optional command instead of direct execution to allow more execution control (including undo & redo support) [#38](https://github.com/eclipse-glsp/glsp-server-node/pull/38)
- This includes major breaking changes across the whole API:
- `OperationHandler` has been refactored from an interface to a common abstract base class. The `execute` method now has to return a `MaybePromise<Command|undefined>`
- Refactored `CreateOperationHandler` to an interface instead of a class
- Renamed the services and handlers of the direct GModel library => consistent use of `GModel` prefix
- The `ModelState` interface no longer has an `isDirty` flag. Dirty state is now handled by the `CommandStack`

## [v1.0.0 - 30/06/2022](https://github.com/eclipse-glsp/glsp-server-node/releases/tag/v1.0.0)

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Below is a list of features that are currently supported.
| Feature | Node Server | Java Server |
| ----------------------------------------------------------------- | :-------------: | :-------------: |
| Model Saving |||
| Model Dirty State | ||
| Model Dirty State | ||
| Model SVG Export |||
| Model Layout |||
| Model Edit Modes<br>- Edit<br>- Read-only | <br>✓<br>&nbsp; | <br>✓<br>✓ |
Expand All @@ -44,7 +44,7 @@ Below is a list of features that are currently supported.
| Edge Routing Points |||
| Element Text Editing |||
| Clipboard (Cut, Copy, Paste) |||
| Undo / Redo | ||
| Undo / Redo | ||
| Contexts<br>- Context Menu<br>- Command Palette<br>- Tool Palette | <br><br>✓<br>✓ | <br>✓<br>✓<br>✓ |

## Build
Expand Down
2 changes: 1 addition & 1 deletion examples/workflow-server/src/common/graph-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class TaskNode extends GNode {
references: string;

static override builder(): TaskNodeBuilder {
return new TaskNodeBuilder(TaskNode).layout('vbox').addArgs(ArgsUtil.cornerRadius(5));
return new TaskNodeBuilder(TaskNode).layout('vbox').addArgs(ArgsUtil.cornerRadius(5)).addCssClass('task');
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export abstract class CreateTaskHandler extends CreateWorkflowNodeOperationHandl
protected builder(point: Point | undefined): TaskNodeBuilder {
return TaskNode.builder()
.position(point ?? Point.ORIGIN)
.addCssClass('task')
.name(this.label.replace(' ', '') + this.modelState.index.getAllByClass(TaskNode).length)
.type(this.elementTypeIds[0])
.taskType(ModelTypes.toNodeType(this.elementTypeIds[0]))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/********************************************************************************
* Copyright (c) 2023 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 { Action, Command, getOrThrow, GModelOperationHandler, hasStringProp, MaybePromise, Operation } from '@eclipse-glsp/server';
import { injectable } from 'inversify';
import { TaskNode } from '../graph-extension';
import { ModelTypes } from '../util/model-types';
/**
* Is send from the {@link TaskEditor} to the GLSP server
* to update a feature from a specified task.
*/
export interface EditTaskOperation extends Operation {
kind: typeof EditTaskOperation.KIND;

/**
* Id of the task that should be edited
*/
taskId: string;

/**
* The feature that is to be updated
*/
feature: 'duration' | 'taskType';

/**
* The new feature value
*/
value: string;
}

export namespace EditTaskOperation {
export const KIND = 'editTask';

export function is(object: any): object is EditTaskOperation {
return (
Action.hasKind(object, KIND) &&
hasStringProp(object, 'taskId') &&
hasStringProp(object, 'feature') &&
hasStringProp(object, 'value')
);
}

export function create(options: { taskId: string; feature: 'duration' | 'taskType'; value: string }): EditTaskOperation {
return {
kind: KIND,
isOperation: true,
...options
};
}
}

@injectable()
export class EditTaskOperationHandler extends GModelOperationHandler {
readonly operationType = EditTaskOperation.KIND;

createCommand(operation: EditTaskOperation): MaybePromise<Command | undefined> {
const task = getOrThrow(
this.modelState.index.findByClass(operation.taskId, TaskNode),
`Cannot find task with id '${operation.taskId}'`
);
switch (operation.feature) {
case 'duration': {
const duration = Number.parseInt(operation.value, 10);
return duration !== task.duration //
? this.commandOf(() => this.editDuration(task, duration))
: undefined;
}
case 'taskType': {
return task.taskType !== operation.value //
? this.commandOf(() => this.editTaskType(task, operation.value))
: undefined;
}
}
}

protected editDuration(task: TaskNode, duration: number): void {
task.duration = duration;
}

protected editTaskType(task: TaskNode, type: string): void {
task.taskType = type;
if (type === 'manual' || type === 'automated') {
const temp = this.createTempTask(type, task);
const toAssign: Partial<TaskNode> = {
taskType: temp.taskType,
type: temp.type,
children: temp.children,
cssClasses: temp.cssClasses
};
Object.assign(task, toAssign);
return;
}
throw new Error(`Could not edit task '${task.id}'. Invalid type: ${type}`);
}

protected createTempTask(type: 'automated' | 'manual', task: TaskNode): TaskNode {
return TaskNode.builder() //
.type(type === 'automated' ? ModelTypes.AUTOMATED_TASK : ModelTypes.MANUAL_TASK)
.taskType(type)
.name(task.name)
.addCssClass(type)
.children()
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/********************************************************************************
* Copyright (c) 2023 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 { ContextActionsProvider, EditorContext, LabeledAction, MaybePromise, ModelState, toTypeGuard } from '@eclipse-glsp/server';
import { inject, injectable } from 'inversify';
import { TaskNode } from '../graph-extension';
import { EditTaskOperation } from './edit-task-operation-handler';

@injectable()
export class TaskEditContextActionProvider implements ContextActionsProvider {
static readonly DURATION_PREFIX = 'duration:';
static readonly TYPE_PREFIX = 'type:';
static readonly TASK_PREFIX = 'task:';

readonly contextId = 'task-editor';

@inject(ModelState)
protected modelState: ModelState;

getActions(editorContext: EditorContext): MaybePromise<LabeledAction[]> {
const text = editorContext.args?.['text'].toString() ?? '';
const taskNode = this.modelState.index.findParentElement(editorContext.selectedElementIds[0], toTypeGuard(TaskNode));
if (!taskNode) {
return [];
}

if (text.startsWith(TaskEditContextActionProvider.TYPE_PREFIX)) {
const taskId = taskNode.id;
return [
{ label: 'type:automated', actions: [EditTaskOperation.create({ taskId, feature: 'taskType', value: 'automated' })] },
{ label: 'type:manual', actions: [EditTaskOperation.create({ taskId, feature: 'taskType', value: 'manual' })] }
];
}

if (text.startsWith(TaskEditContextActionProvider.DURATION_PREFIX)) {
return [];
}

const taskType = taskNode.type.substring(TaskEditContextActionProvider.TASK_PREFIX.length);
const duration = taskNode.duration;
return [
<SetAutocompleteValueAction>{ label: 'type:', actions: [], text: `${TaskEditContextActionProvider.TYPE_PREFIX}${taskType}` },
<SetAutocompleteValueAction>{
label: 'duration:',
actions: [],
text: `${TaskEditContextActionProvider.DURATION_PREFIX}${duration ?? 0}`
}
];
}
}

interface SetAutocompleteValueAction extends LabeledAction {
text: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/********************************************************************************
* Copyright (c) 2023 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 { ContextEditValidator, RequestEditValidationAction, ValidationStatus } from '@eclipse-glsp/server';
import { injectable } from 'inversify';
import { TaskEditContextActionProvider } from './task-edit-context-provider';

@injectable()
export class TaskEditValidator implements ContextEditValidator {
readonly contextId = 'task-editor';

validate(action: RequestEditValidationAction): ValidationStatus {
const text = action.text;
if (text.startsWith(TaskEditContextActionProvider.DURATION_PREFIX)) {
const durationString = text.substring(TaskEditContextActionProvider.DURATION_PREFIX.length);
const duration = Number.parseInt(durationString, 10);
if (Number.isNaN(duration)) {
return { severity: ValidationStatus.Severity.ERROR, message: `'${durationString}' is not a valid number.` };
} else if (duration < 0 || duration > 100) {
return { severity: ValidationStatus.Severity.WARNING, message: `'${durationString}' should be between 0 and 100` };
}
} else if (text.startsWith(TaskEditContextActionProvider.TYPE_PREFIX)) {
const typeString = text.substring(TaskEditContextActionProvider.TYPE_PREFIX.length);
if (typeString !== 'automated' && typeString !== 'manual') {
return {
severity: ValidationStatus.Severity.ERROR,
message: `'Type of task can only be manual or automatic. You entered '${typeString}'.`
};
}
}
return ValidationStatus.NONE;
}
}
16 changes: 16 additions & 0 deletions examples/workflow-server/src/common/workflow-diagram-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import {
BindingTarget,
CommandPaletteActionProvider,
ContextActionsProvider,
ContextEditValidator,
ContextMenuItemProvider,
DiagramConfiguration,
GLSPServer,
Expand Down Expand Up @@ -49,6 +51,9 @@ import { NodeDocumentationNavigationTargetProvider } from './provider/node-docum
import { PreviousNodeNavigationTargetProvider } from './provider/previous-node-navigation-target-provider';
import { WorkflowCommandPaletteActionProvider } from './provider/workflow-command-palette-action-provider';
import { WorkflowContextMenuItemProvider } from './provider/workflow-context-menu-item-provider';
import { EditTaskOperationHandler } from './taskedit/edit-task-operation-handler';
import { TaskEditContextActionProvider } from './taskedit/task-edit-context-provider';
import { TaskEditValidator } from './taskedit/task-edit-validator';
import { WorkflowDiagramConfiguration } from './workflow-diagram-configuration';
import { WorkflowGLSPServer } from './workflow-glsp-server';
import { WorkflowPopupFactory } from './workflow-popup-factory';
Expand Down Expand Up @@ -81,6 +86,7 @@ export class WorkflowDiagramModule extends GModelDiagramModule {
binding.add(CreateMergeNodeHandler);
binding.add(CreateDecisionNodeHandler);
binding.add(CreateCategoryHandler);
binding.add(EditTaskOperationHandler);
}

protected bindDiagramConfiguration(): BindingTarget<DiagramConfiguration> {
Expand Down Expand Up @@ -117,4 +123,14 @@ export class WorkflowDiagramModule extends GModelDiagramModule {
binding.add(PreviousNodeNavigationTargetProvider);
binding.add(NodeDocumentationNavigationTargetProvider);
}

protected override configureContextActionProviders(binding: MultiBinding<ContextActionsProvider>): void {
super.configureContextActionProviders(binding);
binding.add(TaskEditContextActionProvider);
}

protected override configureContextEditValidators(binding: MultiBinding<ContextEditValidator>): void {
super.configureContextEditValidators(binding);
binding.add(TaskEditValidator);
}
}
1 change: 1 addition & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"@eclipse-glsp/protocol": "next",
"@types/uuid": "8.3.1",
"commander": "^8.3.0",
"fast-json-patch": "^3.1.0",
"inversify": "^5.1.1",
"winston": "^3.3.3"
},
Expand Down
Loading

0 comments on commit 44bd1e0

Please sign in to comment.