Skip to content
Snippets Groups Projects
editor.tsx 19.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • import React from "react";
    
    import { DynamicGraph } from "./graph";
    
    import {
    
        deleteGraphJson,
    
        listAllSpaces,
        loadGraphJson,
        saveGraphJson,
    } from "../common/datasets";
    
    import SpaceSelect from "./components/spaceselect";
    
    import "./editor.css";
    
    import * as Helpers from "../common/helpers";
    
    import { Node, NodeProperties } from "../common/graph/node";
    
    import SelectLayer from "./components/selectlayer";
    
    import { Coordinate2D, GraphData, SimGraphData } from "../common/graph/graph";
    
    import { NodeType, NodeTypeData } from "../common/graph/nodetype";
    
    import { GraphRenderer2D } from "./renderer";
    
    import * as Config from "../config";
    
    import Sidepanel from "./components/sidepanel";
    
    import { Link } from "../common/graph/link";
    
    import { Checkpoint } from "../common/history";
    
    export interface NodeDataChangeRequest extends NodeProperties {
        id: number;
        type: NodeType;
    }
    
    
    export interface EditorSettings {
    
        /**
         * Should labels on nodes be rendered, or none at all.
         */
    
    
        /**
         * Should feature be enabled, that nodes get connected with a link of dragged close enough to each other?
         */
    
    }
    
    type stateTypes = {
        /**
         * Graph structure holding the basic information.
         */
        graph: DynamicGraph;
    
        settings: EditorSettings;
    
         * Current width of graph object. Used to specifically adjust and correct the graph size.
    
    
        /**
         * True for each key, that is currently considered pressed. If key has not been pressed yet, it will not exist as dict-key.
         */
    
         * Collection of all currently selected nodes. Can also be undefined or empty.
    
    
        spaces: string[];
    
        spaceId: string;
    
    /**
     * Knowledge space graph editor. Allows easy editing of the graph structure.
     */
    
    export class Editor extends React.PureComponent<any, stateTypes> {
    
        private rendererRef: React.RefObject<GraphRenderer2D>;
    
        constructor(props: any) {
    
    
            // Making sure, all functions retain the proper this-bind
    
            this.loadGraph = this.loadGraph.bind(this);
    
            this.loadSpace = this.loadSpace.bind(this);
    
            this.renameSpace = this.renameSpace.bind(this);
            this.createSpace = this.createSpace.bind(this);
            this.deleteSpace = this.deleteSpace.bind(this);
            this.duplicateSpace = this.duplicateSpace.bind(this);
    
            this.saveSpace = this.saveSpace.bind(this);
    
            this.forceUpdate = this.forceUpdate.bind(this);
    
            this.handleNodeTypeSelect = this.handleNodeTypeSelect.bind(this);
    
            this.handleNodeTypeDataChange =
                this.handleNodeTypeDataChange.bind(this);
            this.handleNodeTypeDeletion = this.handleNodeTypeDeletion.bind(this);
            this.handleNodeTypeCreation = this.handleNodeTypeCreation.bind(this);
    
            this.handleBoxSelect = this.handleBoxSelect.bind(this);
            this.selectNodes = this.selectNodes.bind(this);
    
            this.handleNodeDataChange = this.handleNodeDataChange.bind(this);
    
            this.handleNodeCreation = this.handleNodeCreation.bind(this);
            this.handleNodeDeletion = this.handleNodeDeletion.bind(this);
            this.handleLinkCreation = this.handleLinkCreation.bind(this);
            this.handleLinkDeletion = this.handleLinkDeletion.bind(this);
    
            this.handleCheckpointRequest = this.handleCheckpointRequest.bind(this);
            this.handleUndo = this.handleUndo.bind(this);
            this.handleRedo = this.handleRedo.bind(this);
    
            this.createCheckpoint = this.createCheckpoint.bind(this);
    
    Matthias Konitzny's avatar
    Matthias Konitzny committed
            // Subscribe to global events
    
            document.addEventListener("keydown", (e) => {
                this.keyPressed(e.key);
            });
            document.addEventListener("keyup", (e) => {
                this.keyReleased(e.key);
            });
    
    Matthias Konitzny's avatar
    Matthias Konitzny committed
            window.addEventListener("resize", () => this.handleResize());
    
            this.rendererRef = React.createRef();
    
            // Set as new state
            this.state = {
                graph: undefined,
    
                settings: {
                    visibleLabels: true,
                    connectOnDrag: false,
                },
    
                spaces: [],
                spaceId: Config.SPACE,
    
        keyPressed(key: string) {
            const keys = this.state.keys;
            keys[key] = true;
    
            this.setState({ keys: { ...keys } });
    
        }
    
        keyReleased(key: string) {
            const keys = this.state.keys;
            keys[key] = false;
    
            this.setState({ keys: { ...keys } });
    
        /**
         * Tries to load initial graph after webpage finished loading.
         */
    
        componentDidMount() {
    
            // Get list of all available spaces and pick initial space based on that
            this.loadListOfSpaces().then(
                (spaces: string[]) => {
                    // Try to load first element in list of available spaces
                    // If not available, just load default space
                    const initialSpaceId =
                        spaces.length > 0 ? spaces[0] : this.state.spaceId;
                    this.loadSpace(initialSpaceId);
                },
                () => this.loadSpace(this.state.spaceId)
            );
    
        }
    
        /**
         * Fetches the most current list of available spaces from the server and updates the state accordingly.
         */
        private loadListOfSpaces(): Promise<string[]> {
            return listAllSpaces().then((spaces) => {
                this.setState({ spaces: spaces });
                return spaces;
            });
    
        }
    
        /**
         * Loads a space from the database to the editor.
         * @param spaceId Id of space to load.
         * @returns Promise with boolean value that is true, if successful.
         */
    
        public loadSpace(spaceId: string): Promise<boolean> {
            return loadGraphJson(spaceId)
                .then((data: GraphData) => {
                    // Loading space might have created a new space, if requested space was not available
                    // Just in case, reload list of spaces
                    this.loadListOfSpaces();
                    return data;
                })
                .then((data: GraphData) => this.loadGraph(data, spaceId));
    
        public saveSpace(): Promise<void> {
    
            return saveGraphJson(
                this.state.spaceId,
                this.state.graph.toJSONSerializableObject()
            ).then(
    
                () => {
                    console.log("Successfully saved space!");
    
                    alert("Space saved successfully!");
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                () => {
                    console.log("Something went wrong when saving the space! :(");
                    alert("Something went wrong, could not save space.");
                }
    
    Matthias Konitzny's avatar
    Matthias Konitzny committed
         * Loads another graph based on the data supplied.
    
         * @param data Serialized graph data.
         * @returns True, if successful.
         */
    
        public loadGraph(data: GraphData, id: string): boolean {
    
            console.log("Starting to load new graph ...");
    
            console.log(data);
    
            // Allow a single phase of force simulation when loading the graph.
            if (this.rendererRef.current != undefined) {
                this.rendererRef.current.allowForceSimulation();
            }
    
    
            const graph = new DynamicGraph();
            graph.fromSerializedObject(data);
    
                spaceId: id,
    
        /**
         * Processes resize window event. Focusses on resizing the graph accordingly.
         */
    
            const newGraphWidth = Helpers.getClientWidth("knowledge-space-editor");
    
            this.setState({
                graphWidth: newGraphWidth,
            });
        }
    
    
        handleBoxSelect(selectedNodes: Node[]) {
            if (selectedNodes !== undefined && selectedNodes.length <= 0) {
    
            this.selectNodes([
                ...new Set(selectedNodes.concat(this.state.selectedNodes)),
            ]);
    
        /**
         * Selects multiple nodes, or clears selection if given undefined or empty array.
         * @param nodes Multiple nodes to mark as selected.
         */
        public selectNodes(nodes: Node[]) {
            this.setState({
                selectedNodes: nodes,
            });
    
        private handleNodeTypeSelect(type: NodeType) {
            const nodesWithType = this.state.graph.nodes.filter((n: Node) =>
                n.type.equals(type)
            );
            this.selectNodes(nodesWithType);
        }
    
    
        private handleNodeDataChange(
            nodeData: NodeDataChangeRequest[],
            createCheckpoint = true
        ) {
    
            if (nodeData.length == 0) {
                return;
            }
    
    
            // Create a shallow copy of the graph object to trigger an update over setState
    
            const graph = Object.assign(new DynamicGraph(), this.state.graph);
    
            // Modify node
            for (const request of nodeData) {
                const node = graph.node(request.id);
                Object.assign(node, request);
            }
    
    
            // Create checkpoint
            if (createCheckpoint) {
                graph.createCheckpoint(`Modified ${nodeData.length} node(s) data.`);
            }
    
            // Push shallow copy to state
            this.setState({ graph: graph });
        }
    
        private handleNodeTypeDataChange(
            nodeTypeData: NodeTypeData[],
            createCheckpoint = true
        ) {
            if (nodeTypeData.length == 0) {
                return;
            }
    
            // Create a shallow copy of the graph object to trigger an update over setState
            const graph = Object.assign(new DynamicGraph(), this.state.graph);
    
            // Modify node type
            for (const request of nodeTypeData) {
                const node = graph.nodeType(request.id);
                Object.assign(node, request);
            }
    
            // Create checkpoint
            if (createCheckpoint) {
                graph.createCheckpoint(
                    `Modified ${nodeTypeData.length} node(s) data.`
                );
            }
    
            // Push shallow copy to state
            this.setState({ graph: graph });
        }
    
    
        private handleNodeCreation(
            position?: Coordinate2D,
            createCheckpoint = true
        ): Node {
    
            const graph = Object.assign(new DynamicGraph(), this.state.graph);
            const node = graph.createNode(undefined, position.x, position.y, 0, 0);
    
    
            if (createCheckpoint) {
                graph.createCheckpoint("Created new node.");
            }
    
        private handleNodeDeletion(ids: number[], createCheckpoint = true) {
    
            const graph = Object.assign(new DynamicGraph(), this.state.graph);
    
            ids.forEach((id) => graph.deleteNode(id));
    
            const selectedNodes = this.state.selectedNodes.filter(
                (node) => !ids.includes(node.id)
            );
    
    
            if (createCheckpoint) {
                graph.createCheckpoint(`Deleted ${ids.length} nodes.`);
            }
    
    
            this.setState({ graph: graph, selectedNodes: selectedNodes });
    
        private handleLinkCreation(
            source: number,
            target: number,
            createCheckpoint = true
        ): Link {
    
            const graph = Object.assign(new DynamicGraph(), this.state.graph);
            const link = graph.createLink(source, target);
    
    
            if (createCheckpoint) {
                graph.createCheckpoint(
                    `Created link between ${graph.node(source).name} and ${
                        graph.node(target).name
                    }.`
                );
            }
    
        private handleLinkDeletion(ids: number[], createCheckpoint = true) {
    
            const graph = Object.assign(new DynamicGraph(), this.state.graph);
    
            ids.forEach((id) => graph.deleteLink(id));
    
    
            if (createCheckpoint) {
                graph.createCheckpoint(`Deleted ${ids.length} link(s).`);
            }
    
    
            this.setState({ graph: graph });
        }
    
    
        private handleNodeTypeCreation(createCheckpoint = true): NodeType {
            const graph = Object.assign(new DynamicGraph(), this.state.graph);
            const nodeType = graph.createObjectGroup();
    
            if (createCheckpoint) {
                graph.createCheckpoint("Created new node type.");
            }
    
            this.setState({ graph: graph });
            return nodeType;
        }
    
        private handleNodeTypeDeletion(ids: number[], createCheckpoint = true) {
            const graph = Object.assign(new DynamicGraph(), this.state.graph);
            ids.forEach((id) => graph.deleteNodeType(id));
    
            if (createCheckpoint) {
                graph.createCheckpoint(`Deleted ${ids.length} node type(s).`);
            }
    
            this.setState({ graph: graph });
        }
    
    
        private loadGraphFromCheckpoint(checkpoint: Checkpoint<SimGraphData>) {
            const graph = new DynamicGraph();
            graph.fromSerializedObject(checkpoint.data);
    
    
            // Transfer checkpoints to new graph object
    
            graph.history.copyCheckpointsFromHistory(this.state.graph.history);
    
            // Restore selected nodes
    
            const selectedNodes = this.state.selectedNodes
                .map((node) => graph.node(node.id))
                .filter((node) => node != undefined);
    
            this.setState({ graph: graph, selectedNodes: selectedNodes });
        }
    
        private handleCheckpointRequest(id: number) {
            const history = this.state.graph.history;
            const checkpoint = history.resetToCheckpoint(id);
            this.loadGraphFromCheckpoint(checkpoint);
        }
    
        private handleUndo() {
            const history = this.state.graph.history;
            const checkpoint = history.undo();
            this.loadGraphFromCheckpoint(checkpoint);
        }
    
        private handleRedo() {
            const history = this.state.graph.history;
            const checkpoint = history.redo();
            this.loadGraphFromCheckpoint(checkpoint);
        }
    
        private clearHistory(description = "") {
            const graph = Object.assign(new DynamicGraph(), this.state.graph);
    
        /**
         * Creates a new Checkpoint in the graph history
         * @param description Checkpoint description.
         */
        private createCheckpoint(description: string) {
            const graph = Object.assign(new DynamicGraph(), this.state.graph);
            graph.createCheckpoint(description);
            this.setState({ graph: graph });
        }
    
    
        /**
         * @param newId Explicit id of space that should be deleted.
         */
    
        private deleteSpace(spaceId: string): Promise<void> {
            return deleteGraphJson(spaceId).then(() => {
                // Select first space in list if available, otherwise select defaul space (which will be created)
    
                const remainingSpacesList: string[] = this.state.spaces.filter(
                    (space: string) => space != spaceId
                );
    
                const selectSpaceId: string =
    
                    remainingSpacesList.length > 0
                        ? remainingSpacesList[0]
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                        : Config.SPACE;
    
    
                this.loadSpace(selectSpaceId);
            });
    
        }
    
        /**
         * @param newId New id for currently selected space.
    
         * @returns Promise is true, if new, renamed graph could be loaded successfully.
    
        private renameSpace(newId: string): Promise<boolean> {
            return saveGraphJson(newId, this.state.graph.toJSONSerializableObject())
                .then(() => deleteGraphJson(this.state.spaceId))
                .then(() => this.loadSpace(newId));
    
        }
    
        /**
         * @param newId Id for the newly created space with the data of the currently selected space copied over.
    
         * @returns Promise is true, if newly created graph could be loaded successfully.
    
        private duplicateSpace(newId: string): Promise<boolean> {
            return saveGraphJson(
                newId,
                this.state.graph.toJSONSerializableObject()
            ).then(() => this.loadSpace(newId));
    
        }
    
        /**
         * @param newSpaceId Id for newly created space with the default empty space data.
    
         * @returns Promise is true, if newly created graph could be loaded successfully.
    
        private createSpace(newSpaceId: string): Promise<boolean> {
            return this.loadSpace(newSpaceId);
    
        render(): React.ReactNode {
            return (
                <div id="ks-editor">
                    <h1>Interface</h1>
    
                    <SpaceSelect
                        onLoadSpace={this.loadSpace}
    
                        onDeleteSpace={this.deleteSpace}
                        onRenameSpace={this.renameSpace}
                        onDuplicateSpace={this.duplicateSpace}
                        onCreateSpace={this.createSpace}
    
                        spaces={this.state.spaces}
                        spaceId={this.state.spaceId}
                    />
                    {this.state.graph && (
                        <div id="content">
    
                                    nodes={this.state.graph.nodes}
    
                                            ? this.rendererRef.current
                                                  .screen2GraphCoords
                                            : undefined
                                    }
                                    isEnabled={this.state.keys["Shift"]}
                                    onBoxSelect={this.handleBoxSelect}
                                >
                                    <GraphRenderer2D
                                        ref={this.rendererRef}
                                        graph={this.state.graph}
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                                        width={this.state.graphWidth}
    
                                        onNodeSelectionChanged={this.selectNodes}
    
                                        onNodeCreation={this.handleNodeCreation}
                                        onNodeDeletion={this.handleNodeDeletion}
                                        onLinkCreation={this.handleLinkCreation}
                                        onLinkDeletion={this.handleLinkDeletion}
    
                                        onEngineStop={() =>
                                            this.clearHistory(
                                                `Loaded graph ${this.state.spaceId}.`
                                            )
                                        }
    
                                        selectedNodes={this.state.selectedNodes}
    
                                        settings={this.state.settings}
    
    Maximilian Giller's avatar
    Maximilian Giller committed
                                    />
    
    
                            <Sidepanel
                                graph={this.state.graph}
    
                                onCheckpointRequest={this.handleCheckpointRequest}
                                onUndo={this.handleUndo}
                                onRedo={this.handleRedo}
    
                                onNodeTypeSelect={this.handleNodeTypeSelect}
    
                                onNodeTypeCreation={this.handleNodeTypeCreation}
                                onNodeTypeDelete={this.handleNodeTypeDeletion}
                                onNodeTypeDataChange={this.handleNodeTypeDataChange}
    
                                onSettingsChange={(settings) =>
                                    this.setState({ settings: settings })
    
                                }
                                selectedNodes={this.state.selectedNodes}
    
                                settings={this.state.settings}
    
                                onNodeDataChange={this.handleNodeDataChange}
    
                                onSave={this.saveSpace}
    
                                createCheckpoint={this.createCheckpoint}