Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ability to reject request actions #184

Merged
merged 2 commits into from
Jul 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions examples/multicore/src/chipmodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
import {
SShapeElement, SChildElement, SModelElementSchema, SModelRootSchema,
Bounds, Direction, BoundsAware, boundsFeature, Fadeable, fadeFeature,
layoutContainerFeature, LayoutContainer, Selectable, selectFeature, ViewportRootElement, hoverFeedbackFeature, Hoverable, popupFeature
layoutContainerFeature, LayoutContainer, Selectable, selectFeature,
ViewportRootElement, hoverFeedbackFeature, Hoverable, popupFeature, JsonMap
} from '../../../src';
import { CORE_DISTANCE, CORE_WIDTH } from "./views";

Expand All @@ -31,7 +32,7 @@ export class Processor extends ViewportRootElement implements BoundsAware {

rows: number = 0;
columns: number = 0;
layoutOptions: any;
layoutOptions: JsonMap;

get bounds(): Bounds {
return {
Expand Down
62 changes: 54 additions & 8 deletions src/base/actions/action-dispatcher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,19 @@
import 'reflect-metadata';
import 'mocha';
import { expect } from "chai";
import { Container, injectable } from "inversify";
import { Container, injectable, interfaces } from "inversify";
import { TYPES } from "../types";
import { EMPTY_BOUNDS } from '../../utils/geometry';
import { InitializeCanvasBoundsAction } from '../features/initialize-canvas';
import { RedoAction, UndoAction } from "../../features/undo-redo/undo-redo";
import { Command, CommandExecutionContext, CommandReturn, ICommand } from '../commands/command';
import { ICommandStack } from "../commands/command-stack";
import { ActionDispatcher } from "./action-dispatcher";
import { Action } from "./action";
import { Action, RejectAction } from "./action";
import defaultModule from "../di.config";
import { SetModelAction } from '../features/set-model';
import { SetModelAction, RequestModelAction } from '../features/set-model';
import { EMPTY_ROOT } from '../model/smodel-factory';
import { IActionHandler, configureActionHandler } from './action-handler';

describe('ActionDispatcher', () => {
@injectable()
Expand All @@ -52,7 +53,21 @@ describe('ActionDispatcher', () => {
kind = MockCommand.KIND;
}

function setup() {
@injectable()
class ResolvingHandler implements IActionHandler {
handle(action: RequestModelAction): Action {
return new SetModelAction({ type: 'root', id: 'foo' }, action.requestId);
}
}

@injectable()
class RejectingHandler implements IActionHandler {
handle(action: RequestModelAction): Action {
return new RejectAction('because bar', action.requestId);
}
}

function setup(options: { requestHandler?: interfaces.Newable<IActionHandler>, initialize?: boolean } = {}) {
const state = {
execCount: 0,
undoCount: 0,
Expand Down Expand Up @@ -81,8 +96,14 @@ describe('ActionDispatcher', () => {
const container = new Container();
container.load(defaultModule);
container.rebind(TYPES.ICommandStack).toConstantValue(mockCommandStack);
if (options.requestHandler) {
configureActionHandler(container, RequestModelAction.KIND, options.requestHandler);
}

const actionDispatcher = container.get<ActionDispatcher>(TYPES.IActionDispatcher);
if (options.initialize) {
actionDispatcher.dispatch(new InitializeCanvasBoundsAction(EMPTY_BOUNDS));
}
return { actionDispatcher, state };
}

Expand Down Expand Up @@ -126,13 +147,12 @@ describe('ActionDispatcher', () => {
});

it('should resolve/reject promises', async () => {
const { actionDispatcher } = setup();
await actionDispatcher.dispatch(new InitializeCanvasBoundsAction(EMPTY_BOUNDS));
const { actionDispatcher } = setup({ initialize: true });

// We expect this promise to be resolved
await actionDispatcher.dispatch(new SetModelAction(EMPTY_ROOT));
// We expect this promis to be resolved
// Remove the blocking
await actionDispatcher.dispatch(new InitializeCanvasBoundsAction(EMPTY_BOUNDS));
// remove the blocking

try {
await actionDispatcher.dispatch({ kind: 'unknown' });
Expand All @@ -141,4 +161,30 @@ describe('ActionDispatcher', () => {
// We expect this promise to be rejected
}
});

it('should reject requests without handler', async () => {
const { actionDispatcher } = setup({ initialize: true });
try {
await actionDispatcher.request(RequestModelAction.create());
expect.fail();
} catch (err) {
// We expect this promise to be rejected
}
});

it('should be able to resolve requests', async () => {
const { actionDispatcher } = setup({ requestHandler: ResolvingHandler, initialize: true });
const response = await actionDispatcher.request(RequestModelAction.create());
expect(response.newRoot.id).to.equal('foo');
});

it('should be able to reject requests', async () => {
const { actionDispatcher } = setup({ requestHandler: RejectingHandler, initialize: true });
try {
await actionDispatcher.request(RequestModelAction.create());
expect.fail();
} catch (err) {
expect(err.message).to.equal('because bar');
}
});
});
21 changes: 18 additions & 3 deletions src/base/actions/action-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { ICommandStack } from "../commands/command-stack";
import { AnimationFrameSyncer } from "../animations/animation-frame-syncer";
import { SetModelAction } from '../features/set-model';
import { RedoAction, UndoAction } from "../../features/undo-redo/undo-redo";
import { Action, isAction, RequestAction, ResponseAction, isResponseAction } from './action';
import { Action, isAction, RequestAction, ResponseAction, isResponseAction, RejectAction, isRequestAction } from './action';
import { ActionHandlerRegistry } from "./action-handler";
import { IDiagramLocker } from "./diagram-locker";

Expand Down Expand Up @@ -115,7 +115,14 @@ export class ActionDispatcher implements IActionDispatcher {
const deferred = this.requests.get(action.responseId);
if (deferred !== undefined) {
this.requests.delete(action.responseId);
deferred.resolve(action);
if (action.kind === RejectAction.KIND) {
const rejectAction = action as RejectAction;
deferred.reject(new Error(rejectAction.message));
this.logger.warn(this, `Request with id ${action.responseId} failed.`,
rejectAction.message, rejectAction.detail);
} else {
deferred.resolve(action);
}
return Promise.resolve();
}
this.logger.log(this, 'No matching request for response', action);
Expand All @@ -124,7 +131,15 @@ export class ActionDispatcher implements IActionDispatcher {
const handlers = this.actionHandlerRegistry.get(action.kind);
if (handlers.length === 0) {
this.logger.warn(this, 'Missing handler for action', action);
return Promise.reject(`Missing handler for action '${action.kind}'`);
const error = new Error(`Missing handler for action '${action.kind}'`);
if (isRequestAction(action)) {
const deferred = this.requests.get(action.requestId);
if (deferred !== undefined) {
this.requests.delete(action.requestId);
deferred.reject(error);
}
}
return Promise.reject(error);
}
this.logger.log(this, 'Handle', action);
const promises: Promise<any>[] = [];
Expand Down
14 changes: 14 additions & 0 deletions src/base/actions/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { JsonAny } from '../../utils/json';

/**
* An action describes a change to the model declaratively.
* It is a plain data structure, and as such transferable between server and client. An action must never contain actual
Expand Down Expand Up @@ -63,6 +65,18 @@ export function isResponseAction(object?: any): object is ResponseAction {
&& (object as any)['responseId'] !== '';
}

/**
* A reject action is fired to indicate that a request must be rejected.
*/
export class RejectAction implements ResponseAction {
static readonly KIND = 'rejectRequest';
readonly kind = RejectAction.KIND;

constructor(public readonly message: string,
public readonly responseId: string,
public readonly detail?: JsonAny) {}
}

/**
* A list of actions with a label.
* Labeled actions are used to denote a group of actions in a user-interface context, e.g.,
Expand Down
5 changes: 3 additions & 2 deletions src/base/features/set-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
********************************************************************************/

import { inject, injectable } from "inversify";
import { JsonPrimitive } from '../../utils/json';
import { Action, RequestAction, ResponseAction, generateRequestId } from "../actions/action";
import { CommandExecutionContext, ResetCommand } from "../commands/command";
import { SModelRoot, SModelRootSchema } from "../model/smodel";
Expand All @@ -30,11 +31,11 @@ export class RequestModelAction implements RequestAction<SetModelAction> {
static readonly KIND = 'requestModel';
readonly kind = RequestModelAction.KIND;

constructor(public readonly options?: { [key: string]: string | number | boolean },
constructor(public readonly options?: { [key: string]: JsonPrimitive },
public readonly requestId = '') {}

/** Factory function to dispatch a request with the `IActionDispatcher` */
static create(options?: { [key: string]: string | number | boolean }): RequestAction<SetModelAction> {
static create(options?: { [key: string]: JsonPrimitive }): RequestAction<SetModelAction> {
return new RequestModelAction(options, generateRequestId());
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/base/views/mouse-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export class MouseTool implements IVNodePostprocessor {
on(vnode, 'mouseup', this.mouseUp.bind(this), element);
on(vnode, 'mousemove', this.mouseMove.bind(this), element);
on(vnode, 'wheel', this.wheel.bind(this), element);
on(vnode, 'contextmenu', (target: SModelElement, event: any) => {
on(vnode, 'contextmenu', (target: SModelElement, event: Event) => {
event.preventDefault();
}, element);
on(vnode, 'dblclick', this.doubleClick.bind(this), element);
Expand Down
4 changes: 2 additions & 2 deletions src/features/bounds/abstract-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { ILayout, StatefulLayouter } from './layout';
import { AbstractLayoutOptions, HAlignment, VAlignment } from './layout-options';
import { BoundsData } from './hidden-bounds-updater';

export abstract class AbstractLayout<T extends AbstractLayoutOptions & Object> implements ILayout {
export abstract class AbstractLayout<T extends AbstractLayoutOptions> implements ILayout {

layout(container: SParentElement & LayoutContainer,
layouter: StatefulLayouter) {
Expand Down Expand Up @@ -63,7 +63,7 @@ export abstract class AbstractLayout<T extends AbstractLayoutOptions & Object> i

protected getFixedContainerBounds(
container: SModelElement,
layoutOptions: any,
layoutOptions: T,
layouter: StatefulLayouter): Bounds {
let currentContainer = container;
while (true) {
Expand Down
4 changes: 3 additions & 1 deletion src/features/bounds/layout-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { JsonMap } from '../../utils/json';

export type HAlignment = 'left' | 'center' | 'right';

export type VAlignment = 'top' | 'center' | 'bottom';

export interface AbstractLayoutOptions extends Object {
export interface AbstractLayoutOptions extends JsonMap {
resizeContainer: boolean
paddingTop: number
paddingBottom: number
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,5 +209,6 @@ export * from "./utils/browser";
export * from "./utils/color";
export * from "./utils/geometry";
export * from "./utils/inversify";
export * from "./utils/json";
export * from "./utils/logging";
export * from "./utils/registry";
25 changes: 25 additions & 0 deletions src/utils/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/********************************************************************************
* Copyright (c) 2020 TypeFox 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
********************************************************************************/

export type JsonAny = JsonPrimitive | JsonMap | JsonArray | null;

export type JsonPrimitive = string | number | boolean;

export interface JsonMap {
[key: string]: JsonAny;
}

export interface JsonArray extends Array<JsonAny> {}