mirror of
https://github.com/langgenius/dify.git
synced 2026-04-24 17:16:37 +08:00
add redo undo manager of CRDT
This commit is contained in:
parent
684f7df158
commit
294fc41aec
@ -1,4 +1,4 @@
|
|||||||
import { LoroDoc } from 'loro-crdt'
|
import { LoroDoc, UndoManager } from 'loro-crdt'
|
||||||
import { isEqual } from 'lodash-es'
|
import { isEqual } from 'lodash-es'
|
||||||
import { webSocketClient } from './websocket-manager'
|
import { webSocketClient } from './websocket-manager'
|
||||||
import { CRDTProvider } from './crdt-provider'
|
import { CRDTProvider } from './crdt-provider'
|
||||||
@ -8,6 +8,7 @@ import type { CollaborationState, CursorPosition, OnlineUser } from '../types/co
|
|||||||
|
|
||||||
export class CollaborationManager {
|
export class CollaborationManager {
|
||||||
private doc: LoroDoc | null = null
|
private doc: LoroDoc | null = null
|
||||||
|
private undoManager: UndoManager | null = null
|
||||||
private provider: CRDTProvider | null = null
|
private provider: CRDTProvider | null = null
|
||||||
private nodesMap: any = null
|
private nodesMap: any = null
|
||||||
private edgesMap: any = null
|
private edgesMap: any = null
|
||||||
@ -18,6 +19,7 @@ export class CollaborationManager {
|
|||||||
private isLeader = false
|
private isLeader = false
|
||||||
private leaderId: string | null = null
|
private leaderId: string | null = null
|
||||||
private activeConnections = new Set<string>()
|
private activeConnections = new Set<string>()
|
||||||
|
private isUndoRedoInProgress = false
|
||||||
|
|
||||||
init = (appId: string, reactFlowStore: any): void => {
|
init = (appId: string, reactFlowStore: any): void => {
|
||||||
if (!reactFlowStore) {
|
if (!reactFlowStore) {
|
||||||
@ -28,15 +30,31 @@ export class CollaborationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setNodes = (oldNodes: Node[], newNodes: Node[]): void => {
|
setNodes = (oldNodes: Node[], newNodes: Node[]): void => {
|
||||||
|
if (!this.doc) return
|
||||||
|
|
||||||
|
// Don't track operations during undo/redo to prevent loops
|
||||||
|
if (this.isUndoRedoInProgress) {
|
||||||
|
console.log('Skipping setNodes during undo/redo')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Setting nodes with tracking')
|
||||||
this.syncNodes(oldNodes, newNodes)
|
this.syncNodes(oldNodes, newNodes)
|
||||||
if (this.doc)
|
this.doc.commit()
|
||||||
this.doc.commit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setEdges = (oldEdges: Edge[], newEdges: Edge[]): void => {
|
setEdges = (oldEdges: Edge[], newEdges: Edge[]): void => {
|
||||||
|
if (!this.doc) return
|
||||||
|
|
||||||
|
// Don't track operations during undo/redo to prevent loops
|
||||||
|
if (this.isUndoRedoInProgress) {
|
||||||
|
console.log('Skipping setEdges during undo/redo')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Setting edges with tracking')
|
||||||
this.syncEdges(oldEdges, newEdges)
|
this.syncEdges(oldEdges, newEdges)
|
||||||
if (this.doc)
|
this.doc.commit()
|
||||||
this.doc.commit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy = (): void => {
|
destroy = (): void => {
|
||||||
@ -73,6 +91,54 @@ export class CollaborationManager {
|
|||||||
this.doc = new LoroDoc()
|
this.doc = new LoroDoc()
|
||||||
this.nodesMap = this.doc.getMap('nodes')
|
this.nodesMap = this.doc.getMap('nodes')
|
||||||
this.edgesMap = this.doc.getMap('edges')
|
this.edgesMap = this.doc.getMap('edges')
|
||||||
|
|
||||||
|
// Initialize UndoManager for collaborative undo/redo
|
||||||
|
this.undoManager = new UndoManager(this.doc, {
|
||||||
|
maxUndoSteps: 100,
|
||||||
|
mergeInterval: 500, // Merge operations within 500ms
|
||||||
|
excludeOriginPrefixes: [], // Don't exclude anything - let UndoManager track all local operations
|
||||||
|
onPush: (isUndo, range, event) => {
|
||||||
|
console.log('UndoManager onPush:', { isUndo, range, event })
|
||||||
|
// Store current selection state when an operation is pushed
|
||||||
|
const selectedNode = this.reactFlowStore?.getState().getNodes().find((n: Node) => n.data.selected)
|
||||||
|
|
||||||
|
// Emit event to update UI button states when new operation is pushed
|
||||||
|
setTimeout(() => {
|
||||||
|
this.eventEmitter.emit('undoRedoStateChange', {
|
||||||
|
canUndo: this.undoManager?.canUndo() || false,
|
||||||
|
canRedo: this.undoManager?.canRedo() || false,
|
||||||
|
})
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: {
|
||||||
|
selectedNodeId: selectedNode?.id || null,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
},
|
||||||
|
cursors: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPop: (isUndo, value, counterRange) => {
|
||||||
|
console.log('UndoManager onPop:', { isUndo, value, counterRange })
|
||||||
|
// Restore selection state when undoing/redoing
|
||||||
|
if (value?.value && typeof value.value === 'object' && 'selectedNodeId' in value.value && this.reactFlowStore) {
|
||||||
|
const selectedNodeId = (value.value as any).selectedNodeId
|
||||||
|
if (selectedNodeId) {
|
||||||
|
const { setNodes } = this.reactFlowStore.getState()
|
||||||
|
const nodes = this.reactFlowStore.getState().getNodes()
|
||||||
|
const newNodes = nodes.map((n: Node) => ({
|
||||||
|
...n,
|
||||||
|
data: {
|
||||||
|
...n.data,
|
||||||
|
selected: n.id === selectedNodeId,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
setNodes(newNodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
this.provider = new CRDTProvider(socket, this.doc)
|
this.provider = new CRDTProvider(socket, this.doc)
|
||||||
|
|
||||||
this.setupSubscriptions()
|
this.setupSubscriptions()
|
||||||
@ -98,6 +164,7 @@ export class CollaborationManager {
|
|||||||
webSocketClient.disconnect(this.currentAppId)
|
webSocketClient.disconnect(this.currentAppId)
|
||||||
|
|
||||||
this.provider?.destroy()
|
this.provider?.destroy()
|
||||||
|
this.undoManager = null
|
||||||
this.doc = null
|
this.doc = null
|
||||||
this.provider = null
|
this.provider = null
|
||||||
this.nodesMap = null
|
this.nodesMap = null
|
||||||
@ -105,6 +172,7 @@ export class CollaborationManager {
|
|||||||
this.currentAppId = null
|
this.currentAppId = null
|
||||||
this.reactFlowStore = null
|
this.reactFlowStore = null
|
||||||
this.cursors = {}
|
this.cursors = {}
|
||||||
|
this.isUndoRedoInProgress = false
|
||||||
|
|
||||||
// Only reset leader status when actually disconnecting
|
// Only reset leader status when actually disconnecting
|
||||||
const wasLeader = this.isLeader
|
const wasLeader = this.isLeader
|
||||||
@ -182,6 +250,10 @@ export class CollaborationManager {
|
|||||||
return this.eventEmitter.on('leaderChange', callback)
|
return this.eventEmitter.on('leaderChange', callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onUndoRedoStateChange(callback: (state: { canUndo: boolean; canRedo: boolean }) => void): () => void {
|
||||||
|
return this.eventEmitter.on('undoRedoStateChange', callback)
|
||||||
|
}
|
||||||
|
|
||||||
getLeaderId(): string | null {
|
getLeaderId(): string | null {
|
||||||
return this.leaderId
|
return this.leaderId
|
||||||
}
|
}
|
||||||
@ -190,6 +262,114 @@ export class CollaborationManager {
|
|||||||
return this.isLeader
|
return this.isLeader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collaborative undo/redo methods
|
||||||
|
undo(): boolean {
|
||||||
|
if (!this.undoManager) {
|
||||||
|
console.log('UndoManager not initialized')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const canUndo = this.undoManager.canUndo()
|
||||||
|
console.log('Can undo:', canUndo)
|
||||||
|
|
||||||
|
if (canUndo) {
|
||||||
|
this.isUndoRedoInProgress = true
|
||||||
|
const result = this.undoManager.undo()
|
||||||
|
|
||||||
|
// After undo, manually update React state from CRDT without triggering collaboration
|
||||||
|
if (result && this.reactFlowStore) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// Get ReactFlow's native setters, not the collaborative ones
|
||||||
|
const state = this.reactFlowStore.getState()
|
||||||
|
const updatedNodes = Array.from(this.nodesMap.values())
|
||||||
|
const updatedEdges = Array.from(this.edgesMap.values())
|
||||||
|
console.log('Manually updating React state after undo')
|
||||||
|
|
||||||
|
// Call ReactFlow's native setters directly to avoid triggering collaboration
|
||||||
|
state.setNodes(updatedNodes)
|
||||||
|
state.setEdges(updatedEdges)
|
||||||
|
|
||||||
|
this.isUndoRedoInProgress = false
|
||||||
|
|
||||||
|
// Emit event to update UI button states
|
||||||
|
this.eventEmitter.emit('undoRedoStateChange', {
|
||||||
|
canUndo: this.undoManager?.canUndo() || false,
|
||||||
|
canRedo: this.undoManager?.canRedo() || false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.isUndoRedoInProgress = false
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Undo result:', result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
redo(): boolean {
|
||||||
|
if (!this.undoManager) {
|
||||||
|
console.log('RedoManager not initialized')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const canRedo = this.undoManager.canRedo()
|
||||||
|
console.log('Can redo:', canRedo)
|
||||||
|
|
||||||
|
if (canRedo) {
|
||||||
|
this.isUndoRedoInProgress = true
|
||||||
|
const result = this.undoManager.redo()
|
||||||
|
|
||||||
|
// After redo, manually update React state from CRDT without triggering collaboration
|
||||||
|
if (result && this.reactFlowStore) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// Get ReactFlow's native setters, not the collaborative ones
|
||||||
|
const state = this.reactFlowStore.getState()
|
||||||
|
const updatedNodes = Array.from(this.nodesMap.values())
|
||||||
|
const updatedEdges = Array.from(this.edgesMap.values())
|
||||||
|
console.log('Manually updating React state after redo')
|
||||||
|
|
||||||
|
// Call ReactFlow's native setters directly to avoid triggering collaboration
|
||||||
|
state.setNodes(updatedNodes)
|
||||||
|
state.setEdges(updatedEdges)
|
||||||
|
|
||||||
|
this.isUndoRedoInProgress = false
|
||||||
|
|
||||||
|
// Emit event to update UI button states
|
||||||
|
this.eventEmitter.emit('undoRedoStateChange', {
|
||||||
|
canUndo: this.undoManager?.canUndo() || false,
|
||||||
|
canRedo: this.undoManager?.canRedo() || false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.isUndoRedoInProgress = false
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Redo result:', result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
canUndo(): boolean {
|
||||||
|
if (!this.undoManager) return false
|
||||||
|
return this.undoManager.canUndo()
|
||||||
|
}
|
||||||
|
|
||||||
|
canRedo(): boolean {
|
||||||
|
if (!this.undoManager) return false
|
||||||
|
return this.undoManager.canRedo()
|
||||||
|
}
|
||||||
|
|
||||||
|
clearUndoStack(): void {
|
||||||
|
if (!this.undoManager) return
|
||||||
|
this.undoManager.clear()
|
||||||
|
}
|
||||||
|
|
||||||
debugLeaderStatus(): void {
|
debugLeaderStatus(): void {
|
||||||
console.log('=== Leader Status Debug ===')
|
console.log('=== Leader Status Debug ===')
|
||||||
console.log('Current leader status:', this.isLeader)
|
console.log('Current leader status:', this.isLeader)
|
||||||
@ -334,21 +514,43 @@ export class CollaborationManager {
|
|||||||
|
|
||||||
private setupSubscriptions(): void {
|
private setupSubscriptions(): void {
|
||||||
this.nodesMap?.subscribe((event: any) => {
|
this.nodesMap?.subscribe((event: any) => {
|
||||||
|
console.log('nodesMap subscription event:', event)
|
||||||
if (event.by === 'import' && this.reactFlowStore) {
|
if (event.by === 'import' && this.reactFlowStore) {
|
||||||
|
// Don't update React nodes during undo/redo to prevent loops
|
||||||
|
if (this.isUndoRedoInProgress) {
|
||||||
|
console.log('Skipping nodes subscription update during undo/redo')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const { setNodes } = this.reactFlowStore.getState()
|
// Get ReactFlow's native setters, not the collaborative ones
|
||||||
|
const state = this.reactFlowStore.getState()
|
||||||
const updatedNodes = Array.from(this.nodesMap.values())
|
const updatedNodes = Array.from(this.nodesMap.values())
|
||||||
setNodes(updatedNodes)
|
console.log('Updating React nodes from subscription')
|
||||||
|
|
||||||
|
// Call ReactFlow's native setter directly to avoid triggering collaboration
|
||||||
|
state.setNodes(updatedNodes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.edgesMap?.subscribe((event: any) => {
|
this.edgesMap?.subscribe((event: any) => {
|
||||||
|
console.log('edgesMap subscription event:', event)
|
||||||
if (event.by === 'import' && this.reactFlowStore) {
|
if (event.by === 'import' && this.reactFlowStore) {
|
||||||
|
// Don't update React edges during undo/redo to prevent loops
|
||||||
|
if (this.isUndoRedoInProgress) {
|
||||||
|
console.log('Skipping edges subscription update during undo/redo')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const { setEdges } = this.reactFlowStore.getState()
|
// Get ReactFlow's native setters, not the collaborative ones
|
||||||
|
const state = this.reactFlowStore.getState()
|
||||||
const updatedEdges = Array.from(this.edgesMap.values())
|
const updatedEdges = Array.from(this.edgesMap.values())
|
||||||
setEdges(updatedEdges)
|
console.log('Updating React edges from subscription')
|
||||||
|
|
||||||
|
// Call ReactFlow's native setter directly to avoid triggering collaboration
|
||||||
|
state.setEdges(updatedEdges)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -6,27 +6,39 @@ import {
|
|||||||
RiArrowGoForwardFill,
|
RiArrowGoForwardFill,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import TipPopup from '../operator/tip-popup'
|
import TipPopup from '../operator/tip-popup'
|
||||||
import { useWorkflowHistoryStore } from '../workflow-history-store'
|
|
||||||
import Divider from '../../base/divider'
|
import Divider from '../../base/divider'
|
||||||
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
|
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
|
||||||
import ViewWorkflowHistory from '@/app/components/workflow/header/view-workflow-history'
|
import ViewWorkflowHistory from '@/app/components/workflow/header/view-workflow-history'
|
||||||
|
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
||||||
import classNames from '@/utils/classnames'
|
import classNames from '@/utils/classnames'
|
||||||
|
|
||||||
export type UndoRedoProps = { handleUndo: () => void; handleRedo: () => void }
|
export type UndoRedoProps = { handleUndo: () => void; handleRedo: () => void }
|
||||||
const UndoRedo: FC<UndoRedoProps> = ({ handleUndo, handleRedo }) => {
|
const UndoRedo: FC<UndoRedoProps> = ({ handleUndo, handleRedo }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { store } = useWorkflowHistoryStore()
|
|
||||||
const [buttonsDisabled, setButtonsDisabled] = useState({ undo: true, redo: true })
|
const [buttonsDisabled, setButtonsDisabled] = useState({ undo: true, redo: true })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = store.temporal.subscribe((state) => {
|
// Update button states based on Loro's UndoManager
|
||||||
|
const updateButtonStates = () => {
|
||||||
setButtonsDisabled({
|
setButtonsDisabled({
|
||||||
undo: state.pastStates.length === 0,
|
undo: !collaborationManager.canUndo(),
|
||||||
redo: state.futureStates.length === 0,
|
redo: !collaborationManager.canRedo(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
updateButtonStates()
|
||||||
|
|
||||||
|
// Listen for undo/redo state changes
|
||||||
|
const unsubscribe = collaborationManager.onUndoRedoStateChange((state) => {
|
||||||
|
setButtonsDisabled({
|
||||||
|
undo: !state.canUndo,
|
||||||
|
redo: !state.canRedo,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => unsubscribe()
|
return () => unsubscribe()
|
||||||
}, [store])
|
}, [])
|
||||||
|
|
||||||
const { nodesReadOnly } = useNodesReadOnly()
|
const { nodesReadOnly } = useNodesReadOnly()
|
||||||
|
|
||||||
@ -36,9 +48,9 @@ const UndoRedo: FC<UndoRedoProps> = ({ handleUndo, handleRedo }) => {
|
|||||||
<div
|
<div
|
||||||
data-tooltip-id='workflow.undo'
|
data-tooltip-id='workflow.undo'
|
||||||
className={
|
className={
|
||||||
classNames('flex items-center px-1.5 w-8 h-8 rounded-md system-sm-medium text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary cursor-pointer select-none',
|
classNames('system-sm-medium flex h-8 w-8 cursor-pointer select-none items-center rounded-md px-1.5 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||||
(nodesReadOnly || buttonsDisabled.undo)
|
(nodesReadOnly || buttonsDisabled.undo)
|
||||||
&& 'hover:bg-transparent text-text-disabled hover:text-text-disabled cursor-not-allowed')}
|
&& 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled')}
|
||||||
onClick={() => !nodesReadOnly && !buttonsDisabled.undo && handleUndo()}
|
onClick={() => !nodesReadOnly && !buttonsDisabled.undo && handleUndo()}
|
||||||
>
|
>
|
||||||
<RiArrowGoBackLine className='h-4 w-4' />
|
<RiArrowGoBackLine className='h-4 w-4' />
|
||||||
@ -48,9 +60,9 @@ const UndoRedo: FC<UndoRedoProps> = ({ handleUndo, handleRedo }) => {
|
|||||||
<div
|
<div
|
||||||
data-tooltip-id='workflow.redo'
|
data-tooltip-id='workflow.redo'
|
||||||
className={
|
className={
|
||||||
classNames('flex items-center px-1.5 w-8 h-8 rounded-md system-sm-medium text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary cursor-pointer select-none',
|
classNames('system-sm-medium flex h-8 w-8 cursor-pointer select-none items-center rounded-md px-1.5 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||||
(nodesReadOnly || buttonsDisabled.redo)
|
(nodesReadOnly || buttonsDisabled.redo)
|
||||||
&& 'hover:bg-transparent text-text-disabled hover:text-text-disabled cursor-not-allowed',
|
&& 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled',
|
||||||
)}
|
)}
|
||||||
onClick={() => !nodesReadOnly && !buttonsDisabled.redo && handleRedo()}
|
onClick={() => !nodesReadOnly && !buttonsDisabled.redo && handleRedo()}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -50,7 +50,7 @@ import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
|
|||||||
import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
|
import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
|
||||||
import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
|
import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
|
||||||
import { useNodeLoopInteractions } from '../nodes/loop/use-interactions'
|
import { useNodeLoopInteractions } from '../nodes/loop/use-interactions'
|
||||||
import { useWorkflowHistoryStore } from '../workflow-history-store'
|
import { collaborationManager } from '../collaboration/core/collaboration-manager'
|
||||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||||
import { useHelpline } from './use-helpline'
|
import { useHelpline } from './use-helpline'
|
||||||
import {
|
import {
|
||||||
@ -67,7 +67,6 @@ export const useNodesInteractions = () => {
|
|||||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const reactflow = useReactFlow()
|
const reactflow = useReactFlow()
|
||||||
const { store: workflowHistoryStore } = useWorkflowHistoryStore()
|
|
||||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||||
const {
|
const {
|
||||||
checkNestedParallelLimit,
|
checkNestedParallelLimit,
|
||||||
@ -86,7 +85,7 @@ export const useNodesInteractions = () => {
|
|||||||
} = useNodeLoopInteractions()
|
} = useNodeLoopInteractions()
|
||||||
const dragNodeStartPosition = useRef({ x: 0, y: 0 } as { x: number; y: number })
|
const dragNodeStartPosition = useRef({ x: 0, y: 0 } as { x: number; y: number })
|
||||||
|
|
||||||
const { saveStateToHistory, undo, redo } = useWorkflowHistory()
|
const { saveStateToHistory } = useWorkflowHistory()
|
||||||
|
|
||||||
const handleNodeDragStart = useCallback<NodeDragHandler>((_, node) => {
|
const handleNodeDragStart = useCallback<NodeDragHandler>((_, node) => {
|
||||||
workflowStore.setState({ nodeAnimation: false })
|
workflowStore.setState({ nodeAnimation: false })
|
||||||
@ -1427,31 +1426,35 @@ export const useNodesInteractions = () => {
|
|||||||
if (getNodesReadOnly() || getWorkflowReadOnly())
|
if (getNodesReadOnly() || getWorkflowReadOnly())
|
||||||
return
|
return
|
||||||
|
|
||||||
const { setNodes, setEdges } = collaborativeWorkflow.getState()
|
// Use collaborative undo from Loro
|
||||||
undo()
|
const undoResult = collaborationManager.undo()
|
||||||
|
|
||||||
const { edges, nodes } = workflowHistoryStore.getState()
|
if (undoResult) {
|
||||||
if (edges.length === 0 && nodes.length === 0)
|
// The undo operation will automatically trigger subscriptions
|
||||||
return
|
// which will update the nodes and edges through setupSubscriptions
|
||||||
|
console.log('Collaborative undo performed')
|
||||||
setEdges(edges)
|
}
|
||||||
setNodes(nodes)
|
else {
|
||||||
}, [collaborativeWorkflow, undo, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly])
|
console.log('Nothing to undo')
|
||||||
|
}
|
||||||
|
}, [getNodesReadOnly, getWorkflowReadOnly])
|
||||||
|
|
||||||
const handleHistoryForward = useCallback(() => {
|
const handleHistoryForward = useCallback(() => {
|
||||||
if (getNodesReadOnly() || getWorkflowReadOnly())
|
if (getNodesReadOnly() || getWorkflowReadOnly())
|
||||||
return
|
return
|
||||||
|
|
||||||
const { setNodes, setEdges } = collaborativeWorkflow.getState()
|
// Use collaborative redo from Loro
|
||||||
redo()
|
const redoResult = collaborationManager.redo()
|
||||||
|
|
||||||
const { edges, nodes } = workflowHistoryStore.getState()
|
if (redoResult) {
|
||||||
if (edges.length === 0 && nodes.length === 0)
|
// The redo operation will automatically trigger subscriptions
|
||||||
return
|
// which will update the nodes and edges through setupSubscriptions
|
||||||
|
console.log('Collaborative redo performed')
|
||||||
setEdges(edges)
|
}
|
||||||
setNodes(nodes)
|
else {
|
||||||
}, [redo, collaborativeWorkflow, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly])
|
console.log('Nothing to redo')
|
||||||
|
}
|
||||||
|
}, [getNodesReadOnly, getWorkflowReadOnly])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleNodeDragStart,
|
handleNodeDragStart,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user