Skip to content

Commit 785d321

Browse files
committed
feat(hooks): add layout import/export functionality to useWorkflowState
Add exportLayout, exportLayoutAsString, downloadLayout, importLayout and copyLayoutToClipboard functions to enable saving and loading workflow layouts. Includes validation for imported layouts and metadata handling.
1 parent 31bbf34 commit 785d321

File tree

1 file changed

+154
-0
lines changed

1 file changed

+154
-0
lines changed

src/lib/src/hooks/useWorkflowState.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,153 @@ export function useWorkflowState(initialNodes = [], initialEdges = [], initialMe
126126
};
127127
}, [nodes, edges, lastUpdateTimestamp]);
128128

129+
// Export current React Flow layout as JSON
130+
const exportLayout = useCallback(() => {
131+
const layout = {
132+
version: '1.0',
133+
type: 'react-flow-layout',
134+
timestamp: new Date().toISOString(),
135+
metadata: {
136+
name: workflowMetadata.name || 'Untitled Workflow',
137+
description: workflowMetadata.description || '',
138+
version: workflowMetadata.version || '1.0',
139+
...workflowMetadata
140+
},
141+
nodes: nodes.map(node => ({
142+
id: node.id,
143+
type: node.type,
144+
position: node.position,
145+
data: node.data,
146+
// Include any other React Flow specific properties
147+
...(node.style && { style: node.style }),
148+
...(node.className && { className: node.className }),
149+
...(node.hidden !== undefined && { hidden: node.hidden }),
150+
...(node.selected !== undefined && { selected: node.selected }),
151+
...(node.dragging !== undefined && { dragging: node.dragging })
152+
})),
153+
edges: edges.map(edge => ({
154+
id: edge.id,
155+
source: edge.source,
156+
target: edge.target,
157+
type: edge.type,
158+
...(edge.label && { label: edge.label }),
159+
...(edge.style && { style: edge.style }),
160+
...(edge.className && { className: edge.className }),
161+
...(edge.hidden !== undefined && { hidden: edge.hidden }),
162+
...(edge.selected !== undefined && { selected: edge.selected }),
163+
...(edge.data && { data: edge.data }),
164+
...(edge.sourceHandle && { sourceHandle: edge.sourceHandle }),
165+
...(edge.targetHandle && { targetHandle: edge.targetHandle })
166+
}))
167+
};
168+
169+
return layout;
170+
}, [nodes, edges, workflowMetadata]);
171+
172+
// Export layout as JSON string
173+
const exportLayoutAsString = useCallback((pretty = true) => {
174+
const layout = exportLayout();
175+
return JSON.stringify(layout, null, pretty ? 2 : 0);
176+
}, [exportLayout]);
177+
178+
// Download layout as JSON file
179+
const downloadLayout = useCallback((filename) => {
180+
const layout = exportLayout();
181+
const jsonString = JSON.stringify(layout, null, 2);
182+
183+
const defaultFilename = (workflowMetadata.name || 'workflow')
184+
.toLowerCase()
185+
.replace(/[^a-z0-9]/g, '-')
186+
.replace(/-+/g, '-')
187+
.replace(/^-|-$/g, '');
188+
189+
const blob = new Blob([jsonString], { type: 'application/json' });
190+
const url = URL.createObjectURL(blob);
191+
const a = document.createElement('a');
192+
a.href = url;
193+
a.download = `${filename || defaultFilename}-layout.json`;
194+
document.body.appendChild(a);
195+
a.click();
196+
document.body.removeChild(a);
197+
URL.revokeObjectURL(url);
198+
}, [exportLayout, workflowMetadata.name]);
199+
200+
// Import layout from JSON data
201+
const importLayout = useCallback((layoutData) => {
202+
try {
203+
let layout;
204+
205+
if (typeof layoutData === 'string') {
206+
layout = JSON.parse(layoutData);
207+
} else {
208+
layout = layoutData;
209+
}
210+
211+
// Validate layout structure
212+
if (!layout || typeof layout !== 'object') {
213+
throw new Error('Invalid layout data: must be an object');
214+
}
215+
216+
if (!Array.isArray(layout.nodes)) {
217+
throw new Error('Invalid layout data: nodes must be an array');
218+
}
219+
220+
if (!Array.isArray(layout.edges)) {
221+
throw new Error('Invalid layout data: edges must be an array');
222+
}
223+
224+
// Validate node structure
225+
layout.nodes.forEach((node, index) => {
226+
if (!node.id || !node.type) {
227+
throw new Error(`Invalid node at index ${index}: must have id and type`);
228+
}
229+
if (!node.position || typeof node.position.x !== 'number' || typeof node.position.y !== 'number') {
230+
throw new Error(`Invalid node at index ${index}: must have valid position with x and y coordinates`);
231+
}
232+
});
233+
234+
// Validate edge structure
235+
layout.edges.forEach((edge, index) => {
236+
if (!edge.id || !edge.source || !edge.target) {
237+
throw new Error(`Invalid edge at index ${index}: must have id, source, and target`);
238+
}
239+
});
240+
241+
// Update state with imported layout
242+
setNodes(layout.nodes);
243+
setEdges(layout.edges);
244+
if (layout.metadata) {
245+
setWorkflowMetadata(layout.metadata);
246+
}
247+
248+
return {
249+
nodes: layout.nodes,
250+
edges: layout.edges,
251+
metadata: layout.metadata || {},
252+
layoutInfo: {
253+
version: layout.version,
254+
type: layout.type,
255+
timestamp: layout.timestamp
256+
}
257+
};
258+
} catch (error) {
259+
console.error('Error importing layout:', error);
260+
throw new Error(`Failed to import layout: ${error.message}`);
261+
}
262+
}, []);
263+
264+
// Copy layout to clipboard
265+
const copyLayoutToClipboard = useCallback(async () => {
266+
try {
267+
const layoutString = exportLayoutAsString();
268+
await navigator.clipboard.writeText(layoutString);
269+
return true;
270+
} catch (error) {
271+
console.error('Failed to copy layout to clipboard:', error);
272+
throw error;
273+
}
274+
}, [exportLayoutAsString]);
275+
129276
return {
130277
// Current state
131278
nodes,
@@ -154,6 +301,13 @@ export function useWorkflowState(initialNodes = [], initialEdges = [], initialMe
154301
// Workflow utilities
155302
getWorkflowStats,
156303

304+
// Layout management
305+
exportLayout,
306+
exportLayoutAsString,
307+
downloadLayout,
308+
importLayout,
309+
copyLayoutToClipboard,
310+
157311
// React Flow instance (for advanced usage)
158312
reactFlowInstance,
159313
};

0 commit comments

Comments
 (0)