@@ -62,7 +62,6 @@ interface EntryPointActionsContextValue {
6262const EntryPointActionsContext = createContext < EntryPointActionsContextValue > ( { } )
6363export const useEntryPointActions = ( ) => useContext ( EntryPointActionsContext )
6464
65- const MAX_DELETE_HISTORY = 10
6665const ENTRY_COMPONENT_ID = 'core.workflow.entrypoint'
6766const ENTRY_COMPONENT_SLUG = 'entry-point'
6867
@@ -76,10 +75,7 @@ const isEntryPointNode = (node?: Node<NodeData> | null) => {
7675 return isEntryPointComponentRef ( componentRef )
7776}
7877
79- interface DeleteHistoryEntry {
80- nodes : Node < NodeData > [ ]
81- edges : Edge [ ]
82- }
78+
8379
8480interface CanvasProps {
8581 className ?: string
@@ -102,6 +98,7 @@ interface CanvasProps {
10298 onCloseScheduleSidebar ?: ( ) => void
10399 onClearNodeSelection ?: ( ) => void
104100 onNodeSelectionChange ?: ( node : Node < NodeData > | null ) => void
101+ onSnapshot ?: ( nodes ?: Node < NodeData > [ ] , edges ?: Edge [ ] ) => void
105102}
106103
107104export function Canvas ( {
@@ -125,6 +122,7 @@ export function Canvas({
125122 onCloseScheduleSidebar,
126123 onClearNodeSelection,
127124 onNodeSelectionChange,
125+ onSnapshot,
128126} : CanvasProps ) {
129127 const [ reactFlowInstance , setReactFlowInstance ] = useState < any > ( null )
130128 const [ selectedNode , setSelectedNode ] = useState < Node < NodeData > | null > ( null )
@@ -155,8 +153,9 @@ export function Canvas({
155153 const resolvedOnOpenScheduleSidebar = onOpenScheduleSidebar ?? scheduleContext ?. onOpenScheduleSidebar
156154 const resolvedOnCloseScheduleSidebar = onCloseScheduleSidebar ?? scheduleContext ?. onCloseScheduleSidebar
157155 const applyEdgesChange = onEdgesChange
158- const deleteHistoryRef = useRef < DeleteHistoryEntry [ ] > ( [ ] )
156+
159157 const hasUserInteractedRef = useRef ( false )
158+ const snapshotDebounceRef = useRef < ReturnType < typeof setTimeout > | null > ( null )
160159 const prevModeRef = useRef < typeof mode > ( mode )
161160 const prevNodesLengthRef = useRef ( nodes . length )
162161 const prevEdgesLengthRef = useRef ( edges . length )
@@ -309,36 +308,44 @@ export function Canvas({
309308 isHighlighted : selectedNodeId === params . source || selectedNodeId === params . target ,
310309 } ,
311310 }
312- setEdges ( ( eds ) => addEdge ( newEdge , eds ) )
311+
312+ // Calculate new edges
313+ const newEdges = addEdge ( newEdge , edges )
314+ setEdges ( newEdges )
315+
316+ // Calculate new nodes (if input mapping update is needed)
317+ let nextNodes = nodes
313318
314319 // Update target node's input mapping
315320 if ( params . target && params . targetHandle && params . source && params . sourceHandle ) {
316321 const targetHandle = params . targetHandle
317- setNodes ( ( nds ) =>
318- nds . map ( ( node ) =>
319- node . id === params . target
320- ? {
321- ...node ,
322- data : {
323- ...node . data ,
324- inputs : {
325- ...( node . data . inputs as Record < string , unknown > ) ,
326- [ targetHandle ] : {
327- source : params . source ,
328- output : params . sourceHandle ,
329- } ,
330- } as Record < string , unknown > ,
331- } ,
332- }
333- : node
334- )
322+ nextNodes = nodes . map ( ( node ) =>
323+ node . id === params . target
324+ ? {
325+ ...node ,
326+ data : {
327+ ...node . data ,
328+ inputs : {
329+ ...( node . data . inputs as Record < string , unknown > ) ,
330+ [ targetHandle ] : {
331+ source : params . source ,
332+ output : params . sourceHandle ,
333+ } ,
334+ } as Record < string , unknown > ,
335+ } ,
336+ }
337+ : node
335338 )
339+ setNodes ( nextNodes )
336340 }
337341
342+ // Capture snapshot for history
343+ onSnapshot ?.( nextNodes , newEdges )
344+
338345 // Mark workflow as dirty
339346 markDirty ( )
340347 } ,
341- [ setEdges , setNodes , nodes , edges , getComponent , markDirty , mode , toast ]
348+ [ setEdges , setNodes , nodes , edges , getComponent , markDirty , mode , toast , selectedNodeId , onSnapshot ]
342349 )
343350
344351 // Fit view on initial load, when nodes/edges are added/removed, or when switching modes
@@ -493,7 +500,10 @@ export function Canvas({
493500 } as any ,
494501 }
495502
496- setNodes ( ( nds ) => nds . concat ( newNode ) )
503+ // Update nodes and capture snapshot
504+ const nextNodes = nodes . concat ( newNode )
505+ setNodes ( nextNodes )
506+ onSnapshot ?.( nextNodes , edges )
497507
498508 // Analytics: node added
499509 try {
@@ -507,7 +517,7 @@ export function Canvas({
507517 // Mark workflow as dirty
508518 markDirty ( )
509519 } ,
510- [ reactFlowInstance , setNodes , getComponent , markDirty , mode , nodes , toast , workflowId ]
520+ [ reactFlowInstance , setNodes , getComponent , markDirty , mode , nodes , edges , toast , workflowId , onSnapshot ]
511521 )
512522
513523 const onDrop = useCallback (
@@ -633,17 +643,27 @@ export function Canvas({
633643
634644 // Handle node data update from config panel
635645 const handleUpdateNode = useCallback ( ( nodeId : string , data : Partial < NodeData > ) => {
636- setNodes ( ( nds ) =>
637- nds . map ( ( node ) =>
638- node . id === nodeId
639- ? { ...node , data : { ...node . data , ...data } }
640- : node
641- )
646+ const newNodes = nodes . map ( ( node ) =>
647+ node . id === nodeId
648+ ? { ...node , data : { ...node . data , ...data } }
649+ : node
642650 )
643651
644- // Mark workflow as dirty
652+ setNodes ( newNodes )
653+
654+ // Debounce history snapshot to avoid creating history entries for every keystroke
655+ if ( snapshotDebounceRef . current ) {
656+ clearTimeout ( snapshotDebounceRef . current )
657+ }
658+
659+ snapshotDebounceRef . current = setTimeout ( ( ) => {
660+ onSnapshot ?.( newNodes , edges )
661+ snapshotDebounceRef . current = null
662+ } , 500 )
663+
664+ // Mark workflow as dirty immediately so Save button enables
645665 markDirty ( )
646- } , [ setNodes , markDirty ] )
666+ } , [ nodes , edges , setNodes , markDirty , onSnapshot ] )
647667
648668 // Sync selectedNode with the latest node data from nodes array
649669 useEffect ( ( ) => {
@@ -709,52 +729,7 @@ export function Canvas({
709729 return
710730 }
711731
712- const isUndoShortcut =
713- ( event . key === 'z' || event . key === 'Z' ) &&
714- ( event . metaKey || event . ctrlKey ) &&
715- ! event . shiftKey
716732
717- if ( isUndoShortcut ) {
718- event . preventDefault ( )
719- const lastDeletion = deleteHistoryRef . current . pop ( )
720- if ( ! lastDeletion ) {
721- return
722- }
723-
724- if ( lastDeletion . nodes . length > 0 ) {
725- setNodes ( ( nds ) => {
726- const existingIds = new Set ( nds . map ( ( node ) => node . id ) )
727- const nodesToRestore = lastDeletion . nodes
728- . filter ( ( node ) => ! existingIds . has ( node . id ) )
729- . map ( ( node ) => ( { ...node , selected : false } ) )
730-
731- if ( nodesToRestore . length === 0 ) {
732- return nds
733- }
734-
735- return nds . concat ( nodesToRestore )
736- } )
737- }
738-
739- if ( lastDeletion . edges . length > 0 ) {
740- setEdges ( ( eds ) => {
741- const existingIds = new Set ( eds . map ( ( edge ) => edge . id ) )
742- const edgesToRestore = lastDeletion . edges
743- . filter ( ( edge ) => ! existingIds . has ( edge . id ) )
744- . map ( ( edge ) => ( { ...edge , selected : false } ) )
745-
746- if ( edgesToRestore . length === 0 ) {
747- return eds
748- }
749-
750- return eds . concat ( edgesToRestore )
751- } )
752- }
753-
754- setSelectedNode ( null )
755- markDirty ( )
756- return
757- }
758733
759734 if ( event . key === 'Delete' || event . key === 'Backspace' ) {
760735 const target = event . target
@@ -797,17 +772,7 @@ export function Canvas({
797772 dedupedEdges . set ( edge . id , { ...edge , selected : false } )
798773 } )
799774
800- const historyEntryNodes = selectedNodes . map ( ( node ) => ( { ...node , selected : false } ) )
801- const historyEntryEdges = Array . from ( dedupedEdges . values ( ) )
802775
803- if ( historyEntryNodes . length > 0 || historyEntryEdges . length > 0 ) {
804- const history = deleteHistoryRef . current . slice ( - ( MAX_DELETE_HISTORY - 1 ) )
805- history . push ( {
806- nodes : historyEntryNodes ,
807- edges : historyEntryEdges ,
808- } )
809- deleteHistoryRef . current = history
810- }
811776
812777 if ( selectedNodes . length > 0 ) {
813778 setNodes ( ( nds ) => nds . filter ( ( node ) => ! nodeIds . has ( node . id ) ) )
0 commit comments