Co-authored-by: alleschen <alleschen@tencent.com> Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>tags/1.9.0
| }) | }) | ||||
| setNodes(newNodes) | setNodes(newNodes) | ||||
| if (candidateNode.type === CUSTOM_NOTE_NODE) | if (candidateNode.type === CUSTOM_NOTE_NODE) | ||||
| saveStateToHistory(WorkflowHistoryEvent.NoteAdd) | |||||
| saveStateToHistory(WorkflowHistoryEvent.NoteAdd, { nodeId: candidateNode.id }) | |||||
| else | else | ||||
| saveStateToHistory(WorkflowHistoryEvent.NodeAdd) | |||||
| saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: candidateNode.id }) | |||||
| workflowStore.setState({ candidateNode: undefined }) | workflowStore.setState({ candidateNode: undefined }) | ||||
| const calculateChangeList: ChangeHistoryList = useMemo(() => { | const calculateChangeList: ChangeHistoryList = useMemo(() => { | ||||
| const filterList = (list: any, startIndex = 0, reverse = false) => list.map((state: Partial<WorkflowHistoryState>, index: number) => { | const filterList = (list: any, startIndex = 0, reverse = false) => list.map((state: Partial<WorkflowHistoryState>, index: number) => { | ||||
| const nodes = (state.nodes || store.getState().nodes) || [] | |||||
| const nodeId = state?.workflowHistoryEventMeta?.nodeId | |||||
| const targetTitle = nodes.find(n => n.id === nodeId)?.data?.title ?? '' | |||||
| return { | return { | ||||
| label: state.workflowHistoryEvent && getHistoryLabel(state.workflowHistoryEvent), | label: state.workflowHistoryEvent && getHistoryLabel(state.workflowHistoryEvent), | ||||
| index: reverse ? list.length - 1 - index - startIndex : index - startIndex, | index: reverse ? list.length - 1 - index - startIndex : index - startIndex, | ||||
| state, | |||||
| state: { | |||||
| ...state, | |||||
| workflowHistoryEventMeta: state.workflowHistoryEventMeta ? { | |||||
| ...state.workflowHistoryEventMeta, | |||||
| nodeTitle: state.workflowHistoryEventMeta.nodeTitle || targetTitle, | |||||
| } : undefined, | |||||
| }, | |||||
| } | } | ||||
| }).filter(Boolean) | }).filter(Boolean) | ||||
| } | } | ||||
| }, [futureStates, getHistoryLabel, pastStates, store]) | }, [futureStates, getHistoryLabel, pastStates, store]) | ||||
| const composeHistoryItemLabel = useCallback((nodeTitle: string | undefined, baseLabel: string) => { | |||||
| if (!nodeTitle) | |||||
| return baseLabel | |||||
| return `${nodeTitle} ${baseLabel}` | |||||
| }, []) | |||||
| return ( | return ( | ||||
| ( | ( | ||||
| <PortalToFollowElem | <PortalToFollowElem | ||||
| 'flex items-center text-[13px] font-medium leading-[18px] text-text-secondary', | 'flex items-center text-[13px] font-medium leading-[18px] text-text-secondary', | ||||
| )} | )} | ||||
| > | > | ||||
| {item?.label || t('workflow.changeHistory.sessionStart')} ({calculateStepLabel(item?.index)}{item?.index === currentHistoryStateIndex && t('workflow.changeHistory.currentState')}) | |||||
| {composeHistoryItemLabel( | |||||
| item?.state?.workflowHistoryEventMeta?.nodeTitle, | |||||
| item?.label || t('workflow.changeHistory.sessionStart'), | |||||
| )} ({calculateStepLabel(item?.index)}{item?.index === currentHistoryStateIndex && t('workflow.changeHistory.currentState')}) | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| 'flex items-center text-[13px] font-medium leading-[18px] text-text-secondary', | 'flex items-center text-[13px] font-medium leading-[18px] text-text-secondary', | ||||
| )} | )} | ||||
| > | > | ||||
| {item?.label || t('workflow.changeHistory.sessionStart')} ({calculateStepLabel(item?.index)}) | |||||
| {composeHistoryItemLabel( | |||||
| item?.state?.workflowHistoryEventMeta?.nodeTitle, | |||||
| item?.label || t('workflow.changeHistory.sessionStart'), | |||||
| )} ({calculateStepLabel(item?.index)}) | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> |
| if (x !== 0 && y !== 0) { | if (x !== 0 && y !== 0) { | ||||
| // selecting a note will trigger a drag stop event with x and y as 0 | // selecting a note will trigger a drag stop event with x and y as 0 | ||||
| saveStateToHistory(WorkflowHistoryEvent.NodeDragStop) | |||||
| saveStateToHistory(WorkflowHistoryEvent.NodeDragStop, { nodeId: node.id }) | |||||
| } | } | ||||
| } | } | ||||
| }, [workflowStore, getNodesReadOnly, saveStateToHistory, handleSyncWorkflowDraft]) | }, [workflowStore, getNodesReadOnly, saveStateToHistory, handleSyncWorkflowDraft]) | ||||
| setEdges(newEdges) | setEdges(newEdges) | ||||
| handleSyncWorkflowDraft() | handleSyncWorkflowDraft() | ||||
| saveStateToHistory(WorkflowHistoryEvent.NodeConnect) | |||||
| saveStateToHistory(WorkflowHistoryEvent.NodeConnect, { nodeId: targetNode?.id }) | |||||
| } | } | ||||
| else { | else { | ||||
| const { | const { | ||||
| handleSyncWorkflowDraft() | handleSyncWorkflowDraft() | ||||
| if (currentNode.type === CUSTOM_NOTE_NODE) | if (currentNode.type === CUSTOM_NOTE_NODE) | ||||
| saveStateToHistory(WorkflowHistoryEvent.NoteDelete) | |||||
| saveStateToHistory(WorkflowHistoryEvent.NoteDelete, { nodeId: currentNode.id }) | |||||
| else | else | ||||
| saveStateToHistory(WorkflowHistoryEvent.NodeDelete) | |||||
| saveStateToHistory(WorkflowHistoryEvent.NodeDelete, { nodeId: currentNode.id }) | |||||
| }, [getNodesReadOnly, store, deleteNodeInspectorVars, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, t]) | }, [getNodesReadOnly, store, deleteNodeInspectorVars, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, t]) | ||||
| const handleNodeAdd = useCallback<OnNodeAdd>(( | const handleNodeAdd = useCallback<OnNodeAdd>(( | ||||
| setEdges(newEdges) | setEdges(newEdges) | ||||
| } | } | ||||
| handleSyncWorkflowDraft() | handleSyncWorkflowDraft() | ||||
| saveStateToHistory(WorkflowHistoryEvent.NodeAdd) | |||||
| saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNode.id }) | |||||
| }, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, getAfterNodesInSameBranch, checkNestedParallelLimit]) | }, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, getAfterNodesInSameBranch, checkNestedParallelLimit]) | ||||
| const handleNodeChange = useCallback(( | const handleNodeChange = useCallback(( | ||||
| setEdges(newEdges) | setEdges(newEdges) | ||||
| handleSyncWorkflowDraft() | handleSyncWorkflowDraft() | ||||
| saveStateToHistory(WorkflowHistoryEvent.NodeChange) | |||||
| saveStateToHistory(WorkflowHistoryEvent.NodeChange, { nodeId: currentNodeId }) | |||||
| }, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory]) | }, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory]) | ||||
| const handleNodesCancelSelected = useCallback(() => { | const handleNodesCancelSelected = useCallback(() => { | ||||
| setNodes([...nodes, ...nodesToPaste]) | setNodes([...nodes, ...nodesToPaste]) | ||||
| setEdges([...edges, ...edgesToPaste]) | setEdges([...edges, ...edgesToPaste]) | ||||
| saveStateToHistory(WorkflowHistoryEvent.NodePaste) | |||||
| saveStateToHistory(WorkflowHistoryEvent.NodePaste, { nodeId: nodesToPaste?.[0]?.id }) | |||||
| handleSyncWorkflowDraft() | handleSyncWorkflowDraft() | ||||
| } | } | ||||
| }, [getNodesReadOnly, workflowStore, store, reactflow, saveStateToHistory, handleSyncWorkflowDraft, handleNodeIterationChildrenCopy, handleNodeLoopChildrenCopy]) | }, [getNodesReadOnly, workflowStore, store, reactflow, saveStateToHistory, handleSyncWorkflowDraft, handleNodeIterationChildrenCopy, handleNodeLoopChildrenCopy]) | ||||
| }) | }) | ||||
| setNodes(newNodes) | setNodes(newNodes) | ||||
| handleSyncWorkflowDraft() | handleSyncWorkflowDraft() | ||||
| saveStateToHistory(WorkflowHistoryEvent.NodeResize) | |||||
| saveStateToHistory(WorkflowHistoryEvent.NodeResize, { nodeId }) | |||||
| }, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory]) | }, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory]) | ||||
| const handleNodeDisconnect = useCallback((nodeId: string) => { | const handleNodeDisconnect = useCallback((nodeId: string) => { |
| } from 'reactflow' | } from 'reactflow' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { useWorkflowHistoryStore } from '../workflow-history-store' | import { useWorkflowHistoryStore } from '../workflow-history-store' | ||||
| import type { WorkflowHistoryEventMeta } from '../workflow-history-store' | |||||
| /** | /** | ||||
| * All supported Events that create a new history state. | * All supported Events that create a new history state. | ||||
| // Some events may be triggered multiple times in a short period of time. | // 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 | // We debounce the history state update to avoid creating multiple history states | ||||
| // with minimal changes. | // with minimal changes. | ||||
| const saveStateToHistoryRef = useRef(debounce((event: WorkflowHistoryEvent) => { | |||||
| const saveStateToHistoryRef = useRef(debounce((event: WorkflowHistoryEvent, meta?: WorkflowHistoryEventMeta) => { | |||||
| workflowHistoryStore.setState({ | workflowHistoryStore.setState({ | ||||
| workflowHistoryEvent: event, | workflowHistoryEvent: event, | ||||
| workflowHistoryEventMeta: meta, | |||||
| nodes: store.getState().getNodes(), | nodes: store.getState().getNodes(), | ||||
| edges: store.getState().edges, | edges: store.getState().edges, | ||||
| }) | }) | ||||
| }, 500)) | }, 500)) | ||||
| const saveStateToHistory = useCallback((event: WorkflowHistoryEvent) => { | |||||
| const saveStateToHistory = useCallback((event: WorkflowHistoryEvent, meta?: WorkflowHistoryEventMeta) => { | |||||
| switch (event) { | switch (event) { | ||||
| case WorkflowHistoryEvent.NoteChange: | case WorkflowHistoryEvent.NoteChange: | ||||
| // Hint: Note change does not trigger when note text changes, | // Hint: Note change does not trigger when note text changes, | ||||
| // because the note editors have their own history states. | // because the note editors have their own history states. | ||||
| saveStateToHistoryRef.current(event) | |||||
| saveStateToHistoryRef.current(event, meta) | |||||
| break | break | ||||
| case WorkflowHistoryEvent.NodeTitleChange: | case WorkflowHistoryEvent.NodeTitleChange: | ||||
| case WorkflowHistoryEvent.NodeDescriptionChange: | case WorkflowHistoryEvent.NodeDescriptionChange: | ||||
| case WorkflowHistoryEvent.NoteAdd: | case WorkflowHistoryEvent.NoteAdd: | ||||
| case WorkflowHistoryEvent.LayoutOrganize: | case WorkflowHistoryEvent.LayoutOrganize: | ||||
| case WorkflowHistoryEvent.NoteDelete: | case WorkflowHistoryEvent.NoteDelete: | ||||
| saveStateToHistoryRef.current(event) | |||||
| saveStateToHistoryRef.current(event, meta) | |||||
| break | break | ||||
| default: | default: | ||||
| // We do not create a history state for every event. | // We do not create a history state for every event. |
| const handleTitleBlur = useCallback((title: string) => { | const handleTitleBlur = useCallback((title: string) => { | ||||
| handleNodeDataUpdateWithSyncDraft({ id, data: { title } }) | handleNodeDataUpdateWithSyncDraft({ id, data: { title } }) | ||||
| saveStateToHistory(WorkflowHistoryEvent.NodeTitleChange) | |||||
| saveStateToHistory(WorkflowHistoryEvent.NodeTitleChange, { nodeId: id }) | |||||
| }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) | }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) | ||||
| const handleDescriptionChange = useCallback((desc: string) => { | const handleDescriptionChange = useCallback((desc: string) => { | ||||
| handleNodeDataUpdateWithSyncDraft({ id, data: { desc } }) | handleNodeDataUpdateWithSyncDraft({ id, data: { desc } }) | ||||
| saveStateToHistory(WorkflowHistoryEvent.NodeDescriptionChange) | |||||
| saveStateToHistory(WorkflowHistoryEvent.NodeDescriptionChange, { nodeId: id }) | |||||
| }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) | }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) | ||||
| const isChildNode = !!(data.isInIteration || data.isInLoop) | const isChildNode = !!(data.isInIteration || data.isInLoop) |
| const handleThemeChange = useCallback((theme: NoteTheme) => { | const handleThemeChange = useCallback((theme: NoteTheme) => { | ||||
| handleNodeDataUpdateWithSyncDraft({ id, data: { theme } }) | handleNodeDataUpdateWithSyncDraft({ id, data: { theme } }) | ||||
| saveStateToHistory(WorkflowHistoryEvent.NoteChange) | |||||
| saveStateToHistory(WorkflowHistoryEvent.NoteChange, { nodeId: id }) | |||||
| }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) | }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) | ||||
| const handleEditorChange = useCallback((editorState: EditorState) => { | const handleEditorChange = useCallback((editorState: EditorState) => { | ||||
| const handleShowAuthorChange = useCallback((showAuthor: boolean) => { | const handleShowAuthorChange = useCallback((showAuthor: boolean) => { | ||||
| handleNodeDataUpdateWithSyncDraft({ id, data: { showAuthor } }) | handleNodeDataUpdateWithSyncDraft({ id, data: { showAuthor } }) | ||||
| saveStateToHistory(WorkflowHistoryEvent.NoteChange) | |||||
| saveStateToHistory(WorkflowHistoryEvent.NoteChange, { nodeId: id }) | |||||
| }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) | }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) | ||||
| return { | return { |
| setState: (state: WorkflowHistoryState) => { | setState: (state: WorkflowHistoryState) => { | ||||
| store.setState({ | store.setState({ | ||||
| workflowHistoryEvent: state.workflowHistoryEvent, | workflowHistoryEvent: state.workflowHistoryEvent, | ||||
| workflowHistoryEventMeta: state.workflowHistoryEventMeta, | |||||
| nodes: state.nodes.map((node: Node) => ({ ...node, data: { ...node.data, selected: false } })), | nodes: state.nodes.map((node: Node) => ({ ...node, data: { ...node.data, selected: false } })), | ||||
| edges: state.edges.map((edge: Edge) => ({ ...edge, selected: false }) as Edge), | edges: state.edges.map((edge: Edge) => ({ ...edge, selected: false }) as Edge), | ||||
| }) | }) | ||||
| (set, get) => { | (set, get) => { | ||||
| return { | return { | ||||
| workflowHistoryEvent: undefined, | workflowHistoryEvent: undefined, | ||||
| workflowHistoryEventMeta: undefined, | |||||
| nodes: storeNodes, | nodes: storeNodes, | ||||
| edges: storeEdges, | edges: storeEdges, | ||||
| getNodes: () => get().nodes, | getNodes: () => get().nodes, | ||||
| nodes: Node[] | nodes: Node[] | ||||
| edges: Edge[] | edges: Edge[] | ||||
| workflowHistoryEvent: WorkflowHistoryEvent | undefined | workflowHistoryEvent: WorkflowHistoryEvent | undefined | ||||
| workflowHistoryEventMeta?: WorkflowHistoryEventMeta | |||||
| } | } | ||||
| export type WorkflowHistoryActions = { | export type WorkflowHistoryActions = { | ||||
| edges: Edge[] | edges: Edge[] | ||||
| children: ReactNode | children: ReactNode | ||||
| } | } | ||||
| export type WorkflowHistoryEventMeta = { | |||||
| nodeId?: string | |||||
| nodeTitle?: string | |||||
| } |