Skip to content

Commit 446ceee

Browse files
added functioanlity for URL sharing
1 parent 7fd78cf commit 446ceee

File tree

5 files changed

+420
-2
lines changed

5 files changed

+420
-2
lines changed

docs/usage.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,37 @@ Export to python
201201
For advanced users, PathView allows you to export your graph as a Python script. This feature is useful for integrating your simulation into larger Python projects or for further analysis using Python libraries.
202202

203203
This is useful for instance for performing parametric studies or sensitivity analysis, where you can easily modify parameters in the Python script and rerun the simulation.
204+
205+
Sharing Graphs via URL
206+
----------------------
207+
208+
PathView supports sharing complete graph configurations through URLs, making collaboration and graph distribution easy.
209+
210+
**How to share a graph:**
211+
212+
1. Create and configure your graph with all necessary nodes, connections, and parameters
213+
2. Click the "🔗 Share URL" button in the floating action buttons (top-right area)
214+
3. The complete graph URL is automatically copied to your clipboard
215+
4. Share this URL with others - when they visit it, your exact graph configuration will load automatically
216+
217+
**What's included in shared URLs:**
218+
219+
- All node positions and configurations
220+
- Edge connections and data flow
221+
- Solver parameters and simulation settings
222+
- Global variables and their values
223+
- Event definitions
224+
- Custom Python code
225+
226+
**Best practices:**
227+
228+
- URLs work best for moderately-sized graphs. For very complex graphs with many nodes, consider using the file save/load functionality instead
229+
- URLs contain all graph data encoded in base64, so they can become quite long
230+
- The shared graph state is completely self-contained - no server storage required
231+
232+
**Example use cases:**
233+
234+
- Sharing example configurations with students or colleagues
235+
- Creating bookmarks for frequently-used graph templates
236+
- Collaborating on model development
237+
- Including interactive models in documentation or presentations

src/App.jsx

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import {
99
import '@xyflow/react/dist/style.css';
1010
import './styles/App.css';
1111
import { getApiEndpoint } from './config.js';
12+
import {
13+
getGraphDataFromURL,
14+
generateShareableURL,
15+
updateURLWithGraphData,
16+
clearGraphDataFromURL
17+
} from './utils/urlSharing.js';
1218
import Sidebar from './components/Sidebar';
1319
import NodeSidebar from './components/NodeSidebar';
1420
import { DnDProvider, useDnD } from './components/DnDContext.jsx';
@@ -22,6 +28,7 @@ import GraphView from './components/GraphView.jsx';
2228
import EdgeDetails from './components/EdgeDetails.jsx';
2329
import SolverPanel from './components/SolverPanel.jsx';
2430
import ResultsPanel from './components/ResultsPanel.jsx';
31+
import ShareModal from './components/ShareModal.jsx';
2532

2633
// * Declaring variables *
2734

@@ -99,6 +106,62 @@ const DnDFlow = () => {
99106
// Python code editor state
100107
const [pythonCode, setPythonCode] = useState("# Define your Python variables and functions here\n# Example:\n# my_variable = 42\n# def my_function(x):\n# return x * 2\n");
101108

109+
// State for URL sharing feedback
110+
const [shareUrlFeedback, setShareUrlFeedback] = useState('');
111+
const [showShareModal, setShowShareModal] = useState(false);
112+
const [shareableURL, setShareableURL] = useState('');
113+
114+
// Load graph data from URL on component mount
115+
useEffect(() => {
116+
const loadGraphFromURL = () => {
117+
const urlGraphData = getGraphDataFromURL();
118+
if (urlGraphData) {
119+
try {
120+
// Validate that it's a valid graph file
121+
if (!urlGraphData.nodes || !Array.isArray(urlGraphData.nodes)) {
122+
console.warn("Invalid graph data in URL");
123+
return;
124+
}
125+
126+
// Load the graph data and ensure nodeColor exists on all nodes
127+
const {
128+
nodes: loadedNodes,
129+
edges: loadedEdges,
130+
nodeCounter: loadedNodeCounter,
131+
solverParams: loadedSolverParams,
132+
globalVariables: loadedGlobalVariables,
133+
events: loadedEvents,
134+
pythonCode: loadedPythonCode
135+
} = urlGraphData;
136+
137+
// Ensure all loaded nodes have a nodeColor property
138+
const nodesWithColors = (loadedNodes || []).map(node => ({
139+
...node,
140+
data: {
141+
...node.data,
142+
nodeColor: node.data.nodeColor || '#DDE6ED'
143+
}
144+
}));
145+
146+
setNodes(nodesWithColors);
147+
setEdges(loadedEdges || []);
148+
setSelectedNode(null);
149+
setNodeCounter(loadedNodeCounter ?? loadedNodes.length);
150+
setSolverParams(loadedSolverParams ?? DEFAULT_SOLVER_PARAMS);
151+
setGlobalVariables(loadedGlobalVariables ?? []);
152+
setEvents(loadedEvents ?? []);
153+
setPythonCode(loadedPythonCode ?? "# Define your Python variables and functions here\n# Example:\n# my_variable = 42\n# def my_function(x):\n# return x * 2\n");
154+
155+
console.log('Graph loaded from URL successfully');
156+
} catch (error) {
157+
console.error('Error loading graph from URL:', error);
158+
}
159+
}
160+
};
161+
162+
loadGraphFromURL();
163+
}, []); // Empty dependency array means this runs once on mount
164+
102165
const [defaultValues, setDefaultValues] = useState({});
103166
const [isEditingLabel, setIsEditingLabel] = useState(false);
104167
const [tempLabel, setTempLabel] = useState('');
@@ -500,7 +563,41 @@ const DnDFlow = () => {
500563
setNodeCounter(0);
501564
setSolverParams(DEFAULT_SOLVER_PARAMS);
502565
setGlobalVariables([]);
566+
// Clear URL when resetting graph
567+
clearGraphDataFromURL();
568+
};
569+
570+
// Share current graph via URL
571+
const shareGraphURL = async () => {
572+
const graphData = {
573+
version: versionInfo ? Object.fromEntries(Object.entries(versionInfo).filter(([key]) => key !== 'status')) : 'unknown',
574+
nodes,
575+
edges,
576+
nodeCounter,
577+
solverParams,
578+
globalVariables,
579+
events,
580+
pythonCode
581+
};
582+
583+
try {
584+
const url = generateShareableURL(graphData);
585+
if (url) {
586+
setShareableURL(url);
587+
setShowShareModal(true);
588+
// Update browser URL as well
589+
updateURLWithGraphData(graphData, true);
590+
} else {
591+
setShareUrlFeedback('Error generating share URL');
592+
setTimeout(() => setShareUrlFeedback(''), 3000);
593+
}
594+
} catch (error) {
595+
console.error('Error sharing graph URL:', error);
596+
setShareUrlFeedback('Error generating share URL');
597+
setTimeout(() => setShareUrlFeedback(''), 3000);
598+
}
503599
};
600+
504601
const downloadCsv = async () => {
505602
if (!csvData) return;
506603

@@ -1050,6 +1147,7 @@ const DnDFlow = () => {
10501147
selectedNode, selectedEdge,
10511148
deleteSelectedNode, deleteSelectedEdge,
10521149
saveGraph, loadGraph, resetGraph, saveToPython, runPathsim,
1150+
shareGraphURL,
10531151
dockOpen, setDockOpen, onToggleLogs,
10541152
showKeyboardShortcuts, setShowKeyboardShortcuts,
10551153
}}
@@ -1116,6 +1214,12 @@ const DnDFlow = () => {
11161214
/>
11171215
)}
11181216

1217+
{/* Share URL Modal */}
1218+
<ShareModal
1219+
isOpen={showShareModal}
1220+
onClose={() => setShowShareModal(false)}
1221+
shareableURL={shareableURL}
1222+
/>
11191223

11201224
</div>
11211225
);

src/components/GraphView.jsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ function FloatingButtons({
4949
selectedNode, selectedEdge,
5050
deleteSelectedNode, deleteSelectedEdge,
5151
saveGraph, loadGraph, resetGraph, saveToPython, runPathsim,
52+
shareGraphURL,
5253
dockOpen, onToggleLogs
5354
}) {
5455
return (
@@ -162,7 +163,27 @@ function FloatingButtons({
162163
style={{
163164
position: 'absolute',
164165
right: 20,
165-
top: 150,
166+
top: 143,
167+
zIndex: 10,
168+
padding: '8px 8px',
169+
backgroundColor: '#78A083',
170+
color: 'white',
171+
border: 'none',
172+
borderRadius: 5,
173+
cursor: 'pointer',
174+
display: 'flex',
175+
alignItems: 'center',
176+
gap: '6px',
177+
}}
178+
onClick={shareGraphURL}
179+
>
180+
🔗
181+
</button>
182+
<button
183+
style={{
184+
position: 'absolute',
185+
right: 20,
186+
top: 185,
166187
zIndex: 10,
167188
padding: '8px 12px',
168189
backgroundColor: '#78A083',
@@ -183,7 +204,7 @@ function FloatingButtons({
183204
style={{
184205
position: 'absolute',
185206
right: 20,
186-
top: 200,
207+
top: 230,
187208
zIndex: 10,
188209
padding: '8px 12px',
189210
backgroundColor: '#444',

src/components/ShareModal.jsx

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import React, { useState, useEffect } from 'react';
2+
3+
const ShareModal = ({ isOpen, onClose, shareableURL }) => {
4+
const [copyFeedback, setCopyFeedback] = useState('');
5+
6+
const handleCopy = async () => {
7+
try {
8+
await navigator.clipboard.writeText(shareableURL);
9+
setCopyFeedback('Copied!');
10+
} catch (error) {
11+
// Fallback for older browsers
12+
try {
13+
const textArea = document.createElement('textarea');
14+
textArea.value = shareableURL;
15+
document.body.appendChild(textArea);
16+
textArea.select();
17+
document.execCommand('copy');
18+
document.body.removeChild(textArea);
19+
setCopyFeedback('Copied!');
20+
} catch (fallbackError) {
21+
setCopyFeedback('Failed to copy');
22+
}
23+
}
24+
25+
// Clear feedback after 2 seconds
26+
setTimeout(() => setCopyFeedback(''), 2000);
27+
};
28+
29+
// Reset feedback when modal opens
30+
useEffect(() => {
31+
if (isOpen) {
32+
setCopyFeedback('');
33+
}
34+
}, [isOpen]);
35+
36+
if (!isOpen) return null;
37+
38+
return (
39+
<div
40+
style={{
41+
position: 'fixed',
42+
top: 0,
43+
left: 0,
44+
right: 0,
45+
bottom: 0,
46+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
47+
display: 'flex',
48+
alignItems: 'center',
49+
justifyContent: 'center',
50+
zIndex: 1000,
51+
}}
52+
onClick={onClose}
53+
>
54+
<div
55+
style={{
56+
backgroundColor: 'white',
57+
borderRadius: 8,
58+
padding: 24,
59+
maxWidth: 500,
60+
width: '90%',
61+
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
62+
position: 'relative',
63+
}}
64+
onClick={(e) => e.stopPropagation()}
65+
>
66+
{/* Close button */}
67+
<button
68+
onClick={onClose}
69+
style={{
70+
position: 'absolute',
71+
top: 12,
72+
right: 12,
73+
background: 'none',
74+
border: 'none',
75+
fontSize: 18,
76+
cursor: 'pointer',
77+
color: '#666',
78+
padding: 4,
79+
}}
80+
>
81+
×
82+
</button>
83+
84+
{/* Header */}
85+
<div style={{ marginBottom: 16 }}>
86+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
87+
88+
</div>
89+
<p style={{ margin: 0, color: '#666', fontSize: 14 }}>
90+
Copy this URL to share your workflow with others.
91+
</p>
92+
</div>
93+
94+
{/* URL input and copy button */}
95+
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
96+
<input
97+
type="text"
98+
value={shareableURL}
99+
readOnly
100+
style={{
101+
flex: 1,
102+
padding: '8px 12px',
103+
border: '1px solid #ddd',
104+
borderRadius: 4,
105+
fontSize: 14,
106+
backgroundColor: '#f9f9f9',
107+
color: '#333',
108+
}}
109+
onClick={(e) => e.target.select()}
110+
/>
111+
<button
112+
onClick={handleCopy}
113+
style={{
114+
padding: '8px 16px',
115+
backgroundColor: copyFeedback === 'Copied!' ? '#27ae60' : '#4A90E2',
116+
color: 'white',
117+
border: 'none',
118+
borderRadius: 4,
119+
cursor: 'pointer',
120+
fontSize: 14,
121+
fontWeight: 500,
122+
minWidth: 60,
123+
transition: 'background-color 0.2s',
124+
}}
125+
>
126+
{copyFeedback || 'Copy'}
127+
</button>
128+
</div>
129+
130+
{/* Additional info */}
131+
<div style={{ marginTop: 16, fontSize: 12, color: '#888' }}>
132+
<p style={{ margin: 0 }}>
133+
This URL contains your complete graph configuration including nodes, connections, parameters, and code.
134+
</p>
135+
</div>
136+
</div>
137+
</div>
138+
);
139+
};
140+
141+
export default ShareModal;

0 commit comments

Comments
 (0)