mirror of
https://github.com/langgenius/dify.git
synced 2026-06-07 16:32:01 +08:00
refactor(web): scope workflow hotkeys (#36860)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
ec5404cc9d
commit
2fe8c48255
@ -3405,14 +3405,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/header/run-mode.tsx": {
|
||||
"no-console": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/header/test-run-menu.tsx": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
|
||||
@ -340,7 +340,6 @@ vi.mock('../syncing-data-modal', () => ({
|
||||
|
||||
vi.mock('../shortcuts/use-workflow-hotkeys', () => ({
|
||||
useWorkflowHotkeys: workflowHookMocks.useShortcuts,
|
||||
useWorkflowShortcut: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../hooks', () => ({
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { TestRunMenuRef } from '../test-run-menu'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
@ -13,6 +14,10 @@ const mockHandleWorkflowRunAllTriggersInWorkflow = vi.fn()
|
||||
const mockHandleStopRun = vi.fn()
|
||||
const mockNotify = vi.fn()
|
||||
const mockTrackEvent = vi.fn()
|
||||
const hotkeyRegistrations = vi.hoisted(() => new Map<string, {
|
||||
callback: () => void
|
||||
options?: { ignoreInputs?: boolean }
|
||||
}>())
|
||||
|
||||
let mockWarningNodes: Array<{ id: string }> = []
|
||||
let mockWorkflowRunningData: { result: { status: WorkflowRunningStatus }, task_id: string } | undefined
|
||||
@ -42,9 +47,15 @@ vi.mock('@/app/components/workflow/store/workflow', () => ({
|
||||
selector({ workflowRunningData: mockWorkflowRunningData, isListening: mockIsListening }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/shortcuts/use-workflow-hotkeys', () => ({
|
||||
useWorkflowShortcut: vi.fn(),
|
||||
}))
|
||||
vi.mock('@tanstack/react-hotkeys', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@tanstack/react-hotkeys')>()
|
||||
return {
|
||||
...actual,
|
||||
useHotkey: (hotkey: string, callback: () => void, options?: { ignoreInputs?: boolean }) => {
|
||||
hotkeyRegistrations.set(hotkey, { callback, options })
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../../hooks/use-dynamic-test-run-options', () => ({
|
||||
useDynamicTestRunOptions: () => mockDynamicOptions,
|
||||
@ -77,21 +88,23 @@ vi.mock('@/app/components/base/icons/src/vender/line/mediaAndDevices', () => ({
|
||||
|
||||
vi.mock('../test-run-menu', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../test-run-menu')>()
|
||||
const TestRunMenuMock = ({ children, options, onSelect, ref }: { children: ReactNode, options: Array<{ type: TriggerType, nodeId?: string, relatedNodeIds?: string[] }>, onSelect: (option: { type: TriggerType, nodeId?: string, relatedNodeIds?: string[] }) => void, ref?: React.Ref<TestRunMenuRef> }) => {
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
toggle: vi.fn(),
|
||||
}))
|
||||
return (
|
||||
<div>
|
||||
<button data-testid="trigger-option" onClick={() => onSelect(options[0]!)}>
|
||||
Trigger option
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
...actual,
|
||||
default: React.forwardRef(({ children, options, onSelect }: { children: ReactNode, options: Array<{ type: TriggerType, nodeId?: string, relatedNodeIds?: string[] }>, onSelect: (option: { type: TriggerType, nodeId?: string, relatedNodeIds?: string[] }) => void }, ref) => {
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
toggle: vi.fn(),
|
||||
}))
|
||||
return (
|
||||
<div>
|
||||
<button data-testid="trigger-option" onClick={() => onSelect(options[0]!)}>
|
||||
Trigger option
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}),
|
||||
default: TestRunMenuMock,
|
||||
}
|
||||
})
|
||||
|
||||
@ -101,6 +114,7 @@ describe('RunMode', () => {
|
||||
mockWarningNodes = []
|
||||
mockWorkflowRunningData = undefined
|
||||
mockIsListening = false
|
||||
hotkeyRegistrations.clear()
|
||||
mockDynamicOptions = [
|
||||
{ type: TriggerType.UserInput, nodeId: 'start-node' },
|
||||
]
|
||||
@ -150,4 +164,12 @@ describe('RunMode', () => {
|
||||
|
||||
expect(screen.getByText(/listening/i))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should register the test run menu shortcut as a page command outside text inputs', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
expect(hotkeyRegistrations.get('Alt+R')?.options).toEqual(
|
||||
expect.objectContaining({ ignoreInputs: true }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,7 +2,10 @@ import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import VersionHistoryButton from '../version-history-button'
|
||||
|
||||
let mockTheme: 'light' | 'dark' = 'light'
|
||||
const workflowShortcutHandlers = vi.hoisted(() => new Map<string, () => void | Promise<void>>())
|
||||
const hotkeyRegistrations = vi.hoisted(() => new Map<string, {
|
||||
callback: () => void
|
||||
options?: { ignoreInputs?: boolean }
|
||||
}>())
|
||||
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({
|
||||
@ -10,11 +13,15 @@ vi.mock('@/hooks/use-theme', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../shortcuts/use-workflow-hotkeys', () => ({
|
||||
useWorkflowShortcut: (id: string, callback: () => void | Promise<void>) => {
|
||||
workflowShortcutHandlers.set(id, callback)
|
||||
},
|
||||
}))
|
||||
vi.mock('@tanstack/react-hotkeys', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@tanstack/react-hotkeys')>()
|
||||
return {
|
||||
...actual,
|
||||
useHotkey: (hotkey: string, callback: () => void, options?: { ignoreInputs?: boolean }) => {
|
||||
hotkeyRegistrations.set(hotkey, { callback, options })
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@langgenius/dify-ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
@ -25,7 +32,7 @@ vi.mock('@langgenius/dify-ui/tooltip', () => ({
|
||||
describe('VersionHistoryButton', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
workflowShortcutHandlers.clear()
|
||||
hotkeyRegistrations.clear()
|
||||
mockTheme = 'light'
|
||||
})
|
||||
|
||||
@ -43,10 +50,13 @@ describe('VersionHistoryButton', () => {
|
||||
render(<VersionHistoryButton onClick={onClick} />)
|
||||
|
||||
await act(async () => {
|
||||
await workflowShortcutHandlers.get('workflow.version-history')?.()
|
||||
hotkeyRegistrations.get('Mod+Shift+H')?.callback()
|
||||
})
|
||||
|
||||
expect(onClick).toHaveBeenCalledTimes(1)
|
||||
expect(hotkeyRegistrations.get('Mod+Shift+H')?.options).toEqual(
|
||||
expect.objectContaining({ ignoreInputs: true }),
|
||||
)
|
||||
})
|
||||
|
||||
it('should render the tooltip popup content on hover', async () => {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { TestRunMenuRef, TriggerOption } from './test-run-menu'
|
||||
import type { EventEmitterValue } from '@/context/event-emitter'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useHotkey } from '@tanstack/react-hotkeys'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -8,18 +10,21 @@ import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { useWorkflowRun, useWorkflowRunValidation, useWorkflowStartRun } from '@/app/components/workflow/hooks'
|
||||
import { ShortcutKbd } from '@/app/components/workflow/shortcuts/shortcut-kbd'
|
||||
import { useWorkflowShortcut } from '@/app/components/workflow/shortcuts/use-workflow-hotkeys'
|
||||
import { useStore } from '@/app/components/workflow/store/workflow'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useDynamicTestRunOptions } from '../hooks/use-dynamic-test-run-options'
|
||||
import { TEST_RUN_MENU_HOTKEY } from './shortcuts'
|
||||
import TestRunMenu, { TriggerType } from './test-run-menu'
|
||||
|
||||
type RunModeProps = {
|
||||
text?: string
|
||||
}
|
||||
|
||||
const isWorkflowStopEvent = (value: EventEmitterValue) =>
|
||||
typeof value !== 'string' && value.type === EVENT_WORKFLOW_STOP
|
||||
|
||||
const RunMode = ({
|
||||
text,
|
||||
}: RunModeProps) => {
|
||||
@ -46,7 +51,9 @@ const RunMode = ({
|
||||
testRunMenuRef.current?.toggle()
|
||||
}, [])
|
||||
|
||||
useWorkflowShortcut('workflow.open-test-run-menu', handleToggleTestRunMenu)
|
||||
useHotkey(TEST_RUN_MENU_HOTKEY, handleToggleTestRunMenu, {
|
||||
ignoreInputs: true,
|
||||
})
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
handleStopRun(workflowRunningData?.task_id || '')
|
||||
@ -88,15 +95,11 @@ const RunMode = ({
|
||||
handleWorkflowRunAllTriggersInWorkflow(targetNodeIds)
|
||||
trackEvent('app_start_action_time', { action_type: 'all' })
|
||||
}
|
||||
else {
|
||||
// Placeholder for trigger-specific execution logic for schedule, webhook, plugin types
|
||||
console.log('TODO: Handle trigger execution for type:', option.type, 'nodeId:', option.nodeId)
|
||||
}
|
||||
}, [warningNodes, t, handleWorkflowStartRunInWorkflow, handleWorkflowTriggerScheduleRunInWorkflow, handleWorkflowTriggerWebhookRunInWorkflow, handleWorkflowTriggerPluginRunInWorkflow, handleWorkflowRunAllTriggersInWorkflow])
|
||||
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === EVENT_WORKFLOW_STOP)
|
||||
eventEmitter?.useSubscription((v: EventEmitterValue) => {
|
||||
if (isWorkflowStopEvent(v))
|
||||
handleStop()
|
||||
})
|
||||
|
||||
@ -131,7 +134,7 @@ const RunMode = ({
|
||||
>
|
||||
<span aria-hidden className="mr-1 i-ri-play-large-line size-4" />
|
||||
{text ?? t('common.run', { ns: 'workflow' })}
|
||||
<ShortcutKbd shortcut="workflow.open-test-run-menu" textColor="secondary" />
|
||||
<ShortcutKbd hotkey={TEST_RUN_MENU_HOTKEY} textColor="secondary" />
|
||||
</button>
|
||||
</TestRunMenu>
|
||||
)
|
||||
|
||||
1
web/app/components/workflow/header/shortcuts.ts
Normal file
1
web/app/components/workflow/header/shortcuts.ts
Normal file
@ -0,0 +1 @@
|
||||
export const TEST_RUN_MENU_HOTKEY = 'Alt+R'
|
||||
@ -6,12 +6,14 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@langgenius/dify-ui/tooltip'
|
||||
import { useHotkey } from '@tanstack/react-hotkeys'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { ShortcutKbd } from '../shortcuts/shortcut-kbd'
|
||||
import { useWorkflowShortcut } from '../shortcuts/use-workflow-hotkeys'
|
||||
|
||||
const VERSION_HISTORY_HOTKEY = 'Mod+Shift+H'
|
||||
|
||||
type VersionHistoryButtonProps = {
|
||||
onClick: () => Promise<unknown> | unknown
|
||||
@ -24,7 +26,7 @@ const PopupContent = React.memo(() => {
|
||||
<div className="px-0.5 system-xs-medium text-text-secondary">
|
||||
{t('common.versionHistory', { ns: 'workflow' })}
|
||||
</div>
|
||||
<ShortcutKbd shortcut="workflow.version-history" bgColor="gray" textColor="secondary" />
|
||||
<ShortcutKbd hotkey={VERSION_HISTORY_HOTKEY} bgColor="gray" textColor="secondary" />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@ -39,8 +41,10 @@ const VersionHistoryButton: FC<VersionHistoryButtonProps> = ({
|
||||
await onClick?.()
|
||||
}, [onClick])
|
||||
|
||||
useWorkflowShortcut('workflow.version-history', () => {
|
||||
handleViewVersionHistory()
|
||||
useHotkey(VERSION_HISTORY_HOTKEY, () => {
|
||||
void handleViewVersionHistory()
|
||||
}, {
|
||||
ignoreInputs: true,
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@ -12,6 +12,9 @@ type KeyPressRegistration = {
|
||||
ignoreInputs?: boolean
|
||||
preventDefault?: boolean
|
||||
stopPropagation?: boolean
|
||||
meta?: {
|
||||
scope?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,6 +190,9 @@ describe('useShortcuts', () => {
|
||||
renderWorkflowHook(() => useWorkflowHotkeys())
|
||||
|
||||
const deleteShortcut = findRegistration(registration => registration.keyFilter === 'Delete')
|
||||
expect(deleteShortcut.options?.meta).toEqual(
|
||||
expect.objectContaining({ scope: 'workflow-canvas' }),
|
||||
)
|
||||
|
||||
const bodyEvent = createKeyboardEvent()
|
||||
triggerShortcut(deleteShortcut, bodyEvent)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { RegisterableHotkey } from '@tanstack/react-hotkeys'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { WorkflowShortcutId } from '@/app/components/workflow/shortcuts/definitions'
|
||||
import type { WorkflowCanvasShortcutId } from '@/app/components/workflow/shortcuts/definitions'
|
||||
import { ShortcutKbd } from '@/app/components/workflow/shortcuts/shortcut-kbd'
|
||||
|
||||
export const NODE_ACTIONS_MENU_WIDTH_CLASS_NAME = 'w-[240px] rounded-lg'
|
||||
@ -14,7 +14,7 @@ export function NodeActionsMenuItemContent({
|
||||
}: {
|
||||
children: ReactNode
|
||||
hotkey?: RegisterableHotkey | (string & {})
|
||||
shortcut?: WorkflowShortcutId
|
||||
shortcut?: WorkflowCanvasShortcutId
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import type { FC } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { useHotkey } from '@tanstack/react-hotkeys'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ShortcutKbd } from '@/app/components/workflow/shortcuts/shortcut-kbd'
|
||||
import { useWorkflowShortcut } from '@/app/components/workflow/shortcuts/use-workflow-hotkeys'
|
||||
|
||||
const JSON_SCHEMA_CONFIRM_HOTKEY = 'Mod+Enter'
|
||||
|
||||
type AdvancedActionsProps = {
|
||||
isConfirmDisabled: boolean
|
||||
@ -18,7 +20,7 @@ const AdvancedActions: FC<AdvancedActionsProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
useWorkflowShortcut('workflow.json-schema-confirm', () => {
|
||||
useHotkey(JSON_SCHEMA_CONFIRM_HOTKEY, () => {
|
||||
onConfirm()
|
||||
}, {
|
||||
enabled: !isConfirmDisabled,
|
||||
@ -38,7 +40,7 @@ const AdvancedActions: FC<AdvancedActionsProps> = ({
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<span>{t('operation.confirm', { ns: 'common' })}</span>
|
||||
<ShortcutKbd shortcut="workflow.json-schema-confirm" bgColor="white" />
|
||||
<ShortcutKbd hotkey={JSON_SCHEMA_CONFIRM_HOTKEY} bgColor="white" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { WorkflowShortcutId } from '../shortcuts/definitions'
|
||||
import type { WorkflowCanvasShortcutId } from '../shortcuts/definitions'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@ -11,7 +11,7 @@ import { ShortcutKbd } from '../shortcuts/shortcut-kbd'
|
||||
type TipPopupProps = {
|
||||
title: string
|
||||
children: ReactElement
|
||||
shortcut?: WorkflowShortcutId
|
||||
shortcut?: WorkflowCanvasShortcutId
|
||||
}
|
||||
const TipPopup = ({
|
||||
title,
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { TEST_RUN_MENU_HOTKEY } from './header/shortcuts'
|
||||
import {
|
||||
useDSL,
|
||||
useIsChatMode,
|
||||
@ -120,7 +121,7 @@ export function PanelContextmenu({
|
||||
onClick={handleRunAction}
|
||||
>
|
||||
{isChatMode ? t('common.debugAndPreview', { ns: 'workflow' }) : t('common.run', { ns: 'workflow' })}
|
||||
{!isChatMode && <ShortcutKbd shortcut="workflow.open-test-run-menu" />}
|
||||
{!isChatMode && <ShortcutKbd hotkey={TEST_RUN_MENU_HOTKEY} />}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuGroup>
|
||||
<ContextMenuSeparator />
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import type { RegisterableHotkey } from '@tanstack/react-hotkeys'
|
||||
|
||||
export type WorkflowShortcutId
|
||||
export type WorkflowCanvasShortcutId
|
||||
= | 'workflow.delete'
|
||||
| 'workflow.copy'
|
||||
| 'workflow.paste'
|
||||
| 'workflow.duplicate'
|
||||
| 'workflow.open-test-run-menu'
|
||||
| 'workflow.undo'
|
||||
| 'workflow.redo'
|
||||
| 'workflow.pointer-mode'
|
||||
@ -20,25 +19,23 @@ export type WorkflowShortcutId
|
||||
| 'workflow.zoom-in'
|
||||
| 'workflow.download-import-log'
|
||||
| 'workflow.dim-other-nodes'
|
||||
| 'workflow.json-schema-confirm'
|
||||
| 'workflow.version-history'
|
||||
|
||||
export type WorkflowHotkeyMeta = {
|
||||
id: WorkflowShortcutId
|
||||
scope: 'workflow'
|
||||
export type WorkflowCanvasHotkeyMeta = {
|
||||
id: WorkflowCanvasShortcutId
|
||||
scope: 'workflow-canvas'
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export type WorkflowShortcutDefinition = {
|
||||
id: WorkflowShortcutId
|
||||
export type WorkflowCanvasShortcutDefinition = {
|
||||
id: WorkflowCanvasShortcutId
|
||||
hotkeys: readonly RegisterableHotkey[]
|
||||
displayHotkey?: RegisterableHotkey | (string & {})
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export const WORKFLOW_SHORTCUTS: Record<WorkflowShortcutId, WorkflowShortcutDefinition> = {
|
||||
export const WORKFLOW_CANVAS_SHORTCUTS: Record<WorkflowCanvasShortcutId, WorkflowCanvasShortcutDefinition> = {
|
||||
'workflow.delete': {
|
||||
id: 'workflow.delete',
|
||||
hotkeys: ['Delete', 'Backspace'],
|
||||
@ -64,12 +61,6 @@ export const WORKFLOW_SHORTCUTS: Record<WorkflowShortcutId, WorkflowShortcutDefi
|
||||
name: 'Duplicate',
|
||||
description: 'Duplicate selected workflow nodes',
|
||||
},
|
||||
'workflow.open-test-run-menu': {
|
||||
id: 'workflow.open-test-run-menu',
|
||||
hotkeys: ['Alt+R'],
|
||||
name: 'Open test run menu',
|
||||
description: 'Open the workflow test run menu',
|
||||
},
|
||||
'workflow.undo': {
|
||||
id: 'workflow.undo',
|
||||
hotkeys: ['Mod+Z'],
|
||||
@ -157,21 +148,9 @@ export const WORKFLOW_SHORTCUTS: Record<WorkflowShortcutId, WorkflowShortcutDefi
|
||||
name: 'Dim other nodes',
|
||||
description: 'Dim nodes outside the current workflow selection',
|
||||
},
|
||||
'workflow.json-schema-confirm': {
|
||||
id: 'workflow.json-schema-confirm',
|
||||
hotkeys: ['Mod+Enter'],
|
||||
name: 'Confirm JSON schema edit',
|
||||
description: 'Confirm the current JSON schema edit',
|
||||
},
|
||||
'workflow.version-history': {
|
||||
id: 'workflow.version-history',
|
||||
hotkeys: ['Mod+Shift+H'],
|
||||
name: 'Version history',
|
||||
description: 'Open workflow version history',
|
||||
},
|
||||
}
|
||||
|
||||
export const getWorkflowShortcutDisplayHotkey = (id: WorkflowShortcutId): RegisterableHotkey | (string & {}) => {
|
||||
const shortcut = WORKFLOW_SHORTCUTS[id]
|
||||
export const getWorkflowCanvasShortcutDisplayHotkey = (id: WorkflowCanvasShortcutId): RegisterableHotkey | (string & {}) => {
|
||||
const shortcut = WORKFLOW_CANVAS_SHORTCUTS[id]
|
||||
return shortcut.displayHotkey ?? shortcut.hotkeys[0]!
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import type { KbdColor } from '@langgenius/dify-ui/kbd'
|
||||
import type { FormatDisplayOptions, RegisterableHotkey } from '@tanstack/react-hotkeys'
|
||||
import type { WorkflowShortcutId } from './definitions'
|
||||
import type { WorkflowCanvasShortcutId } from './definitions'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
|
||||
import { formatForDisplay } from '@tanstack/react-hotkeys'
|
||||
import { getWorkflowShortcutDisplayHotkey } from './definitions'
|
||||
import { getWorkflowCanvasShortcutDisplayHotkey } from './definitions'
|
||||
|
||||
type ShortcutKbdProps = {
|
||||
shortcut?: WorkflowShortcutId
|
||||
shortcut?: WorkflowCanvasShortcutId
|
||||
hotkey?: RegisterableHotkey | (string & {})
|
||||
className?: string
|
||||
textColor?: 'default' | 'secondary'
|
||||
@ -38,7 +38,7 @@ export const ShortcutKbd = ({
|
||||
bgColor = 'gray',
|
||||
platform,
|
||||
}: ShortcutKbdProps) => {
|
||||
const displayHotkey = hotkey ?? (shortcut ? getWorkflowShortcutDisplayHotkey(shortcut) : undefined)
|
||||
const displayHotkey = hotkey ?? (shortcut ? getWorkflowCanvasShortcutDisplayHotkey(shortcut) : undefined)
|
||||
|
||||
if (!displayHotkey)
|
||||
return null
|
||||
@ -52,9 +52,9 @@ export const ShortcutKbd = ({
|
||||
)}
|
||||
>
|
||||
{
|
||||
displayKeys.map((key, index) => (
|
||||
displayKeys.map(key => (
|
||||
<Kbd
|
||||
key={`${key}-${index}`}
|
||||
key={key}
|
||||
color={bgColor}
|
||||
className={cn(textColor === 'secondary' && 'text-text-tertiary')}
|
||||
>
|
||||
|
||||
@ -3,7 +3,7 @@ import type {
|
||||
UseHotkeyDefinition,
|
||||
UseHotkeyOptions,
|
||||
} from '@tanstack/react-hotkeys'
|
||||
import type { WorkflowHotkeyMeta, WorkflowShortcutDefinition, WorkflowShortcutId } from './definitions'
|
||||
import type { WorkflowCanvasHotkeyMeta, WorkflowCanvasShortcutDefinition } from './definitions'
|
||||
import { useHotkeys, useKeyHold } from '@tanstack/react-hotkeys'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
@ -19,7 +19,7 @@ import {
|
||||
subscribeWorkflowCommand,
|
||||
WorkflowCommand,
|
||||
} from './commands'
|
||||
import { WORKFLOW_SHORTCUTS } from './definitions'
|
||||
import { WORKFLOW_CANVAS_SHORTCUTS } from './definitions'
|
||||
|
||||
const workflowHotkeyOptions = {
|
||||
ignoreInputs: true,
|
||||
@ -37,7 +37,7 @@ const isInputLikeElement = (element: Element | null) => {
|
||||
}
|
||||
|
||||
const toHotkeyDefinitions = (
|
||||
shortcut: WorkflowShortcutDefinition,
|
||||
shortcut: WorkflowCanvasShortcutDefinition,
|
||||
callback: HotkeyCallback,
|
||||
options?: UseHotkeyOptions,
|
||||
): UseHotkeyDefinition[] => {
|
||||
@ -48,28 +48,14 @@ const toHotkeyDefinitions = (
|
||||
...options,
|
||||
meta: {
|
||||
id: shortcut.id,
|
||||
scope: 'workflow',
|
||||
scope: 'workflow-canvas',
|
||||
name: shortcut.name,
|
||||
description: shortcut.description,
|
||||
} satisfies WorkflowHotkeyMeta,
|
||||
} satisfies WorkflowCanvasHotkeyMeta,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export const useWorkflowShortcut = (
|
||||
id: WorkflowShortcutId,
|
||||
callback: HotkeyCallback,
|
||||
options?: UseHotkeyOptions,
|
||||
) => {
|
||||
const shortcut = WORKFLOW_SHORTCUTS[id]
|
||||
const hotkeys = useMemo(
|
||||
() => toHotkeyDefinitions(shortcut, callback, options),
|
||||
[callback, options, shortcut],
|
||||
)
|
||||
|
||||
useHotkeys(hotkeys, workflowHotkeyOptions)
|
||||
}
|
||||
|
||||
export const useWorkflowHotkeys = (): void => {
|
||||
const {
|
||||
handleNodesCopy,
|
||||
@ -139,71 +125,71 @@ export const useWorkflowHotkeys = (): void => {
|
||||
}, [handleToggleMaximizeCanvas])
|
||||
|
||||
const hotkeys = useMemo<UseHotkeyDefinition[]>(() => [
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.delete'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.delete'], () => {
|
||||
handleNodesDelete()
|
||||
handleEdgeDelete()
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.copy'], handleCopy, {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.copy'], handleCopy, {
|
||||
preventDefault: false,
|
||||
stopPropagation: false,
|
||||
enabled: !showDebugAndPreviewPanel,
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.paste'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.paste'], () => {
|
||||
handleNodesPaste()
|
||||
}, {
|
||||
enabled: !showDebugAndPreviewPanel,
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.duplicate'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.duplicate'], () => {
|
||||
handleNodesDuplicate()
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.undo'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.undo'], () => {
|
||||
handleHistoryBack()
|
||||
}, {
|
||||
enabled: !showDebugAndPreviewPanel && historyShortcutsEnabled,
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.redo'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.redo'], () => {
|
||||
handleHistoryForward()
|
||||
}, {
|
||||
enabled: !showDebugAndPreviewPanel && historyShortcutsEnabled,
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.hand-mode'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.hand-mode'], () => {
|
||||
handleModeHand()
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.pointer-mode'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.pointer-mode'], () => {
|
||||
handleModePointer()
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.comment-mode'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.comment-mode'], () => {
|
||||
handleModeComment()
|
||||
}, {
|
||||
enabled: isCommentModeAvailable,
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.organize'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.organize'], () => {
|
||||
handleLayout()
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.toggle-maximize'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.toggle-maximize'], () => {
|
||||
handleToggleMaximizeCanvas()
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.zoom-to-fit'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.zoom-to-fit'], () => {
|
||||
fitView()
|
||||
handleSyncWorkflowDraft()
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.zoom-to-100'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.zoom-to-100'], () => {
|
||||
zoomTo(1)
|
||||
handleSyncWorkflowDraft()
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.zoom-to-50'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.zoom-to-50'], () => {
|
||||
zoomTo(0.5)
|
||||
handleSyncWorkflowDraft()
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.zoom-out'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.zoom-out'], () => {
|
||||
constrainedZoomOut()
|
||||
handleSyncWorkflowDraft()
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.zoom-in'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.zoom-in'], () => {
|
||||
constrainedZoomIn()
|
||||
handleSyncWorkflowDraft()
|
||||
}),
|
||||
...toHotkeyDefinitions(WORKFLOW_SHORTCUTS['workflow.download-import-log'], () => {
|
||||
...toHotkeyDefinitions(WORKFLOW_CANVAS_SHORTCUTS['workflow.download-import-log'], () => {
|
||||
collaborationManager.downloadGraphImportLog()
|
||||
}),
|
||||
], [
|
||||
|
||||
Loading…
Reference in New Issue
Block a user