fix undo/redo

This commit is contained in:
hjlarry
2026-04-10 10:25:57 +08:00
parent 53fe1c9039
commit 6ab114ce54
28 changed files with 94 additions and 30 deletions

View File

@ -658,10 +658,37 @@ export class CollaborationManager {
})
}
emitGraphViewActive(isActive: boolean): void {
this.graphViewActive = isActive
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId))
return
this.sendCollaborationEvent({
type: 'graph_view_active',
data: { active: isActive },
timestamp: Date.now(),
})
}
emitHistoryAction(action: 'undo' | 'redo' | 'jump'): void {
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId))
return
this.sendCollaborationEvent({
type: 'workflow_history_action',
data: { action },
timestamp: Date.now(),
})
}
onUndoRedoStateChange(callback: (state: { canUndo: boolean, canRedo: boolean }) => void): () => void {
return this.eventEmitter.on('undoRedoStateChange', callback)
}
onHistoryAction(callback: (payload: { action: 'undo' | 'redo' | 'jump', userId?: string }) => void): () => void {
return this.eventEmitter.on('historyAction', callback)
}
emitRestoreRequest(data: RestoreRequestData): void {
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId))
return
@ -1055,6 +1082,11 @@ export class CollaborationManager {
else if (update.type === 'workflow_restore_complete') {
this.eventEmitter.emit('restoreComplete', update.data as RestoreCompleteData)
}
else if (update.type === 'workflow_history_action') {
const data = update.data as { action?: 'undo' | 'redo' | 'jump' } | undefined
if (data?.action)
this.eventEmitter.emit('historyAction', { action: data.action, userId: update.userId })
}
})
socket.on('online_users', (data: { users: OnlineUser[], leader?: string }) => {

View File

@ -66,6 +66,7 @@ export type CollaborationEventType
| 'workflow_restore_request'
| 'workflow_restore_intent'
| 'workflow_restore_complete'
| 'workflow_history_action'
export type CollaborationUpdate = {
type: CollaborationEventType

View File

@ -1,9 +1,9 @@
import type { FC } from 'react'
import { memo, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
import ViewWorkflowHistory from '@/app/components/workflow/header/view-workflow-history'
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
import { useWorkflowHistoryStore } from '@/app/components/workflow/workflow-history-store'
import { cn } from '@/utils/classnames'
import Divider from '../../base/divider'
import TipPopup from '../operator/tip-popup'
@ -11,32 +11,18 @@ import TipPopup from '../operator/tip-popup'
type UndoRedoProps = { handleUndo: () => void, handleRedo: () => void }
const UndoRedo: FC<UndoRedoProps> = ({ handleUndo, handleRedo }) => {
const { t } = useTranslation()
const { store } = useWorkflowHistoryStore()
const [buttonsDisabled, setButtonsDisabled] = useState({ undo: true, redo: true })
useEffect(() => {
// Update button states based on Loro's UndoManager
const updateButtonStates = () => {
const unsubscribe = store.temporal.subscribe((state) => {
setButtonsDisabled({
undo: !collaborationManager.canUndo(),
redo: !collaborationManager.canRedo(),
})
}
// Initial state
Promise.resolve().then(() => {
updateButtonStates()
})
// Listen for undo/redo state changes
const unsubscribe = collaborationManager.onUndoRedoStateChange((state) => {
setButtonsDisabled({
undo: !state.canUndo,
redo: !state.canRedo,
undo: state.pastStates.length === 0,
redo: state.futureStates.length === 0,
})
})
return () => unsubscribe()
}, [])
}, [store])
const { nodesReadOnly } = useNodesReadOnly()

View File

@ -20,6 +20,7 @@ import {
} from '@/app/components/base/portal-to-follow-elem'
import { cn } from '@/utils/classnames'
import Divider from '../../base/divider'
import { collaborationManager } from '../collaboration'
import {
useNodesReadOnly,
useWorkflowHistory,
@ -76,6 +77,8 @@ const ViewWorkflowHistory = () => {
setEdges(edges)
setNodes(nodes)
if (collaborationManager.isConnected())
collaborationManager.emitHistoryAction('jump')
}, [currentHistoryStateIndex, reactFlowStore, redo, store, undo])
const calculateStepLabel = useCallback((index: number) => {

View File

@ -96,7 +96,11 @@ export const useNodesInteractions = () => {
})
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
const { saveStateToHistory, undo } = useWorkflowHistory()
const {
saveStateToHistory,
undo,
redo,
} = useWorkflowHistory()
const autoGenerateWebhookUrl = useAutoGenerateWebhookUrl()
const handleNodeDragStart = useCallback<NodeDragHandler>(
@ -2104,15 +2108,17 @@ export const useNodesInteractions = () => {
if (getNodesReadOnly() || getWorkflowReadOnly())
return
// Use collaborative undo from Loro
collaborationManager.undo()
undo()
const { edges, nodes } = workflowHistoryStore.getState()
if (edges.length === 0 && nodes.length === 0)
return
const { setNodes, setEdges } = collaborativeWorkflow.getState()
setEdges(edges)
setNodes(nodes)
const shouldBroadcast = collaborationManager.isConnected()
setEdges(edges, shouldBroadcast)
setNodes(nodes, shouldBroadcast)
if (shouldBroadcast)
collaborationManager.emitHistoryAction('undo')
workflowStore.setState({ edgeMenu: undefined })
}, [
collaborativeWorkflow,
@ -2127,17 +2133,21 @@ export const useNodesInteractions = () => {
if (getNodesReadOnly() || getWorkflowReadOnly())
return
// Use collaborative redo from Loro
collaborationManager.redo()
redo()
const { edges, nodes } = workflowHistoryStore.getState()
if (edges.length === 0 && nodes.length === 0)
return
const { setNodes, setEdges } = collaborativeWorkflow.getState()
setEdges(edges)
setNodes(nodes)
const shouldBroadcast = collaborationManager.isConnected()
setEdges(edges, shouldBroadcast)
setNodes(nodes, shouldBroadcast)
if (shouldBroadcast)
collaborationManager.emitHistoryAction('redo')
workflowStore.setState({ edgeMenu: undefined })
}, [
collaborativeWorkflow,
redo,
workflowStore,
workflowHistoryStore,
getNodesReadOnly,

View File

@ -40,6 +40,7 @@ import ReactFlow, {
useReactFlow,
useStoreApi,
} from 'reactflow'
import Toast from '@/app/components/base/toast'
import { IS_DEV } from '@/config'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import dynamic from '@/next/dynamic'
@ -196,6 +197,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
onlineUsers,
}) => {
const workflowContainerRef = useRef<HTMLDivElement>(null)
const { t } = useTranslation()
const workflowStore = useWorkflowStore()
const reactflow = useReactFlow()
const store = useStoreApi()
@ -266,6 +268,15 @@ export const Workflow: FC<WorkflowProps> = memo(({
}
})
}, [edges, nodes, setEdges, setNodes, store])
useEffect(() => {
return collaborationManager.onHistoryAction((_) => {
Toast.notify({
type: 'info',
message: t('collaboration.historyAction.generic', { ns: 'workflow' }),
})
})
}, [t])
const {
handleSyncWorkflowDraft,
syncWorkflowDraftWhenPageClose,
@ -300,7 +311,6 @@ export const Workflow: FC<WorkflowProps> = memo(({
const setCommentQuickAdd = useStore(s => s.setCommentQuickAdd)
const setPendingCommentState = useStore(s => s.setPendingComment)
const isCommentInputActive = Boolean(pendingComment) || isCommentPlacing
const { t } = useTranslation()
const visibleComments = useMemo(() => {
if (showResolvedComments)
return comments

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "متغيرات المحادثة",
"chatVariable.storedContent": "المحتوى المخزن",
"chatVariable.updatedAt": "تم التحديث في ",
"collaboration.historyAction.generic": "قام متعاون بعملية تراجع/إعادة",
"comments.actions.deleteReply": "حذف الرد",
"comments.actions.editReply": "تعديل الرد",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "Gesprächsvariablen",
"chatVariable.storedContent": "Gespeicherter Inhalt",
"chatVariable.updatedAt": "Aktualisiert am ",
"collaboration.historyAction.generic": "Ein Mitbearbeiter hat Rückgängig/Wiederholen ausgeführt",
"comments.actions.deleteReply": "Antwort löschen",
"comments.actions.editReply": "Antwort bearbeiten",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "Conversation Variables",
"chatVariable.storedContent": "Stored content",
"chatVariable.updatedAt": "Updated at ",
"collaboration.historyAction.generic": "A collaborator performed an undo/redo action",
"comments.actions.deleteReply": "Delete reply",
"comments.actions.editReply": "Edit reply",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "Variables de Conversación",
"chatVariable.storedContent": "Contenido almacenado",
"chatVariable.updatedAt": "Actualizado el ",
"collaboration.historyAction.generic": "Un colaborador realizó deshacer/rehacer",
"comments.actions.deleteReply": "Eliminar respuesta",
"comments.actions.editReply": "Editar respuesta",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "متغیرهای مکالمه",
"chatVariable.storedContent": "محتوای ذخیره‌شده",
"chatVariable.updatedAt": "به‌روزرسانی شده در ",
"collaboration.historyAction.generic": "یک همکار عملیات برگرداندن/انجام مجدد را انجام داد",
"comments.actions.deleteReply": "حذف پاسخ",
"comments.actions.editReply": "ویرایش پاسخ",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "Variables de Conversation",
"chatVariable.storedContent": "Contenu stocké",
"chatVariable.updatedAt": "Mis à jour le ",
"collaboration.historyAction.generic": "Un collaborateur a effectué une annulation/une réexécution",
"comments.actions.deleteReply": "Supprimer la réponse",
"comments.actions.editReply": "Modifier la réponse",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "वार्तालाप चर",
"chatVariable.storedContent": "संग्रहीत सामग्री",
"chatVariable.updatedAt": "अपडेट किया गया ",
"collaboration.historyAction.generic": "एक सहयोगी ने Undo/Redo किया",
"comments.actions.deleteReply": "जवाब हटाएं",
"comments.actions.editReply": "जवाब संपादित करें",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "Variabel Percakapan",
"chatVariable.storedContent": "Konten yang disimpan",
"chatVariable.updatedAt": "Diperbarui pada",
"collaboration.historyAction.generic": "Seorang kolaborator melakukan urungkan/ulang",
"comments.actions.deleteReply": "Hapus balasan",
"comments.actions.editReply": "Edit balasan",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "Variabili di Conversazione",
"chatVariable.storedContent": "Contenuto memorizzato",
"chatVariable.updatedAt": "Aggiornato il ",
"collaboration.historyAction.generic": "Un collaboratore ha eseguito annulla/ripristina",
"comments.actions.deleteReply": "Elimina risposta",
"comments.actions.editReply": "Modifica risposta",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "会話変数",
"chatVariable.storedContent": "保存内容",
"chatVariable.updatedAt": "最終更新:",
"collaboration.historyAction.generic": "共同編集者が元に戻す/やり直しを実行しました",
"comments.actions.deleteReply": "返信を削除",
"comments.actions.editReply": "返信を編集",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "대화 변수",
"chatVariable.storedContent": "저장된 내용",
"chatVariable.updatedAt": "업데이트 시간: ",
"collaboration.historyAction.generic": "공동 작업자가 실행 취소/다시 실행을 수행했습니다",
"comments.actions.deleteReply": "답글 삭제",
"comments.actions.editReply": "답글 편집",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "Zmienne Konwersacji",
"chatVariable.storedContent": "Przechowywana zawartość",
"chatVariable.updatedAt": "Zaktualizowano ",
"collaboration.historyAction.generic": "Współpracownik wykonał cofnięcie/ponowienie",
"comments.actions.deleteReply": "Delete reply",
"comments.actions.editReply": "Edit reply",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "Variáveis de Conversação",
"chatVariable.storedContent": "Conteúdo armazenado",
"chatVariable.updatedAt": "Atualizado em ",
"collaboration.historyAction.generic": "Um colaborador realizou desfazer/refazer",
"comments.actions.deleteReply": "Delete reply",
"comments.actions.editReply": "Edit reply",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "Variabile de Conversație",
"chatVariable.storedContent": "Conținut stocat",
"chatVariable.updatedAt": "Actualizat la ",
"collaboration.historyAction.generic": "Un colaborator a efectuat anulare/refacere",
"comments.actions.deleteReply": "Delete reply",
"comments.actions.editReply": "Edit reply",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "Переменные разговора",
"chatVariable.storedContent": "Сохраненный контент",
"chatVariable.updatedAt": "Обновлено в ",
"collaboration.historyAction.generic": "Участник выполнил отмену/повтор",
"comments.actions.deleteReply": "Delete reply",
"comments.actions.editReply": "Edit reply",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "Pogovor Spremenljivke",
"chatVariable.storedContent": "Shranjena vsebina",
"chatVariable.updatedAt": "Posodobljeno ob",
"collaboration.historyAction.generic": "Sodelavec je izvedel razveljavitev/ponovitev",
"comments.actions.deleteReply": "Delete reply",
"comments.actions.editReply": "Edit reply",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "ตัวแปรการสนทนา",
"chatVariable.storedContent": "เนื้อหาที่เก็บไว้",
"chatVariable.updatedAt": "อัพเดทเมื่อ",
"collaboration.historyAction.generic": "ผู้ร่วมงานได้ทำการยกเลิก/ทำซ้ำ",
"comments.actions.deleteReply": "Delete reply",
"comments.actions.editReply": "Edit reply",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "Konuşma Değişkenleri",
"chatVariable.storedContent": "Depolanan içerik",
"chatVariable.updatedAt": "Güncellenme zamanı: ",
"collaboration.historyAction.generic": "Bir işbirlikçi geri alma/yeniden yapma gerçekleştirdi",
"comments.actions.deleteReply": "Delete reply",
"comments.actions.editReply": "Edit reply",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "Змінні розмови",
"chatVariable.storedContent": "Збережений вміст",
"chatVariable.updatedAt": "Оновлено ",
"collaboration.historyAction.generic": "Співавтор виконав скасування/повторення",
"comments.actions.deleteReply": "Delete reply",
"comments.actions.editReply": "Edit reply",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "Biến Hội Thoại",
"chatVariable.storedContent": "Nội dung đã lưu",
"chatVariable.updatedAt": "Cập nhật lúc ",
"collaboration.historyAction.generic": "Một cộng tác viên đã thực hiện hoàn tác/làm lại",
"comments.actions.deleteReply": "Delete reply",
"comments.actions.editReply": "Edit reply",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "会话变量",
"chatVariable.storedContent": "存储内容",
"chatVariable.updatedAt": "更新时间 ",
"collaboration.historyAction.generic": "协作者执行了撤销/重做操作",
"comments.actions.deleteReply": "删除回复",
"comments.actions.editReply": "编辑回复",
"comments.actions.addComment": "Add Comment",

View File

@ -109,6 +109,7 @@
"chatVariable.panelTitle": "對話變數",
"chatVariable.storedContent": "已儲存內容",
"chatVariable.updatedAt": "更新於 ",
"collaboration.historyAction.generic": "協作者執行了復原/重做操作",
"comments.actions.deleteReply": "刪除回覆",
"comments.actions.editReply": "編輯回覆",
"comments.actions.addComment": "Add Comment",