Skip to content

Commit

Permalink
Feature/editable graph view (#22)
Browse files Browse the repository at this point in the history
Add a property view that allows the underlying models of a diagram node to be edited. 
---------

Co-authored-by: Martin Fleck <[email protected]>
  • Loading branch information
ozcanxbreeze and martin-fleck-at authored Sep 4, 2023
1 parent e447c9a commit 54b189e
Show file tree
Hide file tree
Showing 44 changed files with 4,651 additions and 2,801 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ lib
tsconfig.tsbuildinfo
src-gen
gen-webpack.config.js
gen-webpack.node.config.js
out
plugins
bin
Expand Down
2 changes: 2 additions & 0 deletions applications/browser-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"@crossbreeze/core": "0.0.0",
"@crossbreeze/form-client": "0.0.0",
"@crossbreeze/glsp-client": "0.0.0",
"@crossbreeze/model-service": "^1.0.0",
"@crossbreeze/product": "0.0.0",
"@crossbreeze/property-view": "^1.0.0",
"@theia/core": "^1.34.4",
"@theia/editor": "^1.34.4",
"@theia/filesystem": "^1.34.4",
Expand Down
4 changes: 3 additions & 1 deletion applications/electron-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
"@crossbreeze/core": "0.0.0",
"@crossbreeze/form-client": "0.0.0",
"@crossbreeze/glsp-client": "0.0.0",
"@crossbreeze/model-service": "^1.0.0",
"@crossbreeze/product": "0.0.0",
"@crossbreeze/property-view": "^1.0.0",
"@theia/core": "^1.34.4",
"@theia/editor": "^1.34.4",
"@theia/electron": "^1.34.4",
Expand All @@ -50,7 +52,7 @@
},
"devDependencies": {
"@theia/cli": "^1.34.4",
"electron": "^15.3.5",
"electron": "^23.2.4",
"electron-builder": "^23.6.0"
},
"productName": "CrossModel Community Edition",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import {
BindingTarget,
ContextActionsProvider,
DiagramConfiguration,
DiagramModule,
GModelFactory,
GModelIndex,
InstanceMultiBinding,
ModelState,
MultiBinding,
OperationHandlerConstructor,
SourceModelStorage
BindingTarget,
ContextActionsProvider,
DiagramConfiguration,
DiagramModule,
GModelFactory,
GModelIndex,
InstanceMultiBinding,
ModelState,
MultiBinding,
OperationHandlerConstructor,
SourceModelStorage
} from '@eclipse-glsp/server';
import { injectable } from 'inversify';
import { CrossModelAddEntityActionProvider } from '../command-palette/add-entity-action-provider';
Expand All @@ -21,6 +21,7 @@ import { CrossModelChangeBoundsOperationHandler } from '../handler/change-bounds
import { CrossModelCreateEdgeOperationHandler } from '../handler/create-edge-operation-handler';
import { CrossModelDeleteOperationHandler } from '../handler/delete-operation-handler';
import { CrossModelDropEntityOperationHandler } from '../handler/drop-entity-operation-handler';
import { CrossModelUpdateClientOperationHandler } from '../handler/update-glsp-client-handler';
import { CrossModelGModelFactory } from '../model/cross-model-gmodel-factory';
import { CrossModelIndex } from '../model/cross-model-index';
import { CrossModelState } from '../model/cross-model-state';
Expand All @@ -32,40 +33,41 @@ import { CrossModelDiagramConfiguration } from './cross-model-diagram-configurat
*/
@injectable()
export class CrossModelDiagramModule extends DiagramModule {
readonly diagramType = 'crossmodel-diagram';
readonly diagramType = 'crossmodel-diagram';

protected bindDiagramConfiguration(): BindingTarget<DiagramConfiguration> {
return CrossModelDiagramConfiguration;
}
protected bindDiagramConfiguration(): BindingTarget<DiagramConfiguration> {
return CrossModelDiagramConfiguration;
}

protected bindSourceModelStorage(): BindingTarget<SourceModelStorage> {
return CrossModelStorage;
}
protected bindSourceModelStorage(): BindingTarget<SourceModelStorage> {
return CrossModelStorage;
}

protected override configureOperationHandlers(binding: InstanceMultiBinding<OperationHandlerConstructor>): void {
super.configureOperationHandlers(binding);
binding.add(CrossModelChangeBoundsOperationHandler); // move + resize behavior
binding.add(CrossModelCreateEdgeOperationHandler); // create 1:1 relationship
binding.add(CrossModelDeleteOperationHandler); // delete elements
binding.add(CrossModelDropEntityOperationHandler);
binding.add(CrossModelAddEntityOperationHandler);
}
protected override configureOperationHandlers(binding: InstanceMultiBinding<OperationHandlerConstructor>): void {
super.configureOperationHandlers(binding);
binding.add(CrossModelChangeBoundsOperationHandler); // move + resize behavior
binding.add(CrossModelCreateEdgeOperationHandler); // create 1:1 relationship
binding.add(CrossModelDeleteOperationHandler); // delete elements
binding.add(CrossModelDropEntityOperationHandler);
binding.add(CrossModelAddEntityOperationHandler);
binding.add(CrossModelUpdateClientOperationHandler);
}

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

protected override bindGModelIndex(): BindingTarget<GModelIndex> {
this.context.bind(CrossModelIndex).toSelf().inSingletonScope();
return { service: CrossModelIndex };
}
protected override bindGModelIndex(): BindingTarget<GModelIndex> {
this.context.bind(CrossModelIndex).toSelf().inSingletonScope();
return { service: CrossModelIndex };
}

protected bindModelState(): BindingTarget<ModelState> {
return { service: CrossModelState };
}
protected bindModelState(): BindingTarget<ModelState> {
return { service: CrossModelState };
}

protected bindGModelFactory(): BindingTarget<GModelFactory> {
return CrossModelGModelFactory;
}
protected bindGModelFactory(): BindingTarget<GModelFactory> {
return CrossModelGModelFactory;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { UpdateClientOperation } from '@crossbreeze/protocol';
import { Command, OperationHandler } from '@eclipse-glsp/server';
import { inject, injectable } from 'inversify';
import { CrossModelState } from '../model/cross-model-state';
import { CrossModelCommand } from './cross-model-command';

@injectable()
export class CrossModelUpdateClientOperationHandler extends OperationHandler {
override operationType = UpdateClientOperation.KIND;

@inject(CrossModelState) protected state: CrossModelState;

createCommand(_operation: UpdateClientOperation): Command {
return new CrossModelCommand(this.state, () => {
/* do nothing, just trigger update*/
});
}
}
15 changes: 12 additions & 3 deletions extensions/crossmodel-lang/src/language-server/generated/ast.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
/******************************************************************************
* This file was generated by langium-cli 1.1.0.
* This file was generated by langium-cli 1.2.1.
* DO NOT EDIT MANUALLY!
******************************************************************************/

/* eslint-disable */
import { AstNode, AbstractAstReflection, Reference, ReferenceInfo, TypeMetaData } from 'langium';
import type { AstNode, Reference, ReferenceInfo, TypeMetaData } from 'langium';
import { AbstractAstReflection } from 'langium';

export type QualifiedName = string;

export function isQualifiedName(item: unknown): item is QualifiedName {
return typeof item === 'string';
}

export type RelationshipType = '1:1' | '1:n' | 'n:1' | 'n:m';

export function isRelationshipType(item: unknown): item is RelationshipType {
return item === '1:1' || item === '1:n' || item === 'n:1' || item === 'n:m';
}

export interface Attribute extends AstNode {
readonly $container: Entity;
readonly $type: 'Attribute';
Expand Down Expand Up @@ -126,7 +135,7 @@ export function isSystemDiagram(item: unknown): item is SystemDiagram {
return reflection.isInstance(item, SystemDiagram);
}

export interface CrossModelAstType {
export type CrossModelAstType = {
Attribute: Attribute
CrossModelRoot: CrossModelRoot
DiagramEdge: DiagramEdge
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/******************************************************************************
* This file was generated by langium-cli 1.1.0.
* This file was generated by langium-cli 1.2.1.
* DO NOT EDIT MANUALLY!
******************************************************************************/

import { loadGrammarFromJson, Grammar } from 'langium';
import type { Grammar } from 'langium';
import { loadGrammarFromJson } from 'langium';

let loadedCrossModelGrammar: Grammar | undefined;
export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (loadedCrossModelGrammar = loadGrammarFromJson(`{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/******************************************************************************
* This file was generated by langium-cli 1.1.0.
* This file was generated by langium-cli 1.2.1.
* DO NOT EDIT MANUALLY!
******************************************************************************/

import { LangiumGeneratedServices, LangiumGeneratedSharedServices, LangiumSharedServices, LangiumServices, LanguageMetaData, Module } from 'langium';
import type { LangiumGeneratedServices, LangiumGeneratedSharedServices, LangiumSharedServices, LangiumServices, LanguageMetaData, Module } from 'langium';
import { CrossModelAstReflection } from './ast';
import { CrossModelGrammar } from './grammar';

Expand Down
85 changes: 79 additions & 6 deletions extensions/crossmodel-lang/src/model-server/model-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/

import { CloseModel, CrossModelRoot, OpenModel, RequestModel, SaveModel, UpdateModel, OnSave } from '@crossbreeze/protocol';
import {
CloseModel,
CrossModelRoot,
OnSave,
OpenModel,
RequestModel,
RequestModelDiagramNode,
SaveModel,
UpdateModel
} from '@crossbreeze/protocol';
import { AstNode, isReference } from 'langium';
import { Disposable } from 'vscode-jsonrpc';
import * as rpc from 'vscode-jsonrpc/node';
import { isCrossModelRoot } from '../language-server/generated/ast';
import { CrossModelRoot as CrossModelRootAst, DiagramNode, Entity, isCrossModelRoot } from '../language-server/generated/ast';

import { ModelService } from './model-service';

/**
Expand All @@ -24,16 +34,67 @@ export class ModelServer implements Disposable {
this.toDispose.push(connection.onRequest(OpenModel, uri => this.openModel(uri)));
this.toDispose.push(connection.onRequest(CloseModel, uri => this.closeModel(uri)));
this.toDispose.push(connection.onRequest(RequestModel, uri => this.requestModel(uri)));
this.toDispose.push(connection.onRequest(RequestModelDiagramNode, (uri, id) => this.requestModelDiagramNode(uri, id)));
this.toDispose.push(connection.onRequest(UpdateModel, (uri, model) => this.updateModel(uri, model)));
this.toDispose.push(connection.onRequest(SaveModel, (uri, model) => this.saveModel(uri, model)));
}

/**
* Returns the entity model of the selected node in the diagram.
*
* @param uri The uri of the opened diagram
* @param id The id of the selected node
* @returns {
* uri: of the entity model
* entity: model of the entity
* }
*/
async requestModelDiagramNode(uri: string, id: string): Promise<DiagramNodeEntity | undefined> {
const root = (await this.modelService.request(uri)) as CrossModelRootAst;
let diagramNode: DiagramNode | undefined = undefined;

if (!root || !root.diagram) {
throw new Error('Something went wrong loading the diagram');
}

for (const node of root.diagram.nodes) {
if (node.name === id) {
if (diagramNode) {
throw new Error('Multiple nodes with the same name');
}

diagramNode = node;
}
}

if (!diagramNode) {
throw new Error('No node found with the given id');
}

const ref: Entity | undefined = diagramNode.semanticElement.ref;

if (!ref || !ref.$container.$document) {
throw new Error('No node found with the given id');
}

const entityUri = ref.$container.$document.uri.toString();
const serializedEntity: CrossModelRoot | undefined = toSerializable({
$type: 'CrossModelRoot',
entity: diagramNode.semanticElement.ref
});

return {
uri: entityUri,
model: serializedEntity
};
}

protected async openModel(uri: string): Promise<void> {
await this.modelService.open(uri);

this.modelService.onSave(uri, newModel => {
// TODO: Research if this also has to be closed after the document closes
this.connection.sendNotification(OnSave, uri, toSerializable(newModel as CrossModelRoot));
this.connection.sendNotification(OnSave, uri, toSerializable(newModel) as CrossModelRoot);
});
}

Expand All @@ -46,8 +107,9 @@ export class ModelServer implements Disposable {
return toSerializable(root) as CrossModelRoot;
}

protected async updateModel(uri: string, model: AstNode): Promise<void> {
await this.modelService.update(uri, model);
protected async updateModel(uri: string, model: CrossModelRoot): Promise<CrossModelRoot> {
const updated = await this.modelService.update(uri, model);
return toSerializable(updated) as CrossModelRoot;
}

protected async saveModel(uri: string, model: AstNode): Promise<void> {
Expand Down Expand Up @@ -80,7 +142,13 @@ export function toSerializable<T extends object>(obj?: T): T | undefined {
}

function cleanValue(value: any): any {
return isContainedObject(value) ? toSerializable(value) : resolvedValue(value);
if (Array.isArray(value)) {
return value.map(cleanValue);
} else if (isContainedObject(value)) {
return toSerializable(value);
} else {
return resolvedValue(value);
}
}

function isContainedObject(value: any): boolean {
Expand All @@ -93,3 +161,8 @@ function resolvedValue(value: any): any {
}
return value;
}

interface DiagramNodeEntity {
uri: string;
model: CrossModelRoot | undefined;
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"extensions/*"
],
"scripts": {
"build": "lerna run build",
"clean": "lerna run clean && rimraf node_modules",
"postinstall": "theia check:theia-version",
"lint": "lerna run lint",
"prepare": "lerna run prepare",
Expand Down
3 changes: 2 additions & 1 deletion packages/form-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@
"dependencies": {
"@crossbreeze/core": "0.0.0",
"@crossbreeze/protocol": "0.0.0",
"@crossbreeze/model-service": "^1.0.0",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@mui/material": "^5.12.1",
"@mui/material": "^5.13.5",
"@mui/x-data-grid": "^6.2.1",
"@theia/core": "^1.34.4",
"lodash": "^4.17.21",
Expand Down
Loading

0 comments on commit 54b189e

Please sign in to comment.