diff --git a/src/backend.tsx b/src/backend.tsx index 6a61d787d1ab0aeaf813026fb9f652879acc2d17..59c15664d7af66ddfc80aa78c68faa42df818c91 100644 --- a/src/backend.tsx +++ b/src/backend.tsx @@ -1,9 +1,8 @@ import { Editor } from "./editor/editor"; -import * as Config from "./config"; import { createRoot } from "react-dom/client"; import React from "react"; const container = document.getElementById("knowledge-space-editor"); const root = createRoot(container); -root.render(<Editor spaceId={Config.SPACE} />); +root.render(<Editor />); diff --git a/src/common/history.ts b/src/common/history.ts index 7f1c931c6b16a6e55d9bcaae4c45f7684f5ff9a1..e7b3dbb8239509eb7feda6d2c7ac0a9372ad8bac 100644 --- a/src/common/history.ts +++ b/src/common/history.ts @@ -1,6 +1,6 @@ import { SerializableItem } from "./serializableitem"; -interface SavePoint<DataType> { +export interface Checkpoint<DataType> { id: number; description: string; data: DataType; @@ -11,7 +11,7 @@ export class History<HistoryDataType> { private current: number; private data: SerializableItem<unknown, HistoryDataType>; - public checkpoints: SavePoint<HistoryDataType>[]; + public checkpoints: Checkpoint<HistoryDataType>[]; private next: number; constructor( @@ -22,7 +22,7 @@ export class History<HistoryDataType> { this.data = data; this.maxCheckpoints = maxCheckpoints; this.checkpoints = []; - this.current = 0; + this.current = -1; this.next = 0; this.createCheckpoint(initialMessage); } @@ -36,9 +36,12 @@ export class History<HistoryDataType> { }; // Remove potential history which is not relevant anymore (maybe caused by undo ops) - this.checkpoints.length = ++this.current; + if (this.current < this.checkpoints.length - 1 && this.current > 0) { + this.checkpoints.length = this.current + 1; + } this.checkpoints.push(checkpoint); + this.current++; } public get currentCheckpoint() { @@ -49,7 +52,7 @@ export class History<HistoryDataType> { return this.checkpoints.map((savepoint) => savepoint.description); } - public resetToCheckpoint(id: number): SavePoint<HistoryDataType> { + public resetToCheckpoint(id: number): Checkpoint<HistoryDataType> { const index = this.checkpoints.findIndex( (checkpoint) => checkpoint.id == id ); @@ -60,14 +63,14 @@ export class History<HistoryDataType> { } } - public undo(): SavePoint<HistoryDataType> { + public undo(): Checkpoint<HistoryDataType> { if (this.hasUndoCheckpoints()) { this.current--; } return this.checkpoints[this.current]; } - public redo(): SavePoint<HistoryDataType> { + public redo(): Checkpoint<HistoryDataType> { if (this.hasRedoCheckpoints()) { this.current++; } diff --git a/src/editor/components/historynavigator.tsx b/src/editor/components/historynavigator.tsx index 4fd76f9bbd775f1d3c9c83cd8fc87abd648777c1..67b74ddc4cfa6c3f991822a6bc3f22b879b57d16 100644 --- a/src/editor/components/historynavigator.tsx +++ b/src/editor/components/historynavigator.tsx @@ -1,104 +1,42 @@ -import React, { ChangeEvent } from "react"; -import { saveGraphJson } from "../../common/datasets"; +import React from "react"; import "./historynavigator.css"; -import { History } from "../../common/history"; +import { Checkpoint, History } from "../../common/history"; -type propTypes = { - history: History<any>; - spaceId: string; - onChange: { (): void }; -}; - -export class HistoryNavigator extends React.Component<propTypes> { - constructor(props: propTypes) { - super(props); - - this.handleUndo = this.handleUndo.bind(this); - this.handleRedo = this.handleRedo.bind(this); - this.handleSave = this.handleSave.bind(this); - this.handleSelectSavepoint = this.handleSelectSavepoint.bind(this); - } - - /** - * Undo a step in the history object. - */ - handleUndo() { - this.props.history.undo(); - } - - /** - * Redo a step in the hisory object. - */ - handleRedo() { - this.props.history.redo(); - } - - /** - * Saves current data of history object. - */ - handleSave() { - this.props.history.createCheckpoint("Saved graph."); - - saveGraphJson( - this.props.spaceId, - this.props.history.currentCheckpoint.data - ); - // TODO: Save successful? - // this.props.historyObject.markChangesAsSaved(); TODO: Reimplement - alert( - "Saved! (Though not for sure, currently not checking for success of failure)" - ); - this.forceUpdate(); - } - - /** - * Loads selected savepoint into history object. - */ - handleSelectSavepoint(e: ChangeEvent<HTMLSelectElement>) { - if (!this.props.history.resetToCheckpoint(Number(e.target.value))) { - alert("Something went wrong, could not load savepoint."); - } else { - this.props.onChange(); - } - } +interface HistoryNavigatorProps<HistoryDataType> { + history: History<HistoryDataType>; + onCheckpointLoad: { (checkpoint: Checkpoint<HistoryDataType>): void }; +} - render(): React.ReactNode { - return ( - <div id="history-navigator"> - <button onClick={this.handleUndo}>Undo</button> - <button onClick={this.handleRedo}>Redo</button> - <select - onChange={this.handleSelectSavepoint} - value={ - this.props.history - ? this.props.history.currentCheckpoint.id - : 0 - } - > - {this.props.history - ? this.props.history.checkpoints.map((savepoint) => { - return ( - <option - key={savepoint.id} - value={savepoint.id} - > - {savepoint.description} - </option> - ); - }) - : ""} - </select> - <button - onClick={this.handleSave} - // disabled={ TODO: Reimplement - // this.props.history - // ? !this.props.history.hasUnsavedChanges() - // : true - // } - > - Save - </button> - </div> - ); - } +function HistoryNavigator<HistoryDataType>({ + history, + onCheckpointLoad, +}: HistoryNavigatorProps<HistoryDataType>) { + return ( + <div id="history-navigator"> + <button onClick={() => onCheckpointLoad(history.undo())}> + Undo + </button> + <button onClick={() => onCheckpointLoad(history.redo())}> + Redo + </button> + <select + onChange={(e) => + onCheckpointLoad( + history.resetToCheckpoint(Number(e.target.value)) + ) + } + value={history.currentCheckpoint.id} + > + {history.checkpoints.map((checkpoint) => { + return ( + <option key={checkpoint.id} value={checkpoint.id}> + {checkpoint.description} + </option> + ); + })} + </select> + </div> + ); } + +export default HistoryNavigator; diff --git a/src/editor/components/nodetypeseditor.tsx b/src/editor/components/nodetypeseditor.tsx index a1432a940a9da57a2ed2dde061e7fab08b623bf7..65321bf91042380a35875a37aa60b08155ba9e62 100644 --- a/src/editor/components/nodetypeseditor.tsx +++ b/src/editor/components/nodetypeseditor.tsx @@ -28,6 +28,7 @@ export class NodeTypesEditor extends React.Component<propTypes> { return ( <div id="node-types-editor"> + <h3>Node types</h3> <ul> {this.props.graph.objectGroups.map((type) => ( <NodeTypeEntry diff --git a/src/editor/components/spaceselect.tsx b/src/editor/components/spaceselect.tsx index d3bfac1cd1816ad711646a3ad7ef53a6ba9cc62f..31c9fd3a193f9d8ae55166643810e1ead02d2dba 100644 --- a/src/editor/components/spaceselect.tsx +++ b/src/editor/components/spaceselect.tsx @@ -1,62 +1,27 @@ -import React, { ChangeEvent } from "react"; -import { ReactNode } from "react"; -import { listAllSpaces } from "../../common/datasets"; +import React from "react"; import "./spaceselect.css"; -type propTypes = { +interface SpaceSelectProps { onLoadSpace: (spaceId: string) => boolean; -}; - -type stateTypes = { spaces: string[]; -}; - -export class SpaceSelect extends React.Component<propTypes, stateTypes> { - constructor(props: propTypes) { - super(props); - this.state = { - spaces: [], - }; - - this.handleSelectChange = this.handleSelectChange.bind(this); - this.updateSpaceList = this.updateSpaceList.bind(this); - - listAllSpaces().then(this.updateSpaceList); - } - - /** - * Render list of spaces as available options. - * @param spaceList Array containing all available space names. - */ - updateSpaceList(spaceList: string[]) { - this.setState({ - spaces: spaceList, - }); - } - - /** - * Triggered when another option (space) is selected. Calls given function to load specified space. - * @param e Select-event that references the select element and allows to access the new value (space). - */ - handleSelectChange(e: ChangeEvent<HTMLSelectElement>) { - const success = this.props.onLoadSpace(e.target.value); - - if (!success) { - alert("Failed to load space with id [" + e.target.value + "]"); - } - } + spaceId: string; +} - render(): ReactNode { - return ( - <div id="spaceselect"> - <select onChange={this.handleSelectChange}> - {this.state.spaces.map((spaceName: string) => ( - <option key={spaceName} value={spaceName}> - {spaceName} - </option> - ))} - </select> - </div> - ); - } +function SpaceSelect({ onLoadSpace, spaces, spaceId }: SpaceSelectProps) { + return ( + <div id="spaceselect"> + <select + onChange={(event) => onLoadSpace(event.target.value)} + defaultValue={spaceId} + > + {spaces.map((spaceName: string) => ( + <option key={spaceName} value={spaceName}> + {spaceName} + </option> + ))} + </select> + </div> + ); } + +export default SpaceSelect; diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx index ce020796570184f2e0efff1f6f0019dd1b458c72..3fbd28466e69f443cca4f8e1125344942992dbcf 100644 --- a/src/editor/editor.tsx +++ b/src/editor/editor.tsx @@ -1,8 +1,8 @@ import React from "react"; import { DynamicGraph } from "./graph"; -import { loadGraphJson } from "../common/datasets"; +import { listAllSpaces, loadGraphJson } from "../common/datasets"; import { NodeDetails } from "./components/nodedetails"; -import { SpaceSelect } from "./components/spaceselect"; +import SpaceSelect from "./components/spaceselect"; import "./editor.css"; import * as Helpers from "../common/helpers"; import { Node } from "../common/graph/node"; @@ -15,10 +15,10 @@ import { NodeType } from "../common/graph/nodetype"; import { GraphRenderer2D } from "./renderer"; import Instructions from "./components/instructions"; import Settings from "./components/settings"; +import HistoryNavigator from "./components/historynavigator"; +import * as Config from "../config"; -type propTypes = { - spaceId: string; -}; +type propTypes = {}; type stateTypes = { /** * Graph structure holding the basic information. @@ -49,6 +49,10 @@ type stateTypes = { * Collection of all currently selected nodes. Can also be undefined or empty. */ selectedNodes: Node[]; + + spaces: string[]; + + spaceId: string; }; /** @@ -78,6 +82,8 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { this.graphContainer = React.createRef(); + listAllSpaces().then((spaces) => this.setState({ spaces: spaces })); + // Set as new state this.state = { graph: undefined, @@ -86,6 +92,8 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { graphWidth: 1000, selectedNodes: [], keys: {}, + spaces: [], + spaceId: Config.SPACE, }; } @@ -105,9 +113,9 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { * Tries to load initial graph after webpage finished loading. */ componentDidMount() { - if (this.props.spaceId !== undefined) { + if (this.state.spaceId !== undefined) { // Load initial space - this.loadSpace(this.props.spaceId); + this.loadSpace(this.state.spaceId); } } @@ -205,14 +213,20 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { this.selectNodes(nodesWithType); } + private onNodeDataChange() {} + render(): React.ReactNode { return ( <div id="ks-editor"> <h1>Interface</h1> - <SpaceSelect onLoadSpace={this.loadSpace} /> + <SpaceSelect + onLoadSpace={this.loadSpace} + spaces={this.state.spaces} + spaceId={this.state.spaceId} + /> <SpaceManager /> - <div id="content"> - {this.state.graph && ( + {this.state.graph && ( + <div id="content"> <div id="force-graph-renderer" ref={this.graphContainer} @@ -241,26 +255,25 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { /> </SelectLayer> </div> - )} - {this.state.graph && ( <div id="sidepanel"> - {/*<HistoryNavigator*/} - {/* spaceId="space"*/} - {/* history={this.state.graph.history}*/} - {/* onChange={this.onGraphDataChange}*/} - {/*/>*/} + <HistoryNavigator + history={this.state.graph.history} + onCheckpointLoad={(checkpoint) => { + const graph = new DynamicGraph(); + this.setState({ + graph: graph.fromSerializedObject( + checkpoint.data + ), + }); + }} + /> <hr /> <NodeDetails selectedNodes={this.state.selectedNodes} - allTypes={ - this.state.graph - ? this.state.graph.objectGroups - : [] - } + allTypes={this.state.graph.objectGroups} onChange={this.forceUpdate} /> <hr /> - <h3>Node types</h3> <NodeTypesEditor onChange={this.forceUpdate} graph={this.state.graph} @@ -270,7 +283,9 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { <Settings labelVisibility={this.state.visibleLabels} onLabelVisibilityChange={(visible) => - this.setState({ visibleLabels: visible }) + this.setState({ + visibleLabels: visible, + }) } connectOnDrag={this.state.connectOnDrag} onConnectOnDragChange={(connectOnDrag) => @@ -285,8 +300,8 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { connectOnDragEnabled={this.state.connectOnDrag} /> </div> - )} - </div> + </div> + )} </div> ); }