= ({
/>
{t('generate.press', { ns: 'appDebug' })}
- /
+ /
{t('generate.to', { ns: 'appDebug' })}
diff --git a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx
index c6be430fac..d9aa8e957a 100644
--- a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx
+++ b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx
@@ -17,8 +17,8 @@ const toastMocks = vi.hoisted(() => ({
error: vi.fn(),
warning: vi.fn(),
}))
-const ahooksMocks = vi.hoisted(() => ({
- handlers: [] as Array<{ keys: unknown, handler: () => void }>,
+const hotkeyMocks = vi.hoisted(() => ({
+ handlers: new Map void, options?: { enabled?: boolean } }>(),
}))
let mockPlanUsage = 0
let mockPlanTotal = 10
@@ -33,11 +33,25 @@ vi.mock('ahooks', () => ({
useDebounceFn: (fn: (...args: any[]) => any) => ({
run: fn,
}),
- useKeyPress: (keys: unknown, handler: () => void) => {
- ahooksMocks.handlers.push({ keys, handler })
- },
}))
+vi.mock('@tanstack/react-hotkeys', async (importOriginal) => {
+ const actual = await importOriginal()
+ return {
+ ...actual,
+ useHotkey: (hotkey: string, handler: () => void, options?: { enabled?: boolean }) => {
+ hotkeyMocks.handlers.set(hotkey, { handler, options })
+ },
+ }
+})
+
+const triggerHotkey = (hotkey: string) => {
+ const registration = hotkeyMocks.handlers.get(hotkey)
+ if (registration?.options?.enabled === false)
+ return
+ registration?.handler()
+}
+
vi.mock('@/next/navigation', () => ({
useRouter: () => ({
push: mockPush,
@@ -98,14 +112,10 @@ vi.mock('@/app/components/billing/apps-full-in-dialog', () => ({
default: () => apps-full
,
}))
-vi.mock('../../workflow/shortcuts-name', () => ({
- default: ({ keys }: { keys: string[] }) => {keys.join('+')},
-}))
-
describe('CreateFromDSLModal', () => {
beforeEach(() => {
vi.clearAllMocks()
- ahooksMocks.handlers.length = 0
+ hotkeyMocks.handlers.clear()
mockPlanUsage = 0
mockPlanTotal = 10
localStorage.clear()
@@ -153,7 +163,7 @@ describe('CreateFromDSLModal', () => {
/>,
)
- ahooksMocks.handlers.find(item => Array.isArray(item.keys))?.handler()
+ triggerHotkey('Mod+Enter')
expect(mockImportDSL).not.toHaveBeenCalled()
await act(async () => {
@@ -166,6 +176,20 @@ describe('CreateFromDSLModal', () => {
expect(handleClose).toHaveBeenCalledTimes(1)
})
+ it('should render the import shortcut with kbd primitives', () => {
+ render(
+ ,
+ )
+
+ const createButton = getCreateButton()
+ expect(createButton.querySelectorAll('kbd')).toHaveLength(2)
+ })
+
it('should import from a URL and redirect after a successful import', async () => {
const handleClose = vi.fn()
const handleSuccess = vi.fn()
@@ -258,8 +282,7 @@ describe('CreateFromDSLModal', () => {
expect(getCreateButton())!.toBeDisabled()
})
- const latestHandlerAfterRemove = [...ahooksMocks.handlers].reverse().find(item => Array.isArray(item.keys))
- latestHandlerAfterRemove?.handler()
+ triggerHotkey('Mod+Enter')
expect(mockImportDSL).not.toHaveBeenCalled()
})
@@ -418,7 +441,7 @@ describe('CreateFromDSLModal', () => {
expect(toastMocks.error).not.toHaveBeenCalled()
})
- it('should handle keyboard shortcuts, quota guard, and escape close', async () => {
+ it('should handle keyboard shortcut and quota guard', async () => {
const handleClose = vi.fn()
mockImportDSL.mockResolvedValue({
id: 'import-shortcut',
@@ -436,7 +459,7 @@ describe('CreateFromDSLModal', () => {
/>,
)
- ahooksMocks.handlers.find(item => Array.isArray(item.keys))?.handler()
+ triggerHotkey('Mod+Enter')
await waitFor(() => {
expect(mockImportDSL).toHaveBeenCalledWith({
@@ -445,9 +468,6 @@ describe('CreateFromDSLModal', () => {
})
})
- ahooksMocks.handlers.find(item => item.keys === 'esc')?.handler()
- expect(handleClose).toHaveBeenCalled()
-
mockPlanUsage = 1
mockPlanTotal = 1
render(
@@ -460,8 +480,7 @@ describe('CreateFromDSLModal', () => {
)
expect(screen.getByText('apps-full'))!.toBeInTheDocument()
- const latestPlanLimitHandler = [...ahooksMocks.handlers].reverse().find(item => Array.isArray(item.keys))
- latestPlanLimitHandler?.handler()
+ triggerHotkey('Mod+Enter')
expect(mockImportDSL).toHaveBeenCalledTimes(1)
})
diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx
index 6e4c9ec366..f6cf347820 100644
--- a/web/app/components/app/create-from-dsl-modal/index.tsx
+++ b/web/app/components/app/create-from-dsl-modal/index.tsx
@@ -4,8 +4,10 @@ import type { MouseEventHandler } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
+import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
import { toast } from '@langgenius/dify-ui/toast'
-import { useDebounceFn, useKeyPress } from 'ahooks'
+import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys'
+import { useDebounceFn } from 'ahooks'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
@@ -25,7 +27,6 @@ import {
} from '@/service/apps'
import { getRedirection } from '@/utils/app-redirection'
import { trackCreateApp } from '@/utils/create-app-tracking'
-import ShortcutsName from '../../workflow/shortcuts-name'
import Uploader from './uploader'
type CreateFromDSLModalProps = {
@@ -150,14 +151,11 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 })
- useKeyPress(['meta.enter', 'ctrl.enter'], () => {
- if (show && !isAppsFull && ((currentTab === CreateFromDSLModalTab.FROM_FILE && currentFile) || (currentTab === CreateFromDSLModalTab.FROM_URL && dslUrlValue)))
- handleCreateApp(undefined)
- })
-
- useKeyPress('esc', () => {
- if (show && !showErrorModal)
- onClose()
+ useHotkey('Mod+Enter', () => {
+ handleCreateApp(undefined)
+ }, {
+ enabled: show && !isAppsFull && ((currentTab === CreateFromDSLModalTab.FROM_FILE && !!currentFile) || (currentTab === CreateFromDSLModalTab.FROM_URL && !!dslUrlValue)),
+ ignoreInputs: false,
})
const onDSLConfirm: MouseEventHandler = async () => {
@@ -215,7 +213,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
return (
<>
-