diff --git a/src/GraphArea.jsx b/src/GraphArea.jsx
index a1e0154..39027c4 100644
--- a/src/GraphArea.jsx
+++ b/src/GraphArea.jsx
@@ -4,7 +4,8 @@ import MyGraph from './graph-builder';
import { actionType as T } from './reducer';
function Graph({
- el, superState, dispatcher, graphID, serverID, graphML, projectName, graphContainerRef, active, authorName,
+ el, superState, dispatcher, graphID, serverID, graphML, importedJson,
+ projectName, graphContainerRef, active, authorName,
}) {
const [instance, setInstance] = useState(null);
const ref = useRef();
@@ -34,6 +35,7 @@ function Graph({
myGraph.forcePullFromServer();
}
if (graphML) myGraph.setGraphML(graphML);
+ if (importedJson) myGraph.loadJson(importedJson);
myGraph.setCurStatus();
myGraph.cy.on('zoom', () => {
dispatcher({ type: T.SET_ZOOM_LEVEL, payload: (myGraph.cy.zoom() * 100).toFixed(0) });
diff --git a/src/GraphWorkspace.jsx b/src/GraphWorkspace.jsx
index 1565287..53e3504 100644
--- a/src/GraphWorkspace.jsx
+++ b/src/GraphWorkspace.jsx
@@ -70,6 +70,7 @@ const GraphComp = (props) => {
graphID={el.graphID}
serverID={el.serverID}
graphML={el.graphML}
+ importedJson={el.importedJson || null}
projectName={el.projectName}
fileHandle={el.fileHandle}
fileName={el.fileName}
@@ -87,7 +88,10 @@ const GraphComp = (props) => {
if (superState.confirmModal.onConfirm) superState.confirmModal.onConfirm();
dispatcher({ type: T.SET_CONFIRM_MODAL, payload: { open: false, message: '', onConfirm: null } });
}}
- onCancel={() => dispatcher({ type: T.SET_CONFIRM_MODAL, payload: { open: false, message: '', onConfirm: null } })}
+ onCancel={() => dispatcher({
+ type: T.SET_CONFIRM_MODAL,
+ payload: { open: false, message: '', onConfirm: null },
+ })}
/>
);
diff --git a/src/component/File-drag-drop.jsx b/src/component/File-drag-drop.jsx
index f99e3b8..bde6938 100644
--- a/src/component/File-drag-drop.jsx
+++ b/src/component/File-drag-drop.jsx
@@ -39,9 +39,10 @@ const app = ({ superState, dispatcher }) => {
const onDrop = (e) => {
e.preventDefault();
fileRef.current.value = null;
- if (e.dataTransfer.files.length === 1
- && e.dataTransfer.files[0].name.split('.').slice(-1)[0] === 'graphml') {
- readFile(superStateRef.current, dispatcherRef.current, e.dataTransfer.files[0]);
+ const droppedFile = e.dataTransfer.files[0];
+ const ext = droppedFile && droppedFile.name.split('.').slice(-1)[0];
+ if (e.dataTransfer.files.length === 1 && (ext === 'graphml' || ext === 'json')) {
+ readFile(superStateRef.current, dispatcherRef.current, droppedFile);
}
};
@@ -69,8 +70,8 @@ const app = ({ superState, dispatcher }) => {
ref={fileRef}
onClick={(e) => { e.target.value = null; }}
style={{ display: 'none' }}
- accept=".graphml"
- onChange={(e) => readFile(superState, dispatcher, e)}
+ accept=".graphml,.json"
+ onChange={(e) => readFile(superState, dispatcher, e.target.files[0])}
/>
⟶
-
-
-
-
- {historyView.map((h, i) => (
-
- {h}
-
- ))}
-
-
+
+
+
+
+
+ {historyView.map((h, i) => (
+
+ {h}
+
+ ))}
+
+
+
-
>
);
diff --git a/src/graph-builder/graph-core/5-load-save.js b/src/graph-builder/graph-core/5-load-save.js
index 98c9675..a042ce9 100644
--- a/src/graph-builder/graph-core/5-load-save.js
+++ b/src/graph-builder/graph-core/5-load-save.js
@@ -205,11 +205,17 @@ class GraphLoadSave extends GraphUndoRedo {
content.edges.forEach((edge) => {
this.addEdge({ ...edge, sourceID: edge.source, targetID: edge.target }, 0);
});
- content.actionHistory.forEach(({
- inverse, equivalent, tid,
- }) => {
- this.addAction(GraphLoadSave.parseAction(inverse), GraphLoadSave.parseAction(equivalent), tid);
- });
+ if (content.actionHistory && content.actionHistory.length) {
+ content.actionHistory.forEach(({
+ inverse, equivalent, tid,
+ }) => {
+ this.addAction(
+ GraphLoadSave.parseAction(inverse),
+ GraphLoadSave.parseAction(equivalent),
+ tid,
+ );
+ });
+ }
this.setProjectName(content.projectName);
this.setServerID(this.serverID || content.serverID);
this.setProjectAuthor(content.authorName);
diff --git a/src/reducer/reducer.js b/src/reducer/reducer.js
index ca244e3..ebadbe3 100644
--- a/src/reducer/reducer.js
+++ b/src/reducer/reducer.js
@@ -101,6 +101,7 @@ const reducer = (state, action) => {
graphID,
serverID: action.payload.serverID,
graphML: action.payload.graphML,
+ importedJson: action.payload.importedJson || null,
fileHandle: action.payload.fileHandle || null,
fileName: action.payload.fileName,
authorName: action.payload.authorName || '',
diff --git a/src/toolbarActions/toolbarFunctions.js b/src/toolbarActions/toolbarFunctions.js
index 282e9ec..5093ec2 100644
--- a/src/toolbarActions/toolbarFunctions.js
+++ b/src/toolbarActions/toolbarFunctions.js
@@ -1,3 +1,4 @@
+import { saveAs } from 'file-saver';
import { toast } from 'react-toastify';
import parser from '../graph-builder/graphml/parser';
import { actionType as T } from '../reducer';
@@ -84,6 +85,41 @@ const saveAction = (state) => {
getGraphFun(state).saveToDisk();
};
+const saveAsJson = (state) => {
+ if (!getGraphFun(state)) {
+ toast.error('No graph open to export.');
+ return;
+ }
+ try {
+ const graphJson = getGraphFun(state).jsonifyGraph();
+ const cleanExport = {
+ projectName: graphJson.projectName || 'Untitled',
+ authorName: graphJson.authorName || '',
+ nodes: graphJson.nodes.map((n) => ({
+ id: n.id,
+ label: n.label,
+ position: n.position,
+ style: n.style,
+ })),
+ edges: graphJson.edges.map((e) => ({
+ id: e.id,
+ label: e.label,
+ source: e.source,
+ target: e.target,
+ style: e.style,
+ })),
+ };
+ const str = JSON.stringify(cleanExport, null, 2);
+ const bytes = new TextEncoder().encode(str);
+ const blob = new Blob([bytes], { type: 'application/json;charset=utf-8' });
+ const fileName = `${cleanExport.projectName}.json`;
+ saveAs(blob, fileName);
+ toast.success('Exported as JSON successfully!');
+ } catch (error) {
+ toast.error('Failed to export JSON.');
+ }
+};
+
async function saveGraphMLFile(state) {
if (state.curGraphInstance) {
const graph = state.graphs[state.curGraphIndex];
@@ -114,7 +150,8 @@ const readFile = async (state, setState, file, fileHandle) => {
}
const fr = new FileReader();
const projectName = file.name;
- if (file.name.split('.').pop() === 'graphml') {
+ const ext = file.name.split('.').pop();
+ if (ext === 'graphml') {
fr.onload = (x) => {
parser(x.target.result).then(({ authorName }) => {
setState({
@@ -127,6 +164,26 @@ const readFile = async (state, setState, file, fileHandle) => {
};
if (fileHandle) fr.readAsText(await fileHandle.getFile());
else fr.readAsText(file);
+ } else if (ext === 'json') {
+ fr.onload = (x) => {
+ try {
+ const parsed = JSON.parse(x.target.result);
+ setState({
+ type: T.ADD_GRAPH,
+ payload: {
+ projectName: parsed.projectName || file.name,
+ graphML: null,
+ fileHandle: null,
+ fileName: file.name,
+ authorName: parsed.authorName || '',
+ importedJson: parsed,
+ },
+ });
+ } catch {
+ toast.error('Invalid JSON file.');
+ }
+ };
+ fr.readAsText(file);
}
}
};
@@ -232,5 +289,5 @@ export {
createFile, readFile, readTextFile, newProject, clearAll, editDetails, undo, redo,
openShareModal, openSettingModal, viewHistory, resetAfterClear, toggleLogs,
copySelected, pasteClipboard,
- toggleServer, optionModalToggle, contribute, openSearchPanel,
+ toggleServer, optionModalToggle, contribute, openSearchPanel, saveAsJson,
};
diff --git a/src/toolbarActions/toolbarList.js b/src/toolbarActions/toolbarList.js
index 572dff7..77ae50c 100644
--- a/src/toolbarActions/toolbarList.js
+++ b/src/toolbarActions/toolbarList.js
@@ -14,6 +14,7 @@ import {
createNode, editElement, deleteElem, downloadImg, saveAction, saveGraphMLFile,
createFile, readFile, clearAll, undo, redo, viewHistory, resetAfterClear,
toggleServer, optionModalToggle, toggleLogs, contribute, copySelected, pasteClipboard, openSearchPanel,
+ saveAsJson,
// openSettingModal,
} from './toolbarFunctions';
@@ -303,6 +304,7 @@ const toolbarList = (state, dispatcher) => [
action: (s, d) => [
{ fn: () => downloadImg(s, d, 'JPG'), name: 'JPG' },
{ fn: () => downloadImg(s, d, 'PNG'), name: 'PNG' },
+ { fn: () => saveAsJson(s, d), name: 'JSON' },
],
visibility: true,
active: state.curGraphInstance,