From ee841ba5253c104dd7887b454c109096a6382fa7 Mon Sep 17 00:00:00 2001
From: Maximilian Giller <m.giller@tu-bs.de>
Date: Wed, 28 Sep 2022 02:21:55 +0200
Subject: [PATCH] Implemented all actions for graph manager

---
 src/common/datasets.js                 | 21 ++++++-
 src/datasets.php                       | 21 ++++++-
 src/editor/components/spacemanager.tsx | 11 +++-
 src/editor/components/spaceselect.tsx  | 28 ++++-----
 src/editor/editor.tsx                  | 84 +++++++++++++++++---------
 src/ks-datasets-database.php           | 10 ++-
 6 files changed, 126 insertions(+), 49 deletions(-)

diff --git a/src/common/datasets.js b/src/common/datasets.js
index 2f30ef7..4081153 100644
--- a/src/common/datasets.js
+++ b/src/common/datasets.js
@@ -3,9 +3,10 @@
 /**
  * Uses the fetch API to create an ajax call for the WordPress API to fetch/post data from/to the server.
  * @param data {FormData} Data for the ajax call. Must contain an entry which specifies the ajax `action`
+ * @param parseJsonResult If true, tries to parse response as JSON. Should be disabled if backend does not return JSON data.
  * @returns {Promise<any>} The response from the server
  */
-function ajaxCall(data) {
+function ajaxCall(data, parseJsonResult = true) {
     let opts = {
         method: "POST",
         body: data,
@@ -13,7 +14,7 @@ function ajaxCall(data) {
 
     return fetch(ks_global.ajax_url, opts).then(
         function (response) {
-            return response.json();
+            return parseJsonResult ? response.json() : response;
         },
         function (reason) {
             console.log(reason);
@@ -23,6 +24,7 @@ function ajaxCall(data) {
 
 /**
  * Returns the json object from the stored graph as promise.
+ * Creates new graph if spaceId does not exist yet.
  *
  * @param {String} spaceId Identification of graph to load.
  *
@@ -36,6 +38,21 @@ export function loadGraphJson(spaceId) {
     return ajaxCall(data);
 }
 
+/**
+ * Removes a graph entry from the database.
+ *
+ * @param {String} spaceId Identification of graph to remove.
+ *
+ * @returns Promise returning state of query.
+ */
+export function deleteGraphJson(spaceId) {
+    const data = new FormData();
+    data.append("action", "delete_space");
+    data.append("space", spaceId);
+
+    return ajaxCall(data, false);
+}
+
 /**
  * Takes the graph json object and stores it in the backend.
  *
diff --git a/src/datasets.php b/src/datasets.php
index 92d2a4d..d2acace 100644
--- a/src/datasets.php
+++ b/src/datasets.php
@@ -53,6 +53,22 @@ function ks_update_space() {
     }
 }
 
+add_action("wp_ajax_delete_space", "ks_delete_space"); // Fires only for logged-in-users
+//add_action("wp_ajax_nopriv_delete_space", 'delete_space' ); // Fires for everyone
+function ks_delete_space()
+{
+    // Check user capabilities
+    if (current_user_can("edit_posts")) {
+        $name = ks_escape_space_name($_POST["space"]);
+
+        ks_delete_graph($name);
+
+        wp_die();
+    } else {
+        echo "Insufficient permissions!";
+    }
+}
+
 function ks_escape_space_name($space_name) {
     // Cleaning up the space id
     $space_name = str_replace("/", "-", $space_name);
@@ -61,5 +77,6 @@ function ks_escape_space_name($space_name) {
     $space_name = str_replace(";", "-", $space_name);
     $space_name = str_replace(":", "-", $space_name);
     $space_name = str_replace(",", "-", $space_name);
-    return strtolower($space_name);
-}
+    return $space_name;
+    // return strtolower($space_name);  Not used, since it reduces readability, but is not really required
+}
\ No newline at end of file
diff --git a/src/editor/components/spacemanager.tsx b/src/editor/components/spacemanager.tsx
index 937f89e..76ed708 100644
--- a/src/editor/components/spacemanager.tsx
+++ b/src/editor/components/spacemanager.tsx
@@ -2,6 +2,7 @@ import React, { useState } from "react";
 import "./spacemanager.css";
 
 interface SpaceManagerProps {
+    spaces: string[];
     spaceId: string;
     onDeleteSpace: (spaceId: string) => void;
     onRenameSpace: (newId: string) => void;
@@ -10,6 +11,7 @@ interface SpaceManagerProps {
 }
 
 function SpaceManager({
+    spaces,
     spaceId,
     onDeleteSpace,
     onRenameSpace,
@@ -25,6 +27,9 @@ function SpaceManager({
         onDeleteSpace(spaceId);
     };
 
+    const isSpaceCreationAllowed = () =>
+        newSpaceName.length === 0 || spaces.includes(newSpaceName.trim());
+
     return (
         <div id="space-manager">
             <label htmlFor="space-name">New graph name</label>
@@ -39,7 +44,7 @@ function SpaceManager({
             <ul>
                 <li>
                     <button
-                        disabled={newSpaceName.length === 0}
+                        disabled={isSpaceCreationAllowed()}
                         onClick={() => onRenameSpace(newSpaceName)}
                     >
                         Rename to {'"' + newSpaceName + '"'}
@@ -47,7 +52,7 @@ function SpaceManager({
                 </li>
                 <li>
                     <button
-                        disabled={newSpaceName.length === 0}
+                        disabled={isSpaceCreationAllowed()}
                         onClick={() => onDuplicateSpace(newSpaceName)}
                     >
                         Duplicate as {'"' + newSpaceName + '"'}
@@ -55,7 +60,7 @@ function SpaceManager({
                 </li>
                 <li>
                     <button
-                        disabled={newSpaceName.length === 0}
+                        disabled={isSpaceCreationAllowed()}
                         onClick={() => onCreateSpace(newSpaceName)}
                     >
                         Create empty graph {'"' + newSpaceName + '"'}
diff --git a/src/editor/components/spaceselect.tsx b/src/editor/components/spaceselect.tsx
index 86503b2..6a1915f 100644
--- a/src/editor/components/spaceselect.tsx
+++ b/src/editor/components/spaceselect.tsx
@@ -3,7 +3,7 @@ import SpaceManager from "./spacemanager";
 import "./spaceselect.css";
 
 interface SpaceSelectProps {
-    onLoadSpace: (spaceId: string) => boolean;
+    onLoadSpace: (spaceId: string) => Promise<boolean>;
     onDeleteSpace: (spaceId: string) => void;
     onRenameSpace: (newId: string) => void;
     onDuplicateSpace: (newId: string) => void;
@@ -21,22 +21,21 @@ function SpaceSelect({
     onCreateSpace,
     onDeleteSpace,
 }: SpaceSelectProps) {
-    const [selected, setSelected] = useState(spaceId);
     const [managerVisible, setMangerVisible] = useState(false);
 
-    const handleDeleteSpace = (spaceId: string) => {
-        setMangerVisible(false);
-        onDeleteSpace(spaceId);
+    const hideManagerAndCall = (call: (id: string) => void) => {
+        return (spaceId: string) => {
+            setMangerVisible(false);
+            call(spaceId);
+        };
     };
 
     return (
         <div id="spaceselect">
             <select
-                onChange={(event) => {
-                    setSelected(event.target.value);
-                    onLoadSpace(event.target.value);
-                }}
-                value={selected}
+                onChange={(event) => onLoadSpace(event.target.value)}
+                value={spaceId}
+                onFocus={() => setMangerVisible(false)}
             >
                 {spaces.map((spaceName: string) => (
                     <option key={spaceName} value={spaceName}>
@@ -51,11 +50,12 @@ function SpaceSelect({
             </button>
             {managerVisible && (
                 <SpaceManager
+                    spaces={spaces}
                     spaceId={spaceId}
-                    onCreateSpace={onCreateSpace}
-                    onRenameSpace={onRenameSpace}
-                    onDeleteSpace={handleDeleteSpace}
-                    onDuplicateSpace={onDuplicateSpace}
+                    onCreateSpace={hideManagerAndCall(onCreateSpace)}
+                    onRenameSpace={hideManagerAndCall(onRenameSpace)}
+                    onDeleteSpace={hideManagerAndCall(onDeleteSpace)}
+                    onDuplicateSpace={hideManagerAndCall(onDuplicateSpace)}
                 />
             )}
         </div>
diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx
index 8879c15..e04cd7a 100644
--- a/src/editor/editor.tsx
+++ b/src/editor/editor.tsx
@@ -1,6 +1,7 @@
 import React from "react";
 import { DynamicGraph } from "./graph";
 import {
+    deleteGraphJson,
     listAllSpaces,
     loadGraphJson,
     saveGraphJson,
@@ -10,7 +11,6 @@ import "./editor.css";
 import * as Helpers from "../common/helpers";
 import { Node, NodeProperties } from "../common/graph/node";
 
-import { SpaceManager } from "./components/spacemanager";
 import SelectLayer from "./components/selectlayer";
 import { Coordinate2D, GraphData, SimGraphData } from "../common/graph/graph";
 import { NodeType, NodeTypeData } from "../common/graph/nodetype";
@@ -70,6 +70,7 @@ type stateTypes = {
  */
 export class Editor extends React.PureComponent<any, stateTypes> {
     private rendererRef: React.RefObject<GraphRenderer2D>;
+    private defaultSpaceId = "Graph";
 
     constructor(props: any) {
         super(props);
@@ -77,6 +78,10 @@ export class Editor extends React.PureComponent<any, stateTypes> {
         // 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);
@@ -107,7 +112,7 @@ export class Editor extends React.PureComponent<any, stateTypes> {
 
         this.rendererRef = React.createRef();
 
-        listAllSpaces().then((spaces) => this.setState({ spaces: spaces }));
+        this.loadListOfSpaces();
 
         // Set as new state
         this.state = {
@@ -140,10 +145,22 @@ export class Editor extends React.PureComponent<any, stateTypes> {
      * Tries to load initial graph after webpage finished loading.
      */
     componentDidMount() {
-        if (this.state.spaceId !== undefined) {
-            // Load initial space
-            this.loadSpace(this.state.spaceId);
-        }
+        // Try to load given spaceId from Config
+        // If not available, just load default space
+        const initialSpaceId = this.state.spaceId
+            ? this.state.spaceId
+            : this.defaultSpaceId;
+        this.loadSpace(initialSpaceId);
+    }
+
+    /**
+     * 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;
+        });
     }
 
     /**
@@ -151,13 +168,18 @@ export class Editor extends React.PureComponent<any, stateTypes> {
      * @param spaceId Id of space to load.
      * @returns Promise with boolean value that is true, if successful.
      */
-    public loadSpace(spaceId: string): any {
-        return loadGraphJson(spaceId).then((data: GraphData) =>
-            this.loadGraph(data, spaceId)
-        );
+    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() {
+    public saveSpace(): Promise<void> {
         return saveGraphJson(
             this.state.spaceId,
             this.state.graph.toJSONSerializableObject()
@@ -429,37 +451,45 @@ export class Editor extends React.PureComponent<any, stateTypes> {
     /**
      * @param newId Explicit id of space that should be deleted.
      */
-    private deleteSpace(spaceId: string) {
-        throw new Error(
-            'Function "deleteSpace(spaceId)" has not been implemented.'
-        );
+    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 selectSpaceId: string =
+                this.state.spaces.length > 0
+                    ? this.state.spaces[0]
+                    : this.defaultSpaceId;
+
+            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) {
-        throw new Error(
-            'Function "renameSpace(newId)" has not been implemented.'
-        );
+    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) {
-        throw new Error(
-            'Function "duplicateSpace(newId)" has not been implemented.'
-        );
+    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) {
-        throw new Error(
-            'Function "createSpace(newSpaceId)" has not been implemented.'
-        );
+    private createSpace(newSpaceId: string): Promise<boolean> {
+        return this.loadSpace(newSpaceId);
     }
 
     render(): React.ReactNode {
diff --git a/src/ks-datasets-database.php b/src/ks-datasets-database.php
index 39d9e69..4a7cd98 100644
--- a/src/ks-datasets-database.php
+++ b/src/ks-datasets-database.php
@@ -11,6 +11,14 @@ function ks_insert_or_update_graph($name, $graph)
     return ks_insert_space($name, $graph);
 }
 
+function ks_delete_graph($name)
+{
+    // Delete graph
+    global $SPACES_TABLE;
+    global $wpdb;
+    $wpdb->delete($SPACES_TABLE, array("name" => $name), array("%s"));
+}
+
 function ks_select_all_spaces()
 {
     global $SPACES_TABLE;
@@ -69,4 +77,4 @@ function ks_insert($table, $data)
     $wpdb->insert($table, $data);
 
     return $wpdb->insert_id;
-}
+}
\ No newline at end of file
-- 
GitLab