ソースを参照

Feat: shortcut hook (#7385)

Yi Xiao 8 ヶ月 前
コミット
8b06105fa1

+ 3 - 1
web/app/components/workflow/header/run-and-history.tsx

@@ -35,7 +35,9 @@ const RunMode = memo(() => {
           'hover:bg-state-accent-hover cursor-pointer',
           isRunning && 'bg-state-accent-hover !cursor-not-allowed',
         )}
-        onClick={() => handleWorkflowStartRunInWorkflow()}
+        onClick={() => {
+          handleWorkflowStartRunInWorkflow()
+        }}
       >
         {
           isRunning

+ 3 - 1
web/app/components/workflow/header/view-history.tsx

@@ -17,7 +17,7 @@ import {
   useWorkflowInteractions,
   useWorkflowRun,
 } from '../hooks'
-import { WorkflowRunningStatus } from '../types'
+import { ControlMode, WorkflowRunningStatus } from '../types'
 import cn from '@/utils/classnames'
 import {
   PortalToFollowElem,
@@ -58,6 +58,7 @@ const ViewHistory = ({
     handleCancelDebugAndPreviewPanel,
   } = useWorkflowInteractions()
   const workflowStore = useWorkflowStore()
+  const setControlMode = useStore(s => s.setControlMode)
   const { appDetail, setCurrentLogItem, setShowMessageLogModal } = useAppStore(useShallow(state => ({
     appDetail: state.appDetail,
     setCurrentLogItem: state.setCurrentLogItem,
@@ -173,6 +174,7 @@ const ViewHistory = ({
                           setOpen(false)
                           handleNodesCancelSelected()
                           handleCancelDebugAndPreviewPanel()
+                          setControlMode(ControlMode.Hand)
                         }}
                       >
                         {

+ 3 - 2
web/app/components/workflow/hooks/index.ts

@@ -7,11 +7,12 @@ export * from './use-workflow'
 export * from './use-workflow-run'
 export * from './use-workflow-template'
 export * from './use-checklist'
-export * from './use-workflow-mode'
-export * from './use-workflow-interactions'
 export * from './use-selection-interactions'
 export * from './use-panel-interactions'
 export * from './use-workflow-start-run'
 export * from './use-nodes-layout'
 export * from './use-workflow-history'
 export * from './use-workflow-variables'
+export * from './use-shortcuts'
+export * from './use-workflow-interactions'
+export * from './use-workflow-mode'

+ 13 - 40
web/app/components/workflow/hooks/use-nodes-interactions.ts

@@ -48,6 +48,7 @@ import { useHelpline } from './use-helpline'
 import {
   useNodesReadOnly,
   useWorkflow,
+  useWorkflowReadOnly,
 } from './use-workflow'
 import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
 
@@ -62,6 +63,7 @@ export const useNodesInteractions = () => {
     getAfterNodesInSameBranch,
   } = useWorkflow()
   const { getNodesReadOnly } = useNodesReadOnly()
+  const { getWorkflowReadOnly } = useWorkflowReadOnly()
   const { handleSetHelpline } = useHelpline()
   const {
     handleNodeIterationChildDrag,
@@ -1029,14 +1031,7 @@ export const useNodesInteractions = () => {
     if (getNodesReadOnly())
       return
 
-    const {
-      setClipboardElements,
-      shortcutsDisabled,
-      showFeaturesPanel,
-    } = workflowStore.getState()
-
-    if (shortcutsDisabled || showFeaturesPanel)
-      return
+    const { setClipboardElements } = workflowStore.getState()
 
     const {
       getNodes,
@@ -1062,14 +1057,9 @@ export const useNodesInteractions = () => {
 
     const {
       clipboardElements,
-      shortcutsDisabled,
-      showFeaturesPanel,
       mousePosition,
     } = workflowStore.getState()
 
-    if (shortcutsDisabled || showFeaturesPanel)
-      return
-
     const {
       getNodes,
       setNodes,
@@ -1107,6 +1097,11 @@ export const useNodesInteractions = () => {
         })
         newNode.id = newNode.id + index
 
+        // If only the iteration start node is copied, remove the isIterationStart flag
+        // This new node is movable and can be placed anywhere
+        if (clipboardElements.length === 1 && newNode.data.isIterationStart)
+          newNode.data.isIterationStart = false
+
         let newChildren: Node[] = []
         if (nodeToPaste.data.type === BlockEnum.Iteration) {
           newNode.data._children = [];
@@ -1145,14 +1140,6 @@ export const useNodesInteractions = () => {
     if (getNodesReadOnly())
       return
 
-    const {
-      shortcutsDisabled,
-      showFeaturesPanel,
-    } = workflowStore.getState()
-
-    if (shortcutsDisabled || showFeaturesPanel)
-      return
-
     const {
       getNodes,
       edges,
@@ -1175,7 +1162,7 @@ export const useNodesInteractions = () => {
 
     if (selectedNode)
       handleNodeDelete(selectedNode.id)
-  }, [store, workflowStore, getNodesReadOnly, handleNodeDelete])
+  }, [store, getNodesReadOnly, handleNodeDelete])
 
   const handleNodeResize = useCallback((nodeId: string, params: ResizeParamsWithDirection) => {
     if (getNodesReadOnly())
@@ -1234,14 +1221,7 @@ export const useNodesInteractions = () => {
   }, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory])
 
   const handleHistoryBack = useCallback(() => {
-    if (getNodesReadOnly())
-      return
-
-    const {
-      shortcutsDisabled,
-    } = workflowStore.getState()
-
-    if (shortcutsDisabled)
+    if (getNodesReadOnly() || getWorkflowReadOnly())
       return
 
     const { setEdges, setNodes } = store.getState()
@@ -1253,17 +1233,10 @@ export const useNodesInteractions = () => {
 
     setEdges(edges)
     setNodes(nodes)
-  }, [store, undo, workflowHistoryStore, workflowStore, getNodesReadOnly])
+  }, [store, undo, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly])
 
   const handleHistoryForward = useCallback(() => {
-    if (getNodesReadOnly())
-      return
-
-    const {
-      shortcutsDisabled,
-    } = workflowStore.getState()
-
-    if (shortcutsDisabled)
+    if (getNodesReadOnly() || getWorkflowReadOnly())
       return
 
     const { setEdges, setNodes } = store.getState()
@@ -1275,7 +1248,7 @@ export const useNodesInteractions = () => {
 
     setEdges(edges)
     setNodes(nodes)
-  }, [redo, store, workflowHistoryStore, workflowStore, getNodesReadOnly])
+  }, [redo, store, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly])
 
   return {
     handleNodeDragStart,

+ 3 - 1
web/app/components/workflow/hooks/use-nodes-sync-draft.ts

@@ -8,7 +8,9 @@ import {
 } from '../store'
 import { BlockEnum } from '../types'
 import { useWorkflowUpdate } from '../hooks'
-import { useNodesReadOnly } from './use-workflow'
+import {
+  useNodesReadOnly,
+} from './use-workflow'
 import { syncWorkflowDraft } from '@/service/workflow'
 import { useFeaturesStore } from '@/app/components/base/features/hooks'
 import { API_PREFIX } from '@/config'

+ 186 - 0
web/app/components/workflow/hooks/use-shortcuts.ts

@@ -0,0 +1,186 @@
+import { useReactFlow } from 'reactflow'
+import { useKeyPress } from 'ahooks'
+import { useCallback } from 'react'
+import {
+  getKeyboardKeyCodeBySystem,
+  isEventTargetInputArea,
+} from '../utils'
+import { useWorkflowHistoryStore } from '../workflow-history-store'
+import { useWorkflowStore } from '../store'
+import {
+  useEdgesInteractions,
+  useNodesInteractions,
+  useNodesSyncDraft,
+  useWorkflowMoveMode,
+  useWorkflowOrganize,
+  useWorkflowStartRun,
+} from '.'
+
+export const useShortcuts = (): void => {
+  const {
+    handleNodesCopy,
+    handleNodesPaste,
+    handleNodesDuplicate,
+    handleNodesDelete,
+    handleHistoryBack,
+    handleHistoryForward,
+  } = useNodesInteractions()
+  const { handleStartWorkflowRun } = useWorkflowStartRun()
+  const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore()
+  const { handleSyncWorkflowDraft } = useNodesSyncDraft()
+  const { handleEdgeDelete } = useEdgesInteractions()
+  const workflowStore = useWorkflowStore()
+  const {
+    handleModeHand,
+    handleModePointer,
+  } = useWorkflowMoveMode()
+  const { handleLayout } = useWorkflowOrganize()
+
+  const {
+    zoomIn,
+    zoomOut,
+    zoomTo,
+    fitView,
+  } = useReactFlow()
+
+  const shouldHandleShortcut = useCallback((e: KeyboardEvent) => {
+    const { showFeaturesPanel } = workflowStore.getState()
+    return !showFeaturesPanel && !isEventTargetInputArea(e.target as HTMLElement)
+  }, [workflowStore])
+
+  useKeyPress(['delete', 'backspace'], (e) => {
+    if (shouldHandleShortcut(e)) {
+      e.preventDefault()
+      handleNodesDelete()
+      handleEdgeDelete()
+    }
+  })
+
+  useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.c`, (e) => {
+    if (shouldHandleShortcut(e)) {
+      e.preventDefault()
+      handleNodesCopy()
+    }
+  }, { exactMatch: true, useCapture: true })
+
+  useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.v`, (e) => {
+    if (shouldHandleShortcut(e)) {
+      e.preventDefault()
+      handleNodesPaste()
+    }
+  }, { exactMatch: true, useCapture: true })
+
+  useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.d`, (e) => {
+    if (shouldHandleShortcut(e)) {
+      e.preventDefault()
+      handleNodesDuplicate()
+    }
+  }, { exactMatch: true, useCapture: true })
+
+  useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, (e) => {
+    if (shouldHandleShortcut(e)) {
+      e.preventDefault()
+      handleStartWorkflowRun()
+    }
+  }, { exactMatch: true, useCapture: true })
+
+  useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.z`, (e) => {
+    if (shouldHandleShortcut(e)) {
+      e.preventDefault()
+      workflowHistoryShortcutsEnabled && handleHistoryBack()
+    }
+  }, { exactMatch: true, useCapture: true })
+
+  useKeyPress(
+    [`${getKeyboardKeyCodeBySystem('ctrl')}.y`, `${getKeyboardKeyCodeBySystem('ctrl')}.shift.z`],
+    (e) => {
+      if (shouldHandleShortcut(e)) {
+        e.preventDefault()
+        workflowHistoryShortcutsEnabled && handleHistoryForward()
+      }
+    },
+    { exactMatch: true, useCapture: true },
+  )
+
+  useKeyPress('h', (e) => {
+    if (shouldHandleShortcut(e)) {
+      e.preventDefault()
+      handleModeHand()
+    }
+  }, {
+    exactMatch: true,
+    useCapture: true,
+  })
+
+  useKeyPress('v', (e) => {
+    if (shouldHandleShortcut(e)) {
+      e.preventDefault()
+      handleModePointer()
+    }
+  }, {
+    exactMatch: true,
+    useCapture: true,
+  })
+
+  useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.o`, (e) => {
+    if (shouldHandleShortcut(e)) {
+      e.preventDefault()
+      handleLayout()
+    }
+  }, { exactMatch: true, useCapture: true })
+
+  useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.1`, (e) => {
+    if (shouldHandleShortcut(e)) {
+      e.preventDefault()
+      fitView()
+      handleSyncWorkflowDraft()
+    }
+  }, {
+    exactMatch: true,
+    useCapture: true,
+  })
+
+  useKeyPress('shift.1', (e) => {
+    if (shouldHandleShortcut(e)) {
+      e.preventDefault()
+      zoomTo(1)
+      handleSyncWorkflowDraft()
+    }
+  }, {
+    exactMatch: true,
+    useCapture: true,
+  })
+
+  useKeyPress('shift.5', (e) => {
+    if (shouldHandleShortcut(e)) {
+      e.preventDefault()
+      zoomTo(0.5)
+      handleSyncWorkflowDraft()
+    }
+  }, {
+    exactMatch: true,
+    useCapture: true,
+  })
+
+  useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.dash`, (e) => {
+    if (shouldHandleShortcut(e)) {
+      e.preventDefault()
+      zoomOut()
+      handleSyncWorkflowDraft()
+    }
+  }, {
+    exactMatch: true,
+    useCapture: true,
+  })
+
+  useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.equalsign`, (e) => {
+    if (shouldHandleShortcut(e)) {
+      e.preventDefault()
+      zoomIn()
+      handleSyncWorkflowDraft()
+    }
+  }, {
+    exactMatch: true,
+    useCapture: true,
+  })
+}

+ 168 - 4
web/app/components/workflow/hooks/use-workflow-interactions.ts

@@ -3,17 +3,29 @@ import {
   useState,
 } from 'react'
 import { useTranslation } from 'react-i18next'
-import { useReactFlow } from 'reactflow'
-import { useWorkflowStore } from '../store'
-import { DSL_EXPORT_CHECK, WORKFLOW_DATA_UPDATE } from '../constants'
-import type { WorkflowDataUpdator } from '../types'
+import { useReactFlow, useStoreApi } from 'reactflow'
+import produce from 'immer'
+import { useStore, useWorkflowStore } from '../store'
 import {
+  CUSTOM_NODE, DSL_EXPORT_CHECK,
+  WORKFLOW_DATA_UPDATE,
+} from '../constants'
+import type { Node, WorkflowDataUpdator } from '../types'
+import { ControlMode } from '../types'
+import {
+  getLayoutByDagre,
   initialEdges,
   initialNodes,
 } from '../utils'
+import {
+  useNodesReadOnly,
+  useSelectionInteractions,
+  useWorkflowReadOnly,
+} from '../hooks'
 import { useEdgesInteractions } from './use-edges-interactions'
 import { useNodesInteractions } from './use-nodes-interactions'
 import { useNodesSyncDraft } from './use-nodes-sync-draft'
+import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
 import { useEventEmitterContextContext } from '@/context/event-emitter'
 import { fetchWorkflowDraft } from '@/service/workflow'
 import { exportAppConfig } from '@/service/apps'
@@ -39,6 +51,158 @@ export const useWorkflowInteractions = () => {
   }
 }
 
+export const useWorkflowMoveMode = () => {
+  const setControlMode = useStore(s => s.setControlMode)
+  const {
+    getNodesReadOnly,
+  } = useNodesReadOnly()
+  const { handleSelectionCancel } = useSelectionInteractions()
+
+  const handleModePointer = useCallback(() => {
+    if (getNodesReadOnly())
+      return
+
+    setControlMode(ControlMode.Pointer)
+  }, [getNodesReadOnly, setControlMode])
+
+  const handleModeHand = useCallback(() => {
+    if (getNodesReadOnly())
+      return
+
+    setControlMode(ControlMode.Hand)
+    handleSelectionCancel()
+  }, [getNodesReadOnly, setControlMode, handleSelectionCancel])
+
+  return {
+    handleModePointer,
+    handleModeHand,
+  }
+}
+
+export const useWorkflowOrganize = () => {
+  const workflowStore = useWorkflowStore()
+  const store = useStoreApi()
+  const reactflow = useReactFlow()
+  const { getNodesReadOnly } = useNodesReadOnly()
+  const { saveStateToHistory } = useWorkflowHistory()
+  const { handleSyncWorkflowDraft } = useNodesSyncDraft()
+
+  const handleLayout = useCallback(async () => {
+    if (getNodesReadOnly())
+      return
+    workflowStore.setState({ nodeAnimation: true })
+    const {
+      getNodes,
+      edges,
+      setNodes,
+    } = store.getState()
+    const { setViewport } = reactflow
+    const nodes = getNodes()
+    const layout = getLayoutByDagre(nodes, edges)
+    const rankMap = {} as Record<string, Node>
+
+    nodes.forEach((node) => {
+      if (!node.parentId && node.type === CUSTOM_NODE) {
+        const rank = layout.node(node.id).rank!
+
+        if (!rankMap[rank]) {
+          rankMap[rank] = node
+        }
+        else {
+          if (rankMap[rank].position.y > node.position.y)
+            rankMap[rank] = node
+        }
+      }
+    })
+
+    const newNodes = produce(nodes, (draft) => {
+      draft.forEach((node) => {
+        if (!node.parentId && node.type === CUSTOM_NODE) {
+          const nodeWithPosition = layout.node(node.id)
+
+          node.position = {
+            x: nodeWithPosition.x - node.width! / 2,
+            y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2,
+          }
+        }
+      })
+    })
+    setNodes(newNodes)
+    const zoom = 0.7
+    setViewport({
+      x: 0,
+      y: 0,
+      zoom,
+    })
+    saveStateToHistory(WorkflowHistoryEvent.LayoutOrganize)
+    setTimeout(() => {
+      handleSyncWorkflowDraft()
+    })
+  }, [getNodesReadOnly, store, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory])
+  return {
+    handleLayout,
+  }
+}
+
+export const useWorkflowZoom = () => {
+  const { handleSyncWorkflowDraft } = useNodesSyncDraft()
+  const { getWorkflowReadOnly } = useWorkflowReadOnly()
+  const {
+    zoomIn,
+    zoomOut,
+    zoomTo,
+    fitView,
+  } = useReactFlow()
+
+  const handleFitView = useCallback(() => {
+    if (getWorkflowReadOnly())
+      return
+
+    fitView()
+    handleSyncWorkflowDraft()
+  }, [getWorkflowReadOnly, fitView, handleSyncWorkflowDraft])
+
+  const handleBackToOriginalSize = useCallback(() => {
+    if (getWorkflowReadOnly())
+      return
+
+    zoomTo(1)
+    handleSyncWorkflowDraft()
+  }, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft])
+
+  const handleSizeToHalf = useCallback(() => {
+    if (getWorkflowReadOnly())
+      return
+
+    zoomTo(0.5)
+    handleSyncWorkflowDraft()
+  }, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft])
+
+  const handleZoomOut = useCallback(() => {
+    if (getWorkflowReadOnly())
+      return
+
+    zoomOut()
+    handleSyncWorkflowDraft()
+  }, [getWorkflowReadOnly, zoomOut, handleSyncWorkflowDraft])
+
+  const handleZoomIn = useCallback(() => {
+    if (getWorkflowReadOnly())
+      return
+
+    zoomIn()
+    handleSyncWorkflowDraft()
+  }, [getWorkflowReadOnly, zoomIn, handleSyncWorkflowDraft])
+
+  return {
+    handleFitView,
+    handleBackToOriginalSize,
+    handleSizeToHalf,
+    handleZoomOut,
+    handleZoomIn,
+  }
+}
+
 export const useWorkflowUpdate = () => {
   const reactflow = useReactFlow()
   const workflowStore = useWorkflowStore()

+ 0 - 76
web/app/components/workflow/hooks/use-workflow.ts

@@ -7,19 +7,14 @@ import {
 import dayjs from 'dayjs'
 import { uniqBy } from 'lodash-es'
 import { useContext } from 'use-context-selector'
-import produce from 'immer'
 import {
   getIncomers,
   getOutgoers,
-  useReactFlow,
   useStoreApi,
 } from 'reactflow'
 import type {
   Connection,
 } from 'reactflow'
-import {
-  getLayoutByDagre,
-} from '../utils'
 import type {
   Edge,
   Node,
@@ -34,15 +29,12 @@ import {
   useWorkflowStore,
 } from '../store'
 import {
-  CUSTOM_NODE,
   SUPPORT_OUTPUT_VARS_NODE,
 } from '../constants'
 import { CUSTOM_NOTE_NODE } from '../note-node/constants'
 import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
 import { useNodesExtraData } from './use-nodes-data'
 import { useWorkflowTemplate } from './use-workflow-template'
-import { useNodesSyncDraft } from './use-nodes-sync-draft'
-import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
 import { useStore as useAppStore } from '@/app/components/app/store'
 import {
   fetchNodesDefaultConfigs,
@@ -68,68 +60,13 @@ export const useIsChatMode = () => {
 export const useWorkflow = () => {
   const { locale } = useContext(I18n)
   const store = useStoreApi()
-  const reactflow = useReactFlow()
   const workflowStore = useWorkflowStore()
   const nodesExtraData = useNodesExtraData()
-  const { handleSyncWorkflowDraft } = useNodesSyncDraft()
-  const { saveStateToHistory } = useWorkflowHistory()
-
   const setPanelWidth = useCallback((width: number) => {
     localStorage.setItem('workflow-node-panel-width', `${width}`)
     workflowStore.setState({ panelWidth: width })
   }, [workflowStore])
 
-  const handleLayout = useCallback(async () => {
-    workflowStore.setState({ nodeAnimation: true })
-    const {
-      getNodes,
-      edges,
-      setNodes,
-    } = store.getState()
-    const { setViewport } = reactflow
-    const nodes = getNodes()
-    const layout = getLayoutByDagre(nodes, edges)
-    const rankMap = {} as Record<string, Node>
-
-    nodes.forEach((node) => {
-      if (!node.parentId && node.type === CUSTOM_NODE) {
-        const rank = layout.node(node.id).rank!
-
-        if (!rankMap[rank]) {
-          rankMap[rank] = node
-        }
-        else {
-          if (rankMap[rank].position.y > node.position.y)
-            rankMap[rank] = node
-        }
-      }
-    })
-
-    const newNodes = produce(nodes, (draft) => {
-      draft.forEach((node) => {
-        if (!node.parentId && node.type === CUSTOM_NODE) {
-          const nodeWithPosition = layout.node(node.id)
-
-          node.position = {
-            x: nodeWithPosition.x - node.width! / 2,
-            y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2,
-          }
-        }
-      })
-    })
-    setNodes(newNodes)
-    const zoom = 0.7
-    setViewport({
-      x: 0,
-      y: 0,
-      zoom,
-    })
-    saveStateToHistory(WorkflowHistoryEvent.LayoutOrganize)
-    setTimeout(() => {
-      handleSyncWorkflowDraft()
-    })
-  }, [workflowStore, store, reactflow, saveStateToHistory, handleSyncWorkflowDraft])
-
   const getTreeLeafNodes = useCallback((nodeId: string) => {
     const {
       getNodes,
@@ -392,19 +329,8 @@ export const useWorkflow = () => {
     return nodes.find(node => node.id === nodeId) || nodes.find(node => node.data.type === BlockEnum.Start)
   }, [store])
 
-  const enableShortcuts = useCallback(() => {
-    const { setShortcutsDisabled } = workflowStore.getState()
-    setShortcutsDisabled(false)
-  }, [workflowStore])
-
-  const disableShortcuts = useCallback(() => {
-    const { setShortcutsDisabled } = workflowStore.getState()
-    setShortcutsDisabled(true)
-  }, [workflowStore])
-
   return {
     setPanelWidth,
-    handleLayout,
     getTreeLeafNodes,
     getBeforeNodesInSameBranch,
     getBeforeNodesInSameBranchIncludeParent,
@@ -418,8 +344,6 @@ export const useWorkflow = () => {
     getNode,
     getBeforeNodeById,
     getIterationNodeChildren,
-    enableShortcuts,
-    disableShortcuts,
   }
 }
 

+ 8 - 48
web/app/components/workflow/index.tsx

@@ -12,7 +12,6 @@ import {
 import { setAutoFreeze } from 'immer'
 import {
   useEventListener,
-  useKeyPress,
 } from 'ahooks'
 import ReactFlow, {
   Background,
@@ -34,6 +33,9 @@ import type {
   EnvironmentVariable,
   Node,
 } from './types'
+import {
+  ControlMode,
+} from './types'
 import { WorkflowContextProvider } from './context'
 import {
   useDSL,
@@ -43,10 +45,10 @@ import {
   useNodesSyncDraft,
   usePanelInteractions,
   useSelectionInteractions,
+  useShortcuts,
   useWorkflow,
   useWorkflowInit,
   useWorkflowReadOnly,
-  useWorkflowStartRun,
   useWorkflowUpdate,
 } from './hooks'
 import Header from './header'
@@ -70,10 +72,8 @@ import {
   useWorkflowStore,
 } from './store'
 import {
-  getKeyboardKeyCodeBySystem,
   initialEdges,
   initialNodes,
-  isEventTargetInputArea,
 } from './utils'
 import {
   CUSTOM_NODE,
@@ -81,7 +81,7 @@ import {
   ITERATION_CHILDREN_Z_INDEX,
   WORKFLOW_DATA_UPDATE,
 } from './constants'
-import { WorkflowHistoryProvider, useWorkflowHistoryStore } from './workflow-history-store'
+import { WorkflowHistoryProvider } from './workflow-history-store'
 import Loading from '@/app/components/base/loading'
 import { FeaturesProvider } from '@/app/components/base/features'
 import type { Features as FeaturesData } from '@/app/components/base/features/types'
@@ -225,17 +225,12 @@ const Workflow: FC<WorkflowProps> = memo(({
     handleNodeConnectStart,
     handleNodeConnectEnd,
     handleNodeContextMenu,
-    handleNodesCopy,
-    handleNodesPaste,
-    handleNodesDuplicate,
-    handleNodesDelete,
     handleHistoryBack,
     handleHistoryForward,
   } = useNodesInteractions()
   const {
     handleEdgeEnter,
     handleEdgeLeave,
-    handleEdgeDelete,
     handleEdgesChange,
   } = useEdgesInteractions()
   const {
@@ -250,7 +245,6 @@ const Workflow: FC<WorkflowProps> = memo(({
   const {
     isValidConnection,
   } = useWorkflow()
-  const { handleStartWorkflowRun } = useWorkflowStartRun()
   const {
     exportCheck,
     handleExportDSL,
@@ -262,41 +256,7 @@ const Workflow: FC<WorkflowProps> = memo(({
     },
   })
 
-  const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore()
-
-  useKeyPress(['delete', 'backspace'], (e) => {
-    if (isEventTargetInputArea(e.target as HTMLElement))
-      return
-
-    handleNodesDelete()
-  })
-  useKeyPress(['delete', 'backspace'], handleEdgeDelete)
-  useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.c`, (e) => {
-    if (isEventTargetInputArea(e.target as HTMLElement))
-      return
-
-    handleNodesCopy()
-  }, { exactMatch: true, useCapture: true })
-  useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.v`, (e) => {
-    if (isEventTargetInputArea(e.target as HTMLElement))
-      return
-
-    handleNodesPaste()
-  }, { exactMatch: true, useCapture: true })
-  useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.d`, handleNodesDuplicate, { exactMatch: true, useCapture: true })
-  useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, handleStartWorkflowRun, { exactMatch: true, useCapture: true })
-  useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, handleStartWorkflowRun, { exactMatch: true, useCapture: true })
-  useKeyPress(
-    `${getKeyboardKeyCodeBySystem('ctrl')}.z`,
-    () => workflowHistoryShortcutsEnabled && handleHistoryBack(),
-    { exactMatch: true, useCapture: true },
-  )
-
-  useKeyPress(
-    [`${getKeyboardKeyCodeBySystem('ctrl')}.y`, `${getKeyboardKeyCodeBySystem('ctrl')}.shift.z`],
-    () => workflowHistoryShortcutsEnabled && handleHistoryForward(),
-    { exactMatch: true, useCapture: true },
-  )
+  useShortcuts()
 
   const store = useStoreApi()
   if (process.env.NODE_ENV === 'development') {
@@ -388,14 +348,14 @@ const Workflow: FC<WorkflowProps> = memo(({
         nodesConnectable={!nodesReadOnly}
         nodesFocusable={!nodesReadOnly}
         edgesFocusable={!nodesReadOnly}
-        panOnDrag={controlMode === 'hand' && !workflowReadOnly}
+        panOnDrag={controlMode === ControlMode.Hand && !workflowReadOnly}
         zoomOnPinch={!workflowReadOnly}
         zoomOnScroll={!workflowReadOnly}
         zoomOnDoubleClick={!workflowReadOnly}
         isValidConnection={isValidConnection}
         selectionKeyCode={null}
         selectionMode={SelectionMode.Partial}
-        selectionOnDrag={controlMode === 'pointer' && !workflowReadOnly}
+        selectionOnDrag={controlMode === ControlMode.Pointer && !workflowReadOnly}
         minZoom={0.25}
       >
         <Background

+ 10 - 59
web/app/components/workflow/operator/control.tsx

@@ -1,7 +1,6 @@
 import type { MouseEvent } from 'react'
 import {
   memo,
-  useCallback,
 } from 'react'
 import { useTranslation } from 'react-i18next'
 import {
@@ -10,13 +9,14 @@ import {
   RiHand,
   RiStickyNoteAddLine,
 } from '@remixicon/react'
-import { useKeyPress } from 'ahooks'
 import {
   useNodesReadOnly,
-  useSelectionInteractions,
-  useWorkflow,
+  useWorkflowMoveMode,
+  useWorkflowOrganize,
 } from '../hooks'
-import { getKeyboardKeyCodeBySystem, isEventTargetInputArea } from '../utils'
+import {
+  ControlMode,
+} from '../types'
 import { useStore } from '../store'
 import AddBlock from './add-block'
 import TipPopup from './tip-popup'
@@ -26,62 +26,13 @@ import cn from '@/utils/classnames'
 const Control = () => {
   const { t } = useTranslation()
   const controlMode = useStore(s => s.controlMode)
-  const setControlMode = useStore(s => s.setControlMode)
-  const { handleLayout } = useWorkflow()
+  const { handleModePointer, handleModeHand } = useWorkflowMoveMode()
+  const { handleLayout } = useWorkflowOrganize()
   const { handleAddNote } = useOperator()
   const {
     nodesReadOnly,
     getNodesReadOnly,
   } = useNodesReadOnly()
-  const { handleSelectionCancel } = useSelectionInteractions()
-
-  const handleModePointer = useCallback(() => {
-    if (getNodesReadOnly())
-      return
-    setControlMode('pointer')
-  }, [getNodesReadOnly, setControlMode])
-  const handleModeHand = useCallback(() => {
-    if (getNodesReadOnly())
-      return
-    setControlMode('hand')
-    handleSelectionCancel()
-  }, [getNodesReadOnly, setControlMode, handleSelectionCancel])
-
-  useKeyPress('h', (e) => {
-    if (getNodesReadOnly())
-      return
-
-    if (isEventTargetInputArea(e.target as HTMLElement))
-      return
-
-    e.preventDefault()
-    handleModeHand()
-  }, {
-    exactMatch: true,
-    useCapture: true,
-  })
-
-  useKeyPress('v', (e) => {
-    if (isEventTargetInputArea(e.target as HTMLElement))
-      return
-
-    e.preventDefault()
-    handleModePointer()
-  }, {
-    exactMatch: true,
-    useCapture: true,
-  })
-
-  const goLayout = () => {
-    if (getNodesReadOnly())
-      return
-    handleLayout()
-  }
-
-  useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.o`, (e) => {
-    e.preventDefault()
-    goLayout()
-  }, { exactMatch: true, useCapture: true })
 
   const addNote = (e: MouseEvent<HTMLDivElement>) => {
     if (getNodesReadOnly())
@@ -110,7 +61,7 @@ const Control = () => {
         <div
           className={cn(
             'flex items-center justify-center mr-[1px] w-8 h-8 rounded-lg cursor-pointer',
-            controlMode === 'pointer' ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700',
+            controlMode === ControlMode.Pointer ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700',
             `${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
           )}
           onClick={handleModePointer}
@@ -122,7 +73,7 @@ const Control = () => {
         <div
           className={cn(
             'flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer',
-            controlMode === 'hand' ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700',
+            controlMode === ControlMode.Hand ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700',
             `${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
           )}
           onClick={handleModeHand}
@@ -137,7 +88,7 @@ const Control = () => {
             'flex items-center justify-center w-8 h-8 rounded-lg hover:bg-black/5 hover:text-gray-700 cursor-pointer',
             `${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
           )}
-          onClick={goLayout}
+          onClick={handleLayout}
         >
           <RiFunctionAddLine className='w-4 h-4' />
         </div>

+ 0 - 89
web/app/components/workflow/operator/zoom-in-out.tsx

@@ -9,7 +9,6 @@ import {
   RiZoomInLine,
   RiZoomOutLine,
 } from '@remixicon/react'
-import { useKeyPress } from 'ahooks'
 import { useTranslation } from 'react-i18next'
 import {
   useReactFlow,
@@ -20,9 +19,7 @@ import {
   useWorkflowReadOnly,
 } from '../hooks'
 import {
-  getKeyboardKeyCodeBySystem,
   getKeyboardKeyNameBySystem,
-  isEventTargetInputArea,
 } from '../utils'
 import ShortcutsName from '../shortcuts-name'
 import TipPopup from './tip-popup'
@@ -116,87 +113,6 @@ const ZoomInOut: FC = () => {
     handleSyncWorkflowDraft()
   }
 
-  useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.1`, (e) => {
-    e.preventDefault()
-    if (workflowReadOnly)
-      return
-
-    fitView()
-    handleSyncWorkflowDraft()
-  }, {
-    exactMatch: true,
-    useCapture: true,
-  })
-
-  useKeyPress('shift.1', (e) => {
-    if (workflowReadOnly)
-      return
-
-    if (isEventTargetInputArea(e.target as HTMLElement))
-      return
-
-    e.preventDefault()
-    zoomTo(1)
-    handleSyncWorkflowDraft()
-  }, {
-    exactMatch: true,
-    useCapture: true,
-  })
-
-  useKeyPress('shift.2', (e) => {
-    if (workflowReadOnly)
-      return
-
-    if (isEventTargetInputArea(e.target as HTMLElement))
-      return
-
-    e.preventDefault()
-    zoomTo(2)
-    handleSyncWorkflowDraft()
-  }, {
-    exactMatch: true,
-    useCapture: true,
-  })
-
-  useKeyPress('shift.5', (e) => {
-    if (workflowReadOnly)
-      return
-
-    if (isEventTargetInputArea(e.target as HTMLElement))
-      return
-
-    e.preventDefault()
-    zoomTo(0.5)
-    handleSyncWorkflowDraft()
-  }, {
-    exactMatch: true,
-    useCapture: true,
-  })
-
-  useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.dash`, (e) => {
-    e.preventDefault()
-    if (workflowReadOnly)
-      return
-
-    zoomOut()
-    handleSyncWorkflowDraft()
-  }, {
-    exactMatch: true,
-    useCapture: true,
-  })
-
-  useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.equalsign`, (e) => {
-    e.preventDefault()
-    if (workflowReadOnly)
-      return
-
-    zoomIn()
-    handleSyncWorkflowDraft()
-  }, {
-    exactMatch: true,
-    useCapture: true,
-  })
-
   const handleTrigger = useCallback(() => {
     if (getWorkflowReadOnly())
       return
@@ -289,11 +205,6 @@ const ZoomInOut: FC = () => {
                             <ShortcutsName keys={['shift', '1']} />
                           )
                         }
-                        {
-                          option.key === ZoomType.zoomTo200 && (
-                            <ShortcutsName keys={['shift', '2']} />
-                          )
-                        }
                       </div>
                     ))
                   }

+ 0 - 7
web/app/components/workflow/panel/index.tsx

@@ -7,7 +7,6 @@ import { Panel as NodePanel } from '../nodes'
 import { useStore } from '../store'
 import {
   useIsChatMode,
-  useWorkflow,
 } from '../hooks'
 import DebugAndPreview from './debug-and-preview'
 import Record from './record'
@@ -28,10 +27,6 @@ const Panel: FC = () => {
   const showEnvPanel = useStore(s => s.showEnvPanel)
   const showChatVariablePanel = useStore(s => s.showChatVariablePanel)
   const isRestoring = useStore(s => s.isRestoring)
-  const {
-    enableShortcuts,
-    disableShortcuts,
-  } = useWorkflow()
   const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
     currentLogItem: state.currentLogItem,
     setCurrentLogItem: state.setCurrentLogItem,
@@ -44,8 +39,6 @@ const Panel: FC = () => {
     <div
       tabIndex={-1}
       className={cn('absolute top-14 right-0 bottom-2 flex z-10 outline-none')}
-      onFocus={disableShortcuts}
-      onBlur={enableShortcuts}
       key={`${isRestoring}`}
     >
       {

+ 0 - 4
web/app/components/workflow/store.ts

@@ -99,8 +99,6 @@ type Shape = {
   setWorkflowTools: (tools: ToolWithProvider[]) => void
   clipboardElements: Node[]
   setClipboardElements: (clipboardElements: Node[]) => void
-  shortcutsDisabled: boolean
-  setShortcutsDisabled: (shortcutsDisabled: boolean) => void
   showDebugAndPreviewPanel: boolean
   setShowDebugAndPreviewPanel: (showDebugAndPreviewPanel: boolean) => void
   showEnvPanel: boolean
@@ -217,8 +215,6 @@ export const createWorkflowStore = () => {
     setWorkflowTools: workflowTools => set(() => ({ workflowTools })),
     clipboardElements: [],
     setClipboardElements: clipboardElements => set(() => ({ clipboardElements })),
-    shortcutsDisabled: false,
-    setShortcutsDisabled: shortcutsDisabled => set(() => ({ shortcutsDisabled })),
     showDebugAndPreviewPanel: false,
     setShowDebugAndPreviewPanel: showDebugAndPreviewPanel => set(() => ({ showDebugAndPreviewPanel })),
     showEnvPanel: false,

+ 5 - 0
web/app/components/workflow/types.ts

@@ -29,6 +29,11 @@ export enum BlockEnum {
   Assigner = 'assigner', // is now named as VariableAssigner
 }
 
+export enum ControlMode {
+  Pointer = 'pointer',
+  Hand = 'hand',
+}
+
 export type Branch = {
   id: string
   name: string