@@ -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 - z 0 - 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