dify/web/app/components/workflow/hooks/use-workflow-history.ts
Stephen Zhou a84c2d36a3
style: format with vp fmt (#38803)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-12 15:57:46 +00:00

159 lines
6.2 KiB
TypeScript

import type { WorkflowHistoryEventMeta } from '../store/workflow/history-slice'
import { debounce } from 'es-toolkit/compat'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useStoreApi } from 'reactflow'
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 const WorkflowHistoryEvent = {
NodeTitleChange: 'NodeTitleChange',
NodeDescriptionChange: 'NodeDescriptionChange',
NodeDragStop: 'NodeDragStop',
NodeChange: 'NodeChange',
NodeConnect: 'NodeConnect',
NodePaste: 'NodePaste',
NodeDelete: 'NodeDelete',
EdgeDelete: 'EdgeDelete',
EdgeDeleteByDeleteBranch: 'EdgeDeleteByDeleteBranch',
EdgeSourceHandleChange: 'EdgeSourceHandleChange',
NodeAdd: 'NodeAdd',
NodeResize: 'NodeResize',
NoteAdd: 'NoteAdd',
NoteChange: 'NoteChange',
NoteDelete: 'NoteDelete',
LayoutOrganize: 'LayoutOrganize',
} as const
export type WorkflowHistoryEventT = keyof typeof WorkflowHistoryEvent
export const useWorkflowHistory = () => {
const store = useStoreApi()
const { store: workflowHistoryStore } = useWorkflowHistoryStore()
const { t } = useTranslation()
const [undoCallbacks, setUndoCallbacks] = useState<(() => void)[]>([])
const [redoCallbacks, setRedoCallbacks] = useState<(() => void)[]>([])
const onUndo = useCallback((callback: () => void) => {
setUndoCallbacks((prev) => [...prev, callback])
return () => setUndoCallbacks((prev) => prev.filter((cb) => cb !== callback))
}, [])
const onRedo = useCallback((callback: () => void) => {
setRedoCallbacks((prev) => [...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: WorkflowHistoryEventT, meta?: WorkflowHistoryEventMeta) => {
workflowHistoryStore.setState({
workflowHistoryEvent: event,
workflowHistoryEventMeta: meta,
nodes: store.getState().getNodes(),
edges: store.getState().edges,
})
}, 500),
)
const saveStateToHistory = useCallback(
(event: WorkflowHistoryEventT, meta?: WorkflowHistoryEventMeta) => {
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, meta)
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, meta)
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: WorkflowHistoryEventT) => {
switch (event) {
case WorkflowHistoryEvent.NodeTitleChange:
return t(($) => $['changeHistory.nodeTitleChange'], { ns: 'workflow' })
case WorkflowHistoryEvent.NodeDescriptionChange:
return t(($) => $['changeHistory.nodeDescriptionChange'], { ns: 'workflow' })
case WorkflowHistoryEvent.LayoutOrganize:
case WorkflowHistoryEvent.NodeDragStop:
return t(($) => $['changeHistory.nodeDragStop'], { ns: 'workflow' })
case WorkflowHistoryEvent.NodeChange:
return t(($) => $['changeHistory.nodeChange'], { ns: 'workflow' })
case WorkflowHistoryEvent.NodeConnect:
return t(($) => $['changeHistory.nodeConnect'], { ns: 'workflow' })
case WorkflowHistoryEvent.NodePaste:
return t(($) => $['changeHistory.nodePaste'], { ns: 'workflow' })
case WorkflowHistoryEvent.NodeDelete:
return t(($) => $['changeHistory.nodeDelete'], { ns: 'workflow' })
case WorkflowHistoryEvent.NodeAdd:
return t(($) => $['changeHistory.nodeAdd'], { ns: 'workflow' })
case WorkflowHistoryEvent.EdgeDelete:
case WorkflowHistoryEvent.EdgeDeleteByDeleteBranch:
return t(($) => $['changeHistory.edgeDelete'], { ns: 'workflow' })
case WorkflowHistoryEvent.NodeResize:
return t(($) => $['changeHistory.nodeResize'], { ns: 'workflow' })
case WorkflowHistoryEvent.NoteAdd:
return t(($) => $['changeHistory.noteAdd'], { ns: 'workflow' })
case WorkflowHistoryEvent.NoteChange:
return t(($) => $['changeHistory.noteChange'], { ns: 'workflow' })
case WorkflowHistoryEvent.NoteDelete:
return t(($) => $['changeHistory.noteDelete'], { ns: 'workflow' })
default:
return 'Unknown Event'
}
},
[t],
)
return {
store: workflowHistoryStore,
saveStateToHistory,
getHistoryLabel,
undo,
redo,
onUndo,
onRedo,
}
}