mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
237 lines
7.3 KiB
TypeScript
237 lines
7.3 KiB
TypeScript
import type { HotkeyCallback, UseHotkeyDefinition, UseHotkeyOptions } from '@tanstack/react-hotkeys'
|
|
import type { WorkflowCanvasHotkeyDefinition, WorkflowCanvasHotkeyMeta } from './definitions'
|
|
import { useHotkeys, useKeyHold } from '@tanstack/react-hotkeys'
|
|
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
import { useReactFlow } from 'reactflow'
|
|
import { collaborationManager } from '../collaboration/core/collaboration-manager'
|
|
import { useEdgesInteractions } from '../hooks/use-edges-interactions'
|
|
import { useNodesInteractions } from '../hooks/use-nodes-interactions'
|
|
import { useNodesSyncDraft } from '../hooks/use-nodes-sync-draft'
|
|
import { useWorkflowOrganize } from '../hooks/use-workflow-organize'
|
|
import { useWorkflowMoveMode } from '../hooks/use-workflow-panel-interactions'
|
|
import { useStore } from '../store/workflow'
|
|
import { WORKFLOW_CANVAS_SHORTCUTS } from './definitions'
|
|
|
|
const workflowHotkeyOptions = {
|
|
ignoreInputs: true,
|
|
conflictBehavior: 'warn',
|
|
} satisfies UseHotkeyOptions
|
|
|
|
const isInputLikeElement = (element: Element | null) => {
|
|
if (!element) return false
|
|
|
|
return (
|
|
element instanceof HTMLInputElement ||
|
|
element instanceof HTMLTextAreaElement ||
|
|
element instanceof HTMLSelectElement ||
|
|
(element instanceof HTMLElement && element.isContentEditable)
|
|
)
|
|
}
|
|
|
|
const toHotkeyDefinitions = (
|
|
shortcut: WorkflowCanvasHotkeyDefinition,
|
|
callback: HotkeyCallback,
|
|
options?: UseHotkeyOptions,
|
|
): UseHotkeyDefinition[] => {
|
|
return shortcut.hotkeys.map((hotkey) => ({
|
|
hotkey,
|
|
callback,
|
|
options: {
|
|
...options,
|
|
meta: {
|
|
id: shortcut.id,
|
|
scope: 'workflow-canvas',
|
|
name: shortcut.name,
|
|
description: shortcut.description,
|
|
} satisfies WorkflowCanvasHotkeyMeta,
|
|
},
|
|
}))
|
|
}
|
|
|
|
export const useWorkflowHotkeys = (): void => {
|
|
const {
|
|
handleNodesCopy,
|
|
handleNodesPaste,
|
|
handleNodesDuplicate,
|
|
handleNodesDelete,
|
|
handleHistoryBack,
|
|
handleHistoryForward,
|
|
dimOtherNodes,
|
|
undimAllNodes,
|
|
} = useNodesInteractions()
|
|
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
|
const { handleEdgeDelete } = useEdgesInteractions()
|
|
const showDebugAndPreviewPanel = useStore((s) => s.showDebugAndPreviewPanel)
|
|
const historyShortcutsEnabled = useStore((s) => s.historyShortcutsEnabled)
|
|
const { handleModeHand, handleModePointer, handleModeComment, isCommentModeAvailable } =
|
|
useWorkflowMoveMode()
|
|
const { handleLayout } = useWorkflowOrganize()
|
|
|
|
const { zoomTo, getZoom, fitView, getNodes } = useReactFlow()
|
|
const isShiftHeld = useKeyHold(WORKFLOW_CANVAS_SHORTCUTS['workflow.dim-other-nodes'].holdKey)
|
|
const shiftDimmedRef = useRef(false)
|
|
const undimAllNodesRef = useRef(undimAllNodes)
|
|
undimAllNodesRef.current = undimAllNodes
|
|
|
|
const constrainedZoomOut = useCallback(() => {
|
|
const currentZoom = getZoom()
|
|
const newZoom = Math.max(currentZoom - 0.1, 0.25)
|
|
zoomTo(newZoom)
|
|
}, [getZoom, zoomTo])
|
|
|
|
const constrainedZoomIn = useCallback(() => {
|
|
const currentZoom = getZoom()
|
|
const newZoom = Math.min(currentZoom + 0.1, 2)
|
|
zoomTo(newZoom)
|
|
}, [getZoom, zoomTo])
|
|
|
|
const shouldHandleCopy = useCallback(() => {
|
|
if (getNodes().some((node) => node.data._isBundled)) return true
|
|
|
|
const selection = document.getSelection()
|
|
return !selection || selection.isCollapsed || !selection.rangeCount
|
|
}, [getNodes])
|
|
|
|
const handleCopy = useCallback<HotkeyCallback>(
|
|
(event) => {
|
|
if (!shouldHandleCopy()) return
|
|
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
handleNodesCopy()
|
|
},
|
|
[handleNodesCopy, shouldHandleCopy],
|
|
)
|
|
|
|
const hotkeys = useMemo<UseHotkeyDefinition[]>(
|
|
() => [
|
|
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.delete'], () => {
|
|
handleNodesDelete()
|
|
handleEdgeDelete()
|
|
}),
|
|
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.copy'], handleCopy, {
|
|
preventDefault: false,
|
|
stopPropagation: false,
|
|
enabled: !showDebugAndPreviewPanel,
|
|
}),
|
|
...toHotkeyDefinitions(
|
|
WORKFLOW_CANVAS_SHORTCUTS['workflow.paste'],
|
|
() => {
|
|
handleNodesPaste()
|
|
},
|
|
{
|
|
enabled: !showDebugAndPreviewPanel,
|
|
},
|
|
),
|
|
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.duplicate'], () => {
|
|
handleNodesDuplicate()
|
|
}),
|
|
...toHotkeyDefinitions(
|
|
WORKFLOW_CANVAS_SHORTCUTS['workflow.undo'],
|
|
() => {
|
|
handleHistoryBack()
|
|
},
|
|
{
|
|
enabled: !showDebugAndPreviewPanel && historyShortcutsEnabled,
|
|
},
|
|
),
|
|
...toHotkeyDefinitions(
|
|
WORKFLOW_CANVAS_SHORTCUTS['workflow.redo'],
|
|
() => {
|
|
handleHistoryForward()
|
|
},
|
|
{
|
|
enabled: !showDebugAndPreviewPanel && historyShortcutsEnabled,
|
|
},
|
|
),
|
|
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.hand-mode'], () => {
|
|
handleModeHand()
|
|
}),
|
|
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.pointer-mode'], () => {
|
|
handleModePointer()
|
|
}),
|
|
...toHotkeyDefinitions(
|
|
WORKFLOW_CANVAS_SHORTCUTS['workflow.comment-mode'],
|
|
() => {
|
|
handleModeComment()
|
|
},
|
|
{
|
|
enabled: isCommentModeAvailable,
|
|
},
|
|
),
|
|
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.organize'], () => {
|
|
handleLayout()
|
|
}),
|
|
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.zoom-to-fit'], () => {
|
|
fitView()
|
|
handleSyncWorkflowDraft()
|
|
}),
|
|
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.zoom-to-100'], () => {
|
|
zoomTo(1)
|
|
handleSyncWorkflowDraft()
|
|
}),
|
|
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.zoom-to-50'], () => {
|
|
zoomTo(0.5)
|
|
handleSyncWorkflowDraft()
|
|
}),
|
|
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.zoom-out'], () => {
|
|
constrainedZoomOut()
|
|
handleSyncWorkflowDraft()
|
|
}),
|
|
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.zoom-in'], () => {
|
|
constrainedZoomIn()
|
|
handleSyncWorkflowDraft()
|
|
}),
|
|
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.download-import-log'], () => {
|
|
collaborationManager.downloadGraphImportLog()
|
|
}),
|
|
],
|
|
[
|
|
constrainedZoomIn,
|
|
constrainedZoomOut,
|
|
fitView,
|
|
handleCopy,
|
|
handleEdgeDelete,
|
|
handleHistoryBack,
|
|
handleHistoryForward,
|
|
handleLayout,
|
|
handleModeComment,
|
|
handleModeHand,
|
|
handleModePointer,
|
|
handleNodesDelete,
|
|
handleNodesDuplicate,
|
|
handleNodesPaste,
|
|
handleSyncWorkflowDraft,
|
|
historyShortcutsEnabled,
|
|
isCommentModeAvailable,
|
|
showDebugAndPreviewPanel,
|
|
zoomTo,
|
|
],
|
|
)
|
|
|
|
useHotkeys(hotkeys, workflowHotkeyOptions)
|
|
|
|
useEffect(() => {
|
|
if (isShiftHeld) {
|
|
if (shiftDimmedRef.current) return
|
|
|
|
if (isInputLikeElement(document.activeElement)) return
|
|
|
|
shiftDimmedRef.current = true
|
|
dimOtherNodes()
|
|
return
|
|
}
|
|
|
|
if (!shiftDimmedRef.current) return
|
|
|
|
shiftDimmedRef.current = false
|
|
undimAllNodes()
|
|
}, [dimOtherNodes, isShiftHeld, undimAllNodes])
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (shiftDimmedRef.current) undimAllNodesRef.current()
|
|
}
|
|
}, [])
|
|
}
|