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' /** * 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. */ export enum 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', } export const useWorkflowHistory = () => { const store = useStoreApi() const { store: workflowHistoryStore } = useWorkflowHistoryStore() const { t } = useTranslation() const [undoCallbacks, setUndoCallbacks] = useState([]) const [redoCallbacks, setRedoCallbacks] = useState([]) const onUndo = useCallback((callback: unknown) => { setUndoCallbacks((prev: any) => [...prev, callback]) return () => setUndoCallbacks(prev => prev.filter(cb => cb !== callback)) }, []) const onRedo = useCallback((callback: unknown) => { setRedoCallbacks((prev: any) => [...prev, callback]) 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. const saveStateToHistoryRef = useRef(debounce((event: WorkflowHistoryEvent) => { workflowHistoryStore.setState({ workflowHistoryEvent: event, nodes: store.getState().getNodes(), edges: store.getState().edges, }) }, 500)) const saveStateToHistory = useCallback((event: WorkflowHistoryEvent) => { switch (event) { case WorkflowHistoryEvent.NoteChange: // Hint: Note change does not trigger when note text changes, // because the note editors have their own history states. saveStateToHistoryRef.current(event) 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: saveStateToHistoryRef.current(event) 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 } }, []) const getHistoryLabel = useCallback((event: WorkflowHistoryEvent) => { 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, } }