2024-06-26 08:37:12 +02:00
|
|
|
import {
|
|
|
|
|
useCallback,
|
|
|
|
|
useRef, useState,
|
|
|
|
|
} from 'react'
|
|
|
|
|
import { debounce } from 'lodash-es'
|
|
|
|
|
import {
|
|
|
|
|
useStoreApi,
|
|
|
|
|
} from 'reactflow'
|
|
|
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
|
import { useWorkflowHistoryStore } from '../workflow-history-store'
|
2025-09-09 15:18:42 +08:00
|
|
|
import type { WorkflowHistoryEventMeta } from '../workflow-history-store'
|
2024-06-26 08:37:12 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* All supported Events that create a new history state.
|
|
|
|
|
* Current limitations:
|
|
|
|
|
* - InputChange events in Node Panels do not trigger state changes.
|
|
|
|
|
* - Resizing UI elements does not trigger state changes.
|
|
|
|
|
*/
|
2025-10-07 14:21:08 +08:00
|
|
|
export const WorkflowHistoryEvent = {
|
|
|
|
|
NodeTitleChange: 'NodeTitleChange',
|
|
|
|
|
NodeDescriptionChange: 'NodeDescriptionChange',
|
|
|
|
|
NodeDragStop: 'NodeDragStop',
|
|
|
|
|
NodeChange: 'NodeChange',
|
|
|
|
|
NodeConnect: 'NodeConnect',
|
|
|
|
|
NodePaste: 'NodePaste',
|
|
|
|
|
NodeDelete: 'NodeDelete',
|
|
|
|
|
EdgeDelete: 'EdgeDelete',
|
|
|
|
|
EdgeDeleteByDeleteBranch: 'EdgeDeleteByDeleteBranch',
|
|
|
|
|
NodeAdd: 'NodeAdd',
|
|
|
|
|
NodeResize: 'NodeResize',
|
|
|
|
|
NoteAdd: 'NoteAdd',
|
|
|
|
|
NoteChange: 'NoteChange',
|
|
|
|
|
NoteDelete: 'NoteDelete',
|
|
|
|
|
LayoutOrganize: 'LayoutOrganize',
|
|
|
|
|
} as const
|
|
|
|
|
|
|
|
|
|
export type WorkflowHistoryEventT = keyof typeof WorkflowHistoryEvent
|
2024-06-26 08:37:12 +02:00
|
|
|
|
|
|
|
|
export const useWorkflowHistory = () => {
|
|
|
|
|
const store = useStoreApi()
|
|
|
|
|
const { store: workflowHistoryStore } = useWorkflowHistoryStore()
|
|
|
|
|
const { t } = useTranslation()
|
|
|
|
|
|
2025-10-09 21:36:42 +08:00
|
|
|
const [undoCallbacks, setUndoCallbacks] = useState<(() => void)[]>([])
|
|
|
|
|
const [redoCallbacks, setRedoCallbacks] = useState<(() => void)[]>([])
|
2024-06-26 08:37:12 +02:00
|
|
|
|
2025-10-09 21:36:42 +08:00
|
|
|
const onUndo = useCallback((callback: () => void) => {
|
|
|
|
|
setUndoCallbacks(prev => [...prev, callback])
|
2024-06-26 08:37:12 +02:00
|
|
|
return () => setUndoCallbacks(prev => prev.filter(cb => cb !== callback))
|
|
|
|
|
}, [])
|
|
|
|
|
|
2025-10-09 21:36:42 +08:00
|
|
|
const onRedo = useCallback((callback: () => void) => {
|
|
|
|
|
setRedoCallbacks(prev => [...prev, callback])
|
2024-06-26 08:37:12 +02:00
|
|
|
return () => setRedoCallbacks(prev => prev.filter(cb => cb !== callback))
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const undo = useCallback(() => {
|
|
|
|
|
workflowHistoryStore.temporal.getState().undo()
|
|
|
|
|
undoCallbacks.forEach(callback => callback())
|
|
|
|
|
}, [undoCallbacks, workflowHistoryStore.temporal])
|
|
|
|
|
|
|
|
|
|
const redo = useCallback(() => {
|
|
|
|
|
workflowHistoryStore.temporal.getState().redo()
|
|
|
|
|
redoCallbacks.forEach(callback => callback())
|
|
|
|
|
}, [redoCallbacks, workflowHistoryStore.temporal])
|
|
|
|
|
|
|
|
|
|
// Some events may be triggered multiple times in a short period of time.
|
|
|
|
|
// We debounce the history state update to avoid creating multiple history states
|
|
|
|
|
// with minimal changes.
|
2025-10-07 14:21:08 +08:00
|
|
|
const saveStateToHistoryRef = useRef(debounce((event: WorkflowHistoryEventT, meta?: WorkflowHistoryEventMeta) => {
|
2024-06-26 08:37:12 +02:00
|
|
|
workflowHistoryStore.setState({
|
|
|
|
|
workflowHistoryEvent: event,
|
2025-09-09 15:18:42 +08:00
|
|
|
workflowHistoryEventMeta: meta,
|
2024-06-26 08:37:12 +02:00
|
|
|
nodes: store.getState().getNodes(),
|
|
|
|
|
edges: store.getState().edges,
|
|
|
|
|
})
|
|
|
|
|
}, 500))
|
|
|
|
|
|
2025-10-07 14:21:08 +08:00
|
|
|
const saveStateToHistory = useCallback((event: WorkflowHistoryEventT, meta?: WorkflowHistoryEventMeta) => {
|
2024-06-26 08:37:12 +02:00
|
|
|
switch (event) {
|
|
|
|
|
case WorkflowHistoryEvent.NoteChange:
|
|
|
|
|
// Hint: Note change does not trigger when note text changes,
|
|
|
|
|
// because the note editors have their own history states.
|
2025-09-09 15:18:42 +08:00
|
|
|
saveStateToHistoryRef.current(event, meta)
|
2024-06-26 08:37:12 +02:00
|
|
|
break
|
|
|
|
|
case WorkflowHistoryEvent.NodeTitleChange:
|
|
|
|
|
case WorkflowHistoryEvent.NodeDescriptionChange:
|
|
|
|
|
case WorkflowHistoryEvent.NodeDragStop:
|
|
|
|
|
case WorkflowHistoryEvent.NodeChange:
|
|
|
|
|
case WorkflowHistoryEvent.NodeConnect:
|
|
|
|
|
case WorkflowHistoryEvent.NodePaste:
|
|
|
|
|
case WorkflowHistoryEvent.NodeDelete:
|
|
|
|
|
case WorkflowHistoryEvent.EdgeDelete:
|
|
|
|
|
case WorkflowHistoryEvent.EdgeDeleteByDeleteBranch:
|
|
|
|
|
case WorkflowHistoryEvent.NodeAdd:
|
|
|
|
|
case WorkflowHistoryEvent.NodeResize:
|
|
|
|
|
case WorkflowHistoryEvent.NoteAdd:
|
|
|
|
|
case WorkflowHistoryEvent.LayoutOrganize:
|
|
|
|
|
case WorkflowHistoryEvent.NoteDelete:
|
2025-09-09 15:18:42 +08:00
|
|
|
saveStateToHistoryRef.current(event, meta)
|
2024-06-26 08:37:12 +02:00
|
|
|
break
|
|
|
|
|
default:
|
|
|
|
|
// We do not create a history state for every event.
|
|
|
|
|
// Some events of reactflow may change things the user would not want to undo/redo.
|
|
|
|
|
// For example: UI state changes like selecting a node.
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
2025-10-07 14:21:08 +08:00
|
|
|
const getHistoryLabel = useCallback((event: WorkflowHistoryEventT) => {
|
2024-06-26 08:37:12 +02:00
|
|
|
switch (event) {
|
|
|
|
|
case WorkflowHistoryEvent.NodeTitleChange:
|
|
|
|
|
return t('workflow.changeHistory.nodeTitleChange')
|
|
|
|
|
case WorkflowHistoryEvent.NodeDescriptionChange:
|
|
|
|
|
return t('workflow.changeHistory.nodeDescriptionChange')
|
|
|
|
|
case WorkflowHistoryEvent.LayoutOrganize:
|
|
|
|
|
case WorkflowHistoryEvent.NodeDragStop:
|
|
|
|
|
return t('workflow.changeHistory.nodeDragStop')
|
|
|
|
|
case WorkflowHistoryEvent.NodeChange:
|
|
|
|
|
return t('workflow.changeHistory.nodeChange')
|
|
|
|
|
case WorkflowHistoryEvent.NodeConnect:
|
|
|
|
|
return t('workflow.changeHistory.nodeConnect')
|
|
|
|
|
case WorkflowHistoryEvent.NodePaste:
|
|
|
|
|
return t('workflow.changeHistory.nodePaste')
|
|
|
|
|
case WorkflowHistoryEvent.NodeDelete:
|
|
|
|
|
return t('workflow.changeHistory.nodeDelete')
|
|
|
|
|
case WorkflowHistoryEvent.NodeAdd:
|
|
|
|
|
return t('workflow.changeHistory.nodeAdd')
|
|
|
|
|
case WorkflowHistoryEvent.EdgeDelete:
|
|
|
|
|
case WorkflowHistoryEvent.EdgeDeleteByDeleteBranch:
|
|
|
|
|
return t('workflow.changeHistory.edgeDelete')
|
|
|
|
|
case WorkflowHistoryEvent.NodeResize:
|
|
|
|
|
return t('workflow.changeHistory.nodeResize')
|
|
|
|
|
case WorkflowHistoryEvent.NoteAdd:
|
|
|
|
|
return t('workflow.changeHistory.noteAdd')
|
|
|
|
|
case WorkflowHistoryEvent.NoteChange:
|
|
|
|
|
return t('workflow.changeHistory.noteChange')
|
|
|
|
|
case WorkflowHistoryEvent.NoteDelete:
|
|
|
|
|
return t('workflow.changeHistory.noteDelete')
|
|
|
|
|
default:
|
|
|
|
|
return 'Unknown Event'
|
|
|
|
|
}
|
|
|
|
|
}, [t])
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
store: workflowHistoryStore,
|
|
|
|
|
saveStateToHistory,
|
|
|
|
|
getHistoryLabel,
|
|
|
|
|
undo,
|
|
|
|
|
redo,
|
|
|
|
|
onUndo,
|
|
|
|
|
onRedo,
|
|
|
|
|
}
|
|
|
|
|
}
|