Added functional ability to save and open workflows
This commit is contained in:
parent
467225d89e
commit
b1c7424026
@ -77,7 +77,6 @@ const CustomNode = ({ data }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// FlowEditor with drag-and-drop and updated behavior
|
|
||||||
function FlowEditor({ nodes, edges, setNodes, setEdges }) {
|
function FlowEditor({ nodes, edges, setNodes, setEdges }) {
|
||||||
const reactFlowWrapper = useRef(null);
|
const reactFlowWrapper = useRef(null);
|
||||||
const { project } = useReactFlow();
|
const { project } = useReactFlow();
|
||||||
@ -202,10 +201,10 @@ export default function App() {
|
|||||||
const [nodes, setNodes] = useState([]);
|
const [nodes, setNodes] = useState([]);
|
||||||
const [edges, setEdges] = useState([]);
|
const [edges, setEdges] = useState([]);
|
||||||
const [confirmCloseOpen, setConfirmCloseOpen] = useState(false);
|
const [confirmCloseOpen, setConfirmCloseOpen] = useState(false);
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
const handleAboutMenuOpen = (event) => setAboutAnchorEl(event.currentTarget);
|
const handleAboutMenuOpen = (event) => setAboutAnchorEl(event.currentTarget);
|
||||||
const handleAboutMenuClose = () => setAboutAnchorEl(null);
|
const handleAboutMenuClose = () => setAboutAnchorEl(null);
|
||||||
|
|
||||||
const handleOpenCloseDialog = () => setConfirmCloseOpen(true);
|
const handleOpenCloseDialog = () => setConfirmCloseOpen(true);
|
||||||
const handleCloseDialog = () => setConfirmCloseOpen(false);
|
const handleCloseDialog = () => setConfirmCloseOpen(false);
|
||||||
const handleConfirmCloseWorkflow = () => {
|
const handleConfirmCloseWorkflow = () => {
|
||||||
@ -214,21 +213,59 @@ export default function App() {
|
|||||||
setConfirmCloseOpen(false);
|
setConfirmCloseOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddTestNode = () => {
|
const handleSaveWorkflow = async () => {
|
||||||
const id = `test-node-${Date.now()}`;
|
const data = JSON.stringify({ nodes, edges }, null, 2);
|
||||||
const newNode = {
|
const blob = new Blob([data], { type: "application/json" });
|
||||||
id,
|
|
||||||
type: "custom",
|
if (window.showSaveFilePicker) {
|
||||||
data: {
|
try {
|
||||||
label: "Custom Node",
|
const fileHandle = await window.showSaveFilePicker({
|
||||||
content: "Placeholder"
|
suggestedName: "workflow.json",
|
||||||
},
|
types: [{
|
||||||
position: {
|
description: "Workflow JSON File",
|
||||||
x: 250 + Math.random() * 300,
|
accept: { "application/json": [".json"] }
|
||||||
y: 150 + Math.random() * 200
|
}]
|
||||||
|
});
|
||||||
|
const writable = await fileHandle.createWritable();
|
||||||
|
await writable.write(blob);
|
||||||
|
await writable.close();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Save cancelled or failed:", error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = "workflow.json";
|
||||||
|
a.style.display = "none";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
document.body.removeChild(a);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
setNodes((nds) => [...nds, newNode]);
|
|
||||||
|
const handleOpenWorkflow = async () => {
|
||||||
|
if (window.showOpenFilePicker) {
|
||||||
|
try {
|
||||||
|
const [fileHandle] = await window.showOpenFilePicker({
|
||||||
|
types: [{
|
||||||
|
description: "Workflow JSON File",
|
||||||
|
accept: { "application/json": [".json"] }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
const file = await fileHandle.getFile();
|
||||||
|
const text = await file.text();
|
||||||
|
const { nodes: loadedNodes, edges: loadedEdges } = JSON.parse(text);
|
||||||
|
const confirm = window.confirm("Opening a workflow will overwrite your current one. Continue?");
|
||||||
|
if (!confirm) return;
|
||||||
|
setNodes(loadedNodes);
|
||||||
|
setEdges(loadedEdges);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Open cancelled or failed:", error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -272,15 +309,9 @@ export default function App() {
|
|||||||
<Typography align="left" sx={{ fontSize: "0.9rem", color: "#0475c2" }}><b>Workflows</b></Typography>
|
<Typography align="left" sx={{ fontSize: "0.9rem", color: "#0475c2" }}><b>Workflows</b></Typography>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails sx={{ p: 0 }}>
|
<AccordionDetails sx={{ p: 0 }}>
|
||||||
<Button fullWidth sx={sidebarBtnStyle}>SAVE WORKFLOW</Button>
|
<Button fullWidth sx={sidebarBtnStyle} onClick={handleSaveWorkflow}>SAVE WORKFLOW</Button>
|
||||||
<Button fullWidth sx={sidebarBtnStyle}>OPEN WORKFLOW</Button>
|
<Button fullWidth sx={sidebarBtnStyle} onClick={handleOpenWorkflow}>OPEN WORKFLOW</Button>
|
||||||
<Button
|
<Button fullWidth sx={sidebarBtnStyle} onClick={handleOpenCloseDialog}>CLOSE WORKFLOW</Button>
|
||||||
fullWidth
|
|
||||||
sx={sidebarBtnStyle}
|
|
||||||
onClick={handleOpenCloseDialog}
|
|
||||||
>
|
|
||||||
CLOSE WORKFLOW
|
|
||||||
</Button>
|
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
@ -316,23 +347,12 @@ export default function App() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box component="footer" sx={{
|
<Box component="footer" sx={{ bgcolor: "#1e1e1e", color: "white", px: 2, py: 1, textAlign: "left" }}>
|
||||||
bgcolor: "#1e1e1e",
|
|
||||||
color: "white",
|
|
||||||
px: 2,
|
|
||||||
py: 1,
|
|
||||||
textAlign: "left"
|
|
||||||
}}>
|
|
||||||
<b>Nodes</b>: <span id="nodeCount">0</span> | <b>Update Rate</b>: 500ms
|
<b>Nodes</b>: <span id="nodeCount">0</span> | <b>Update Rate</b>: 500ms
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Confirmation Dialog */}
|
<Dialog open={confirmCloseOpen} onClose={handleCloseDialog} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
|
||||||
<Dialog
|
|
||||||
open={confirmCloseOpen}
|
|
||||||
onClose={handleCloseDialog}
|
|
||||||
PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff" } }}
|
|
||||||
>
|
|
||||||
<DialogTitle>Close Workflow?</DialogTitle>
|
<DialogTitle>Close Workflow?</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText sx={{ color: "#ccc" }}>
|
<DialogContentText sx={{ color: "#ccc" }}>
|
||||||
@ -340,14 +360,31 @@ export default function App() {
|
|||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleCloseDialog} sx={{ color: "#58a6ff" }}>
|
<Button onClick={handleCloseDialog} sx={{ color: "#58a6ff" }}>Cancel</Button>
|
||||||
Cancel
|
<Button onClick={handleConfirmCloseWorkflow} sx={{ color: "#ff4f4f" }}>Close Workflow</Button>
|
||||||
</Button>
|
|
||||||
<Button onClick={handleConfirmCloseWorkflow} sx={{ color: "#ff4f4f" }}>
|
|
||||||
Close Workflow
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".json,application/json"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const { nodes: loadedNodes, edges: loadedEdges } = JSON.parse(text);
|
||||||
|
const confirm = window.confirm("Opening a workflow will overwrite your current one. Continue?");
|
||||||
|
if (!confirm) return;
|
||||||
|
setNodes(loadedNodes);
|
||||||
|
setEdges(loadedEdges);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to read file:", err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
17
Data/WebUI/src/index.js
Normal file
17
Data/WebUI/src/index.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App.jsx';
|
||||||
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals();
|
80
Workflows/Test.json
Normal file
80
Workflows/Test.json
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "node-1743304886846",
|
||||||
|
"type": "custom",
|
||||||
|
"position": {
|
||||||
|
"x": 214.63333129882812,
|
||||||
|
"y": 146.66666666666666
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"label": "Custom Node",
|
||||||
|
"content": "Placeholder"
|
||||||
|
},
|
||||||
|
"width": 160,
|
||||||
|
"height": 72
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "node-1743304888268",
|
||||||
|
"type": "custom",
|
||||||
|
"position": {
|
||||||
|
"x": 621.2999979654948,
|
||||||
|
"y": 276.6666666666667
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"label": "Custom Node",
|
||||||
|
"content": "Placeholder"
|
||||||
|
},
|
||||||
|
"width": 160,
|
||||||
|
"height": 72
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "node-1743304891251",
|
||||||
|
"type": "custom",
|
||||||
|
"position": {
|
||||||
|
"x": 814.6333312988281,
|
||||||
|
"y": 65.33333333333334
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"label": "Custom Node",
|
||||||
|
"content": "Placeholder"
|
||||||
|
},
|
||||||
|
"width": 160,
|
||||||
|
"height": 72,
|
||||||
|
"selected": true,
|
||||||
|
"positionAbsolute": {
|
||||||
|
"x": 814.6333312988281,
|
||||||
|
"y": 65.33333333333334
|
||||||
|
},
|
||||||
|
"dragging": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"source": "node-1743304886846",
|
||||||
|
"sourceHandle": null,
|
||||||
|
"target": "node-1743304888268",
|
||||||
|
"targetHandle": null,
|
||||||
|
"type": "smoothstep",
|
||||||
|
"animated": true,
|
||||||
|
"style": {
|
||||||
|
"strokeDasharray": "6 3",
|
||||||
|
"stroke": "#58a6ff"
|
||||||
|
},
|
||||||
|
"id": "reactflow__edge-node-1743304886846-node-1743304888268"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "node-1743304886846",
|
||||||
|
"sourceHandle": null,
|
||||||
|
"target": "node-1743304891251",
|
||||||
|
"targetHandle": null,
|
||||||
|
"type": "smoothstep",
|
||||||
|
"animated": true,
|
||||||
|
"style": {
|
||||||
|
"strokeDasharray": "6 3",
|
||||||
|
"stroke": "#58a6ff"
|
||||||
|
},
|
||||||
|
"id": "reactflow__edge-node-1743304886846-node-1743304891251"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user