nope/resources/ui/graph/editor.tsx
Martin Karkowski 838d910c2f adding gojs
2020-12-01 13:05:35 +01:00

1010 lines
40 KiB
TypeScript

/**
* @author Martin Karkowski
* @email m.karkowski@zema.de
* @create date 2020-11-25 08:19:39
* @modify date 2020-11-25 12:05:20
* @desc [description]
*/
import * as React from "react";
import { Button, ListGroup } from "react-bootstrap";
import { v4 as generateID } from "uuid";
import { parseWithFunctions, stringifyWithFunctions } from "../../../lib/helpers/jsonMethods";
import { deepClone, rgetattr, rsetattr } from "../../../lib/helpers/objectMethods";
import { IJsonSchema } from "../../../lib/types/IJSONSchema";
import { readDataFromClipboard, writeToClipboard } from "../helpers/clipboard";
import { getCurrentThemeColors } from "../helpers/colors";
import DynamicLayout, { IDynamicLayoutProps } from "../layout/dynamicLayout";
import { ILayout } from "../layout/interfaces/ILayout";
import { ISelection } from "../layout/interfaces/ISelection";
import { ITab } from "../layout/interfaces/ITab";
import { ITabEntry } from "../layout/interfaces/ITabEntry";
import Selection from "../layout/selection";
import TabEntry, { ITabProps } from "../layout/tabs";
// Import Addons:
import { makeMeMultiSelect } from "./addons/selectionbox.extension";
import { generateDefaultSelection } from "./defaults/default.elements";
import { generateGraphOptions } from "./defaults/default.graph-options";
import { defaultHotkeys } from "./defaults/default.hotkeys";
import { defaultToolbar } from "./defaults/default.toolbar";
// Graph related imports:
import { UndoRedoGraph } from "./graph";
import { generateColors } from "./helpers/color.theme";
import { adaptIDS, adaptPositions, getSelectedElements } from "./helpers/data.handlers";
import { IBaseEdgeOptions } from "./interfaces/IBaseEdgeOptions";
import { IBaseNodeOptions } from "./interfaces/IBaseNodeOptions";
import { IBasicTemplate } from "./interfaces/IBasicTemplate";
import { IEdgeTypeDefinition } from "./interfaces/IEdgeTypeDefinition";
import { IEvent } from "./interfaces/IEvents";
import { IUndoRedoGraph } from "./interfaces/IGraph";
import { IGraphCallbackData } from "./interfaces/IGraphCallbackData";
import { DEFAULT_NETWORK, INetwork } from "./interfaces/INetwork";
import { IVisjsOptions } from "./interfaces/IVisjsOptions";
export interface GraphicalEditorComponentProps<N, E> {
graphOptions?: IVisjsOptions;
network?: INetwork<N, E>;
addEdgeCallback?: (edgeData: E, callback: (edgeData: E) => void) => void;
editOnSelect?: boolean;
editOnChange?: boolean;
parseFunctions?: boolean;
enableContextMenu?: boolean;
enableEditing?: boolean;
hidePanelOnDeselect?: boolean;
hideToolbar?: boolean;
hideRightPanel?: boolean;
allowSelfConnection?: boolean;
allowDoubleConnections?: boolean;
edgeTypes?: IEdgeTypeDefinition<N, E>[];
showEdgeSelction?: boolean
}
export interface GraphicalEditorComponentState {
update: null
}
class GraphicalEditorComponent<N, E> extends React.Component<GraphicalEditorComponentProps<N, E>, GraphicalEditorComponentState> {
/**
* The Element containing the Network.
*/
public Network: IUndoRedoGraph<N, E>;
public LayoutHandler: ILayout;
public TabHandler: ITabEntry;
public graphOptions: IVisjsOptions;
public theme: {
colors: {
primary: string;
secondary: string;
success: string;
info: string;
warning: string;
danger: string;
light: string;
dark: string;
};
font: {
size: number;
type: string;
};
}
constructor(props) {
super(props);
this.theme = getCurrentThemeColors();
}
public tabs: { [index: string]: INetwork<N, E> } = {};
/**
* Function to add the Addons of the Graph:
* @param network
*/
public addGraphAddons(network: IUndoRedoGraph<N, E>) {
makeMeMultiSelect(network);
}
/**
* Function to add an Edge.
* @param edgeData
* @param callback
*/
protected async _addEdge(edgeData: IBaseEdgeOptions, callback: (edge: IBaseEdgeOptions) => void) {
// Defaulty prevent self connections.
if (this.props.allowSelfConnection || edgeData.from !== edgeData.to) {
let addEdge = true;
// If multiple Connections from one node to the same node are
// for bidden, test if there already exists such an edge.
if (!this.props.allowDoubleConnections) {
this.Network.edges.forEach((edge) => {
if (edge.from === edgeData.from && edge.to === edgeData.to) {
addEdge = false;
}
});
}
// If adding an Edge is allowed => Add the Edge.
if (addEdge) {
// Manually add an ID to the Edge
edgeData.id = generateID();
// Extract the From and To Node.
const from = this.Network.getNode(edgeData.from);
const to = this.Network.getNode(edgeData.to);
const _this = this;
const _addEdge = (type: IEdgeTypeDefinition<N, E>) => {
if (typeof type.customAddFunction == "function") {
type.customAddFunction(from, to, _this.Network);
} else {
// Define the Edge Object
const edge = Object.assign(deepClone(type.visual), {
from: edgeData.from,
to: edgeData.to
});
// Add the Edge.
callback(edge);
}
};
switch (edgeData.type) {
case "automatic":
// Search for Possible Connection Types.
const possibleEdges: IEdgeTypeDefinition<N, E>[] = [];
for (const edgeType of this.props.edgeTypes || []) {
if ((edgeType.fromTypes.includes(from.type) || edgeType.fromTypes.length === 0) && (edgeType.toTypes.includes(to.type) || edgeType.toTypes.length === 0)) {
possibleEdges.push(edgeType);
}
}
// Test if Edges has been found
switch (possibleEdges.length) {
case 0:
this.LayoutHandler.showToast("No Matching Edge Type has been found. Please select the corresponding Edge Type manually", "info");
return;
case 1:
// Define the New Edge.
return _addEdge(possibleEdges[0]);
default:
if (this.props.showEdgeSelction) {
// Create a Popup, on which the corresponding edge can be selected:
try {
const type: IEdgeTypeDefinition<N, E> = await this.LayoutHandler.getDialogData({
content: {
props: {
possibleEdges
},
component(props: {
possibleEdges: IEdgeTypeDefinition<N, E>[],
onSubmit: (data: IEdgeTypeDefinition<N, E>) => void;
onCancel: (error: any) => void
}) {
return (
<>
Select a possible edge by clicking on the item
<ListGroup>
{
props.possibleEdges.map((item, idx) => {
<ListGroup.Item key={idx} onClick={_ => props.onSubmit(item)}>{item.type}</ListGroup.Item>;
})
}
</ListGroup>
<Button variant="danger" onClick={e => props.onCancel(e)}>Cancel </Button>
</>
);
},
},
header: "Please select an edge-type!",
closeButton: true
});
// Add the Edge itself.
return _addEdge(type);
} catch (e) {
// The User has aborted the selection.
return;
}
}
this.LayoutHandler.showToast("Mulitple Valid Edge Type has been found. Please select the corresponding Edge Type manually", "info");
}
return;
default:
for (const type of this.props.edgeTypes || []) {
if ((edgeData.type === type.type) && (type.fromTypes.includes(from.type) || type.fromTypes.length === 0) && (type.toTypes.includes(to.type) || type.toTypes.length === 0)) {
// Quit the Method
return _addEdge(type);
}
}
this.LayoutHandler.showToast("You are not allowed to connect the desired Nodes with this edges", "error");
break;
}
} else {
this.LayoutHandler.showToast("Double Connections not allowed! You can not connect these elements again", "error");
}
} else {
this.LayoutHandler.showToast("Self-Connections forbidden! You can not connect elements to itself", "error");
}
}
protected async _editNodes(_template: IBasicTemplate<N,E>, mode: "add" | "update" = "update") {
// Defaulty prevent self connections.
if (_template?.nodes.length > 0) {
try {
if (_template.nodes[0].editorComponentSelector && false){
} else {
// Open up the corresponding Node Panel:
_template.nodes[0] = await this.LayoutHandler.getDialogData({
content: {
component: "DynamicForm",
props: {
schema: {
type: "object",
properties:{
label: {
description: "Label of the Node",
type: "string"
},
title: {
description: "Tooltip, presented on Hovering",
type: "string"
},
// color: {
// description: 'Color of the Node.',
// type: 'string'
// }
},
required: [
"label"
]
} as IJsonSchema,
uiSchema: {
// color: {
// "ui:widget": "color"
// }
},
data: Object.assign(generateColors(_template.nodes[0].shape, this.theme), _template.nodes[0])
}
}
});
}
switch (mode) {
case "add":
this.Network.addNode(_template.nodes);
this.Network.addEdge(_template.edges);
break;
case "update":
this.Network.updateNode(_template.nodes);
this.Network.updateEdge(_template.edges);
break;
}
// Tabs
this.tabs[this.TabSettings.tabs.active] = this.Network.getData();
} catch(error) {
console.log(error);
}
} else {
this.LayoutHandler.showToast("Please Select a Node Before.", "error");
}
}
public initEditor(ref: React.RefObject<any>) {
if (process.browser){
const _this = this;
// Based on the Provided Options =>
// select the corresponding graph options,
// either use th provided one or use the
// default options.
if (this.props.graphOptions) {
this.graphOptions = this.props.graphOptions;
} else {
this.graphOptions = generateGraphOptions();
}
if (!rgetattr(this.graphOptions, "manipulation.enabled", false)) {
rsetattr(this.graphOptions, "manipulation.enabled", false);
}
// Generate the Default Callback for adding an Edge
rsetattr(
this.graphOptions,
"manipulation.addEdge",
(data,callback) => {
_this._addEdge(data,callback).catch(console.error);
}
);
// Generate the Default Callback for adding a Node
rsetattr(
this.graphOptions,
"manipulation.addNode",
(data,callback) => {
_this._addNode(data).catch(console.error);
}
);
// Create the Network
this.Network = new UndoRedoGraph(ref, this.graphOptions);
// Add a new item if possible.
const editItemIfPossible = (params) => {
if (params.nodes.length === 1) {
// A Node was Selected. Figure out, whether it is a cluster or not.
const nodeID = params.nodes[0];
if (_this.Network.network.isCluster(nodeID)) {
// Uncluster the Nodes
_this.Network.network.openCluster(nodeID);
} else {
// Open up the Settings Tab
const nodes = getSelectedElements(_this.Network, false);
_this._editNodes({
edges: [],
nodes,
type: "element"
});
}
} else if (params.edges.length === 1) {
// A Node was Selected. Figure out, whether it is a cluster or not.
const edges = _this.Network.data.edges.get(params.edges[0]);
_this.updateEdges(edges);
}
};
if (!this.Network){
throw Error("Something went wrong");
}
// Make the Default behaviour adding new Nodes
this.Network.on("doubleClick", (params) => {
if (_this.props.enableEditing || true) {
// Test if no Element was Selected
if (params.nodes.length === 0 && params.edges.length === 0) {
// If so, just add the new Element
_this._addNode(params.pointer.canvas);
} else {
editItemIfPossible(params);
}
}
});
this.Network.on("oncopy", event => {
try {
_this.LayoutHandler.showToast("Copied");
writeToClipboard(stringifyWithFunctions(_this.createTemplateOfSelectedElements()));
} catch (e) {
}
});
this.Network.on("onpaste", async event => {
try {
const data = await readDataFromClipboard();
const mousePosition = _this.Network.network.DOMtoCanvas({
x: _this.LayoutHandler.currentMousePosition.offsetX,
y: _this.LayoutHandler.currentMousePosition.offsetY
});
_this.paste(parseWithFunctions(data), mousePosition, false);
_this.LayoutHandler.showToast("Pasted");
} catch (e) {
_this.LayoutHandler.showToast("Failed Pasting","error");
console.error(e);
}
});
// // Make shure the Sidepanel is working correctly
this.Network.on("select", (params) => {
// if (_this.graphOptions.enableEditing){
// if (_this.graphOptions.editOnSelect && _this.graphOptions.hidePanelOnDeselect && params.nodes.length === 0 && params.edges.length === 0){
// _this.layout.panels.right.hide();
// } else if (_this.graphOptions.editOnSelect){
// editItemIfPossible(params);
// }
// }
});
// enableClusterPreview(this);
// this.network.addNode(this._nodes);
// this.network.addEdge(this._edges);
// editor.resize();
console.log("TABS",this.tabs);
this.Network.loadData(this.tabs[this.TabSettings?.tabs?.active] || DEFAULT_NETWORK());
// Add all Addons as required.
this.addGraphAddons(this.Network);
}
return this.Network;
}
/**
* Function, which is used to provide a
*/
public createTemplateOfSelectedElements() {
const selected: IBasicTemplate<N,E> = {
nodes: [],
edges: [],
type: "element"
};
const selection = this.Network.network.getSelection(true);
selected.nodes = deepClone(this.Network.nodes.filter(item => selection.nodes.indexOf(item.id) !== -1));
selected.edges = deepClone(this.Network.edges.filter(item => selection.edges.indexOf(item.id) !== -1));
return selected;
}
public paste(
template: IBasicTemplate<N,E>,
position: {
x: number,
y: number
},
useExistingNodesForEdges: boolean) {
const data = adaptPositions(adaptIDS(template, useExistingNodesForEdges), position);
this.Network.addNode(data.nodes);
this.Network.addEdge(data.edges);
}
/**
* Function to Update the Data of a Node.
* @param selection The Selected Node.
*/
public async updateNode(selection: Array<IBaseNodeOptions<N>>) {
const _self = this;
/** Extract the Component, which should be used in the Prompt */
let componentSelector = "default";
if (selection.length > 0 && selection[0].editorComponentSelector) {
componentSelector = selection[0].editorComponentSelector;
}
// const comp = this.editPanelDict.nodes[componentSelector];
// if (comp) {
// /** Open the Window, with the Edit-Prompt */
// this.openEditInterface(this.editPanelDict.nodes[componentSelector], {
// inputTemplate: {
// nodes: selection,
// edges: [],
// type: 'elements'
// },
// },
// 'Edit Node',
// (data) => {
// _self.network.updateNode(data.template.nodes);
// });
// } else {
// this.layout.showToast('Editor is Trying to open an Unkown Edit-Component', 'error');
// }
}
protected async _addNode(pos: { x: number, y: number }) {
// Defaulty prevent self connections.
if (this._template?.nodes.length > 0) {
try {
// Adapt its IDs. (Creates a Copy)
let _template = adaptIDS(this._template);
// Adapt its Position
_template = adaptPositions(_template, pos);
// Remove the Initial Label:
delete _template.nodes[0].label;
await this._editNodes(_template, "add");
} catch(error) {
console.error(error);
}
} else {
this.LayoutHandler.showToast("Please Select a Node Before.", "error");
}
}
public EditorSettings: GraphicalEditorComponentProps<N, E>
public LayoutSettings: IDynamicLayoutProps<IGraphCallbackData<N,E>>
public TabSettings: ITabProps;
protected _template: IBasicTemplate<N,E>
public selectTemplate(template: IBasicTemplate<N,E>){
this._template = template;
}
public initializeGraph() {
const _this = this;
// Assign a default Network:
const networkToRender: INetwork<N,E> = this.props.network || this.tabs[this.TabSettings?.tabs?.active] || DEFAULT_NETWORK();
// Initally store the Network to Render
this.tabs[networkToRender.id] = networkToRender;
this.TabSettings = {
onMount(item){
_this.TabHandler = item;
},
async onNewTab() {
// Function to create a new tab.
try {
if (_this.TabHandler.tabs.length == 0) {
const tab = {
delteable: true,
id: generateID(),
label: "Unsaved Content"
};
await _this.TabHandler.createTab(tab);
// Store the Content of the tab:
_this.tabs[tab.id] = _this.Network.getData();
}
const label = await _this.LayoutHandler.getDialogData({
header: "Enter Network-Name",
content: {
component: "DynamicForm",
props: {
schema: {
type: "string",
description: "Name of the Element"
}
}
}
});
const id = Date.now().toString();
// const label = id;
_this.tabs[id] = DEFAULT_NETWORK();
return {
id,
label,
delteable: true
};
} catch (error) {
return false;
}
},
async onTabSelect(oldTab: ITab, newTab: ITab) {
if (_this.Network) {
// Contains the Main Logic for selecting another tab
// Therefore
if (oldTab) {
_this.tabs[oldTab.id] = _this.Network.getData();
}
// Use the content of the new Tab as rendering:
_this.Network.loadData(_this.tabs[newTab.id]);
}
return true;
},
async onTabDelete(tab: ITab, force: boolean) {
// A Tab should be deleted
if (force) {
return true;
}
try {
const saveData = await _this.LayoutHandler.getDialogData<boolean>({
header: "Close Tab",
content: {
component: (props) => {
return (<>
<p>
You are about to close the tab "{tab.label}". Do you want to save changes?
</p>
<Button variant="success" onClick={_ => props.onSubmit(true)}>Yes</Button>{" "}
<Button variant="danger" onClick={_ => props.onSubmit(false)}>No</Button>{" "}
<Button variant="secondary" onClick={_ => props.onCancel(new Error("Canceled"))}>Cancel</Button>{" "}
</>);
},
props: {}
}
});
// The variable save Data contains the info, whether the user wants to save or discard the changes
return true;
} catch (error) {
return false;
}
},
async onNoTabSelected() {
// Nothing to Display!
if (_this.Network) {
console.log("No Tab");
// Use the content of the new Tab as rendering:
_this.Network.loadData(DEFAULT_NETWORK());
}
},
async onTabEdit(tab){
const label = await _this.LayoutHandler.getDialogData<string>({
header: "Enter Network-Name",
content: {
component: "DynamicForm",
props: {
schema: {
type: "string",
description: "Name of the Element"
},
data: tab.label
}
}
});
tab.label = label;
return tab;
},
tabs: {
active: "-1",
allowNewTabs: true,
items: [],
}
};
this.LayoutSettings = {
components: [
{
component() {
const graph = React.useRef(null);
// Similar to componentDidMount and componentDidUpdate:
// Createa a Function, that if mounted defines the Network.
React.useEffect(() => {
// Define the Network.
_this.initEditor(graph);
// Return a Function, that will destroy the network.
return () => {
_this.tabs[_this.TabSettings.tabs.active] = _this.Network.getData();
// Destroy the Network.
_this.Network.destroy();
_this.Network = null;
};
});
return (<>
{React.createElement(TabEntry, { ..._this.TabSettings })}
<div ref={graph} style={{ height: "100%" }} onMouseEnter={e => {
// Enable Hotkeys
_this.LayoutHandler.hotkeysEnabled = true;
}} onMouseLeave={e => {
// Disable Hotkeys
_this.LayoutHandler.hotkeysEnabled = false;
}}></div>
</>
);
},
gridSettings: {
h: 6,
w: 11,
x: 5,
y: 0,
minW: 3,
minH: 3,
},
id: "main",
label: "Editor",
props: {
},
visible: true,
bg: "light",
text: "dark"
},
{
component(props: {selection: ISelection<IBasicTemplate<N,E>>}){
return (<Selection<IBasicTemplate<N,E>> selection={props.selection} allowUserSelect={true} onItemSelected={template =>_this.selectTemplate(template)}></Selection>);
},
gridSettings: {
h: 3,
w: 4,
x: 0,
y: 0,
maxW: 5,
},
id: "selection",
label: "Selection",
props: {
selection: generateDefaultSelection()
},
hideable: true,
bg:"light",
visible: true,
text: "dark",
showLabel: true,
},
{
component() {
const [img, setImg] = React.useState();
const [pos, setPos] = React.useState<{left: number, top: number, height: number, width: number}>({
left: 0,
top: 0,
width: 0,
height: 0,
});
const [style, setStyle] = React.useState<any>();
const radar = React.useRef(null);
const image = React.useRef(null);
// Similar to componentDidMount and componentDidUpdate:
// Createa a Function, that if mounted subscribe to changes
// of the Nework.
React.useEffect(() => {
const wait = () => {
return new Promise((resolve, reject) => {
_this.Network.once("afterDrawing", resolve);
});
};
const updateImage = async () => {
// Store the current View Settings
const scale = _this.Network.network.getScale();
const translate = _this.Network.network.getViewPosition();
// Zoom out for the Image.
_this.Network.fit({animation: false});
// Wait for the Network to Redraw
await wait();
const canvas = _this.Network.network.body.container.getElementsByTagName("canvas")[0];
const imgData = canvas.toDataURL("image/png");
setImg(imgData);
/**
* Extract the Widht and Height
*/
const {
clientWidth,
clientHeight
} = _this.Network.network.body.container;
// Store the Position.
setPos({
left: translate.x - clientWidth/ scale / 2,
top: translate.y - clientHeight/ scale / 2,
width: clientWidth/ scale,
height: clientHeight / scale,
});
// Use inital View
_this.Network.network.moveTo({
position: translate,
scale: scale,
animation: false
});
};
const getScaling = (start,size, current, scale = true) => {
if (scale){
return Math.min(1, Math.max(0, (current - start) / size));
}
return (current - start) / size;
};
/**
* Function to draw the current Positon of the View.
*/
const drawRadar = () => {
if (radar?.current && image?.current){
const displayedWidth = image.current.clientWidth;
const displayedHeight = image.current.clientHeight;
/**
* Extract the Widht and Height
*/
const {
clientWidth,
clientHeight
} = _this.Network.network.body.container;
// Determine visible Nodes:
const scale = _this.Network.network.getScale();
const translate = _this.Network.network.getViewPosition();
// Contains the Left Position of the View.
// The following Transformation is used:
// _____
// |(top,left)-Original = 0,0
// | ___
// | |(top, left of the View)
//
const current = {
left: translate.x - clientWidth/ scale / 2,
top: translate.y - clientHeight/ scale / 2,
width: clientWidth/ scale,
height: clientHeight / scale,
};
const top = getScaling(pos.top, pos.height, current.top);
const bottom = getScaling(pos.top, pos.height, current.top + current.height);
const height = bottom-top;
const left = getScaling(pos.left, pos.width, current.left);
const right = getScaling(pos.left, pos.width, current.left + current.width);
const width = right - left;
setStyle({
position: "absolute",
backgroundColor: "rgba(16, 84, 154, 0.26)",
top: (image.current.offsetTop + top * displayedHeight).toString() + "px",
left: (image.current.offsetLeft + left * displayedWidth).toString() + "px",
height: (height* displayedHeight).toString() + "px",
width: Math.max(0,width * displayedWidth- image.current.offsetLeft).toString() + "px"
});
}
};
const listeners: Partial<{[K in IEvent]: any}> = {
"dataUpdate": updateImage,
"afterDrawing" : drawRadar
};
const subscriptions: Array<() => void> = [];
if (_this.Network) {
for (const key in listeners){
subscriptions.push(_this.Network.on(key as IEvent, listeners[key]));
}
}
// Return a Function, that will destroy the network.
return () => {
for (const unsubscribe of subscriptions){
unsubscribe();
}
};
});
return (
<div style={{overflow: "hidden"}}>
<img ref={image} src={img} style={{position: "absolute", maxHeight:"100%", maxWidth: "100%"}}></img>
<div ref={radar} style={style}></div>
</div>
);
},
gridSettings: {
h: 2,
w: 4,
x: 0,
y: 4,
maxW: 5,
},
id: "minimap",
label: "Minimap",
props: {},
hideable: true,
bg:"light",
visible: true,
text: "dark",
showLabel: true,
},
{
component(){
return (<>Preview</>);
},
gridSettings: {
h: 3,
w: 3,
x: 3,
y: 5,
},
id: "preview",
label: "Preview",
props: {},
hideable: true,
bg:"light",
visible: false,
text: "dark"
},
],
layoutSettings: {
width: process.browser ? window.innerWidth : 1920,
autoSize: false,
preventCollision: false,
cols: 15,
compactType: "horizontal",
onLayoutChange(){
if (_this.Network) {
_this.Network.resize();
}
}
},
generateData(){
// Get the Current Selection.
const selection = _this.Network.network.getSelection(true);
const ret: IGraphCallbackData<N,E> = {
network: _this.Network,
layout: _this.LayoutHandler,
selectedEdges: selection.edges,
selectedNodes: selection.nodes,
tabs: _this.TabHandler
};
Object.defineProperty(ret, "network", {
get(){
return _this.Network;
},
});
Object.defineProperty(ret, "layout", {
get(){
return _this.LayoutHandler;
},
});
return ret;
},
onResize() {
if (_this.Network) {
_this.Network.resize();
}
},
onMount(layout){
_this.LayoutHandler = layout;
},
toolbar: defaultToolbar(),
hotkeys: defaultHotkeys()
};
return this.LayoutSettings;
}
public requestRerender() {
this.setState({
update: null
});
}
public componentDidMount() {
const _this = this;
}
public render() {
this.initializeGraph();
return React.createElement(DynamicLayout, {... this.LayoutSettings});
}
}
export default GraphicalEditorComponent;