Feat: shortcut hook (#7385)

This commit is contained in:
Yi Xiao 2024-08-19 18:11:11 +08:00 committed by GitHub
parent 68dc6d5bc3
commit 8b06105fa1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 402 additions and 332 deletions

View File

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

View File

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

View File

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

View File

@ -48,6 +48,7 @@ import { useHelpline } from './use-helpline'
import { import {
useNodesReadOnly, useNodesReadOnly,
useWorkflow, useWorkflow,
useWorkflowReadOnly,
} from './use-workflow' } from './use-workflow'
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
@ -62,6 +63,7 @@ export const useNodesInteractions = () => {
getAfterNodesInSameBranch, getAfterNodesInSameBranch,
} = useWorkflow() } = useWorkflow()
const { getNodesReadOnly } = useNodesReadOnly() const { getNodesReadOnly } = useNodesReadOnly()
const { getWorkflowReadOnly } = useWorkflowReadOnly()
const { handleSetHelpline } = useHelpline() const { handleSetHelpline } = useHelpline()
const { const {
handleNodeIterationChildDrag, handleNodeIterationChildDrag,
@ -1029,14 +1031,7 @@ export const useNodesInteractions = () => {
if (getNodesReadOnly()) if (getNodesReadOnly())
return return
const { const { setClipboardElements } = workflowStore.getState()
setClipboardElements,
shortcutsDisabled,
showFeaturesPanel,
} = workflowStore.getState()
if (shortcutsDisabled || showFeaturesPanel)
return
const { const {
getNodes, getNodes,
@ -1062,14 +1057,9 @@ export const useNodesInteractions = () => {
const { const {
clipboardElements, clipboardElements,
shortcutsDisabled,
showFeaturesPanel,
mousePosition, mousePosition,
} = workflowStore.getState() } = workflowStore.getState()
if (shortcutsDisabled || showFeaturesPanel)
return
const { const {
getNodes, getNodes,
setNodes, setNodes,
@ -1107,6 +1097,11 @@ export const useNodesInteractions = () => {
}) })
newNode.id = newNode.id + index 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[] = [] let newChildren: Node[] = []
if (nodeToPaste.data.type === BlockEnum.Iteration) { if (nodeToPaste.data.type === BlockEnum.Iteration) {
newNode.data._children = []; newNode.data._children = [];
@ -1145,14 +1140,6 @@ export const useNodesInteractions = () => {
if (getNodesReadOnly()) if (getNodesReadOnly())
return return
const {
shortcutsDisabled,
showFeaturesPanel,
} = workflowStore.getState()
if (shortcutsDisabled || showFeaturesPanel)
return
const { const {
getNodes, getNodes,
edges, edges,
@ -1175,7 +1162,7 @@ export const useNodesInteractions = () => {
if (selectedNode) if (selectedNode)
handleNodeDelete(selectedNode.id) handleNodeDelete(selectedNode.id)
}, [store, workflowStore, getNodesReadOnly, handleNodeDelete]) }, [store, getNodesReadOnly, handleNodeDelete])
const handleNodeResize = useCallback((nodeId: string, params: ResizeParamsWithDirection) => { const handleNodeResize = useCallback((nodeId: string, params: ResizeParamsWithDirection) => {
if (getNodesReadOnly()) if (getNodesReadOnly())
@ -1234,14 +1221,7 @@ export const useNodesInteractions = () => {
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory]) }, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory])
const handleHistoryBack = useCallback(() => { const handleHistoryBack = useCallback(() => {
if (getNodesReadOnly()) if (getNodesReadOnly() || getWorkflowReadOnly())
return
const {
shortcutsDisabled,
} = workflowStore.getState()
if (shortcutsDisabled)
return return
const { setEdges, setNodes } = store.getState() const { setEdges, setNodes } = store.getState()
@ -1253,17 +1233,10 @@ export const useNodesInteractions = () => {
setEdges(edges) setEdges(edges)
setNodes(nodes) setNodes(nodes)
}, [store, undo, workflowHistoryStore, workflowStore, getNodesReadOnly]) }, [store, undo, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly])
const handleHistoryForward = useCallback(() => { const handleHistoryForward = useCallback(() => {
if (getNodesReadOnly()) if (getNodesReadOnly() || getWorkflowReadOnly())
return
const {
shortcutsDisabled,
} = workflowStore.getState()
if (shortcutsDisabled)
return return
const { setEdges, setNodes } = store.getState() const { setEdges, setNodes } = store.getState()
@ -1275,7 +1248,7 @@ export const useNodesInteractions = () => {
setEdges(edges) setEdges(edges)
setNodes(nodes) setNodes(nodes)
}, [redo, store, workflowHistoryStore, workflowStore, getNodesReadOnly]) }, [redo, store, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly])
return { return {
handleNodeDragStart, handleNodeDragStart,

View File

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

View File

@ -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,
})
}

View File

@ -3,17 +3,29 @@ import {
useState, useState,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useReactFlow } from 'reactflow' import { useReactFlow, useStoreApi } from 'reactflow'
import { useWorkflowStore } from '../store' import produce from 'immer'
import { DSL_EXPORT_CHECK, WORKFLOW_DATA_UPDATE } from '../constants' import { useStore, useWorkflowStore } from '../store'
import type { WorkflowDataUpdator } from '../types'
import { import {
CUSTOM_NODE, DSL_EXPORT_CHECK,
WORKFLOW_DATA_UPDATE,
} from '../constants'
import type { Node, WorkflowDataUpdator } from '../types'
import { ControlMode } from '../types'
import {
getLayoutByDagre,
initialEdges, initialEdges,
initialNodes, initialNodes,
} from '../utils' } from '../utils'
import {
useNodesReadOnly,
useSelectionInteractions,
useWorkflowReadOnly,
} from '../hooks'
import { useEdgesInteractions } from './use-edges-interactions' import { useEdgesInteractions } from './use-edges-interactions'
import { useNodesInteractions } from './use-nodes-interactions' import { useNodesInteractions } from './use-nodes-interactions'
import { useNodesSyncDraft } from './use-nodes-sync-draft' import { useNodesSyncDraft } from './use-nodes-sync-draft'
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
import { fetchWorkflowDraft } from '@/service/workflow' import { fetchWorkflowDraft } from '@/service/workflow'
import { exportAppConfig } from '@/service/apps' 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 = () => { export const useWorkflowUpdate = () => {
const reactflow = useReactFlow() const reactflow = useReactFlow()
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()

View File

@ -7,19 +7,14 @@ import {
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { uniqBy } from 'lodash-es' import { uniqBy } from 'lodash-es'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import produce from 'immer'
import { import {
getIncomers, getIncomers,
getOutgoers, getOutgoers,
useReactFlow,
useStoreApi, useStoreApi,
} from 'reactflow' } from 'reactflow'
import type { import type {
Connection, Connection,
} from 'reactflow' } from 'reactflow'
import {
getLayoutByDagre,
} from '../utils'
import type { import type {
Edge, Edge,
Node, Node,
@ -34,15 +29,12 @@ import {
useWorkflowStore, useWorkflowStore,
} from '../store' } from '../store'
import { import {
CUSTOM_NODE,
SUPPORT_OUTPUT_VARS_NODE, SUPPORT_OUTPUT_VARS_NODE,
} from '../constants' } from '../constants'
import { CUSTOM_NOTE_NODE } from '../note-node/constants' import { CUSTOM_NOTE_NODE } from '../note-node/constants'
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils' import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
import { useNodesExtraData } from './use-nodes-data' import { useNodesExtraData } from './use-nodes-data'
import { useWorkflowTemplate } from './use-workflow-template' 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 { useStore as useAppStore } from '@/app/components/app/store'
import { import {
fetchNodesDefaultConfigs, fetchNodesDefaultConfigs,
@ -68,68 +60,13 @@ export const useIsChatMode = () => {
export const useWorkflow = () => { export const useWorkflow = () => {
const { locale } = useContext(I18n) const { locale } = useContext(I18n)
const store = useStoreApi() const store = useStoreApi()
const reactflow = useReactFlow()
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
const nodesExtraData = useNodesExtraData() const nodesExtraData = useNodesExtraData()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { saveStateToHistory } = useWorkflowHistory()
const setPanelWidth = useCallback((width: number) => { const setPanelWidth = useCallback((width: number) => {
localStorage.setItem('workflow-node-panel-width', `${width}`) localStorage.setItem('workflow-node-panel-width', `${width}`)
workflowStore.setState({ panelWidth: width }) workflowStore.setState({ panelWidth: width })
}, [workflowStore]) }, [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 getTreeLeafNodes = useCallback((nodeId: string) => {
const { const {
getNodes, getNodes,
@ -392,19 +329,8 @@ export const useWorkflow = () => {
return nodes.find(node => node.id === nodeId) || nodes.find(node => node.data.type === BlockEnum.Start) return nodes.find(node => node.id === nodeId) || nodes.find(node => node.data.type === BlockEnum.Start)
}, [store]) }, [store])
const enableShortcuts = useCallback(() => {
const { setShortcutsDisabled } = workflowStore.getState()
setShortcutsDisabled(false)
}, [workflowStore])
const disableShortcuts = useCallback(() => {
const { setShortcutsDisabled } = workflowStore.getState()
setShortcutsDisabled(true)
}, [workflowStore])
return { return {
setPanelWidth, setPanelWidth,
handleLayout,
getTreeLeafNodes, getTreeLeafNodes,
getBeforeNodesInSameBranch, getBeforeNodesInSameBranch,
getBeforeNodesInSameBranchIncludeParent, getBeforeNodesInSameBranchIncludeParent,
@ -418,8 +344,6 @@ export const useWorkflow = () => {
getNode, getNode,
getBeforeNodeById, getBeforeNodeById,
getIterationNodeChildren, getIterationNodeChildren,
enableShortcuts,
disableShortcuts,
} }
} }

View File

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

View File

@ -1,7 +1,6 @@
import type { MouseEvent } from 'react' import type { MouseEvent } from 'react'
import { import {
memo, memo,
useCallback,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
@ -10,13 +9,14 @@ import {
RiHand, RiHand,
RiStickyNoteAddLine, RiStickyNoteAddLine,
} from '@remixicon/react' } from '@remixicon/react'
import { useKeyPress } from 'ahooks'
import { import {
useNodesReadOnly, useNodesReadOnly,
useSelectionInteractions, useWorkflowMoveMode,
useWorkflow, useWorkflowOrganize,
} from '../hooks' } from '../hooks'
import { getKeyboardKeyCodeBySystem, isEventTargetInputArea } from '../utils' import {
ControlMode,
} from '../types'
import { useStore } from '../store' import { useStore } from '../store'
import AddBlock from './add-block' import AddBlock from './add-block'
import TipPopup from './tip-popup' import TipPopup from './tip-popup'
@ -26,62 +26,13 @@ import cn from '@/utils/classnames'
const Control = () => { const Control = () => {
const { t } = useTranslation() const { t } = useTranslation()
const controlMode = useStore(s => s.controlMode) const controlMode = useStore(s => s.controlMode)
const setControlMode = useStore(s => s.setControlMode) const { handleModePointer, handleModeHand } = useWorkflowMoveMode()
const { handleLayout } = useWorkflow() const { handleLayout } = useWorkflowOrganize()
const { handleAddNote } = useOperator() const { handleAddNote } = useOperator()
const { const {
nodesReadOnly, nodesReadOnly,
getNodesReadOnly, getNodesReadOnly,
} = useNodesReadOnly() } = 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>) => { const addNote = (e: MouseEvent<HTMLDivElement>) => {
if (getNodesReadOnly()) if (getNodesReadOnly())
@ -110,7 +61,7 @@ const Control = () => {
<div <div
className={cn( className={cn(
'flex items-center justify-center mr-[1px] w-8 h-8 rounded-lg cursor-pointer', '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'}`, `${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
)} )}
onClick={handleModePointer} onClick={handleModePointer}
@ -122,7 +73,7 @@ const Control = () => {
<div <div
className={cn( className={cn(
'flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer', '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'}`, `${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
)} )}
onClick={handleModeHand} 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', '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'}`, `${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
)} )}
onClick={goLayout} onClick={handleLayout}
> >
<RiFunctionAddLine className='w-4 h-4' /> <RiFunctionAddLine className='w-4 h-4' />
</div> </div>

View File

@ -9,7 +9,6 @@ import {
RiZoomInLine, RiZoomInLine,
RiZoomOutLine, RiZoomOutLine,
} from '@remixicon/react' } from '@remixicon/react'
import { useKeyPress } from 'ahooks'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
useReactFlow, useReactFlow,
@ -20,9 +19,7 @@ import {
useWorkflowReadOnly, useWorkflowReadOnly,
} from '../hooks' } from '../hooks'
import { import {
getKeyboardKeyCodeBySystem,
getKeyboardKeyNameBySystem, getKeyboardKeyNameBySystem,
isEventTargetInputArea,
} from '../utils' } from '../utils'
import ShortcutsName from '../shortcuts-name' import ShortcutsName from '../shortcuts-name'
import TipPopup from './tip-popup' import TipPopup from './tip-popup'
@ -116,87 +113,6 @@ const ZoomInOut: FC = () => {
handleSyncWorkflowDraft() 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(() => { const handleTrigger = useCallback(() => {
if (getWorkflowReadOnly()) if (getWorkflowReadOnly())
return return
@ -289,11 +205,6 @@ const ZoomInOut: FC = () => {
<ShortcutsName keys={['shift', '1']} /> <ShortcutsName keys={['shift', '1']} />
) )
} }
{
option.key === ZoomType.zoomTo200 && (
<ShortcutsName keys={['shift', '2']} />
)
}
</div> </div>
)) ))
} }

View File

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

View File

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

View File

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