fix(skill-editor): fix blur effects of tool-picker and file-picker

This commit is contained in:
twwu
2026-03-30 16:03:31 +08:00
parent 5e4481453e
commit 9ae94b3217
8 changed files with 64 additions and 109 deletions

View File

@@ -14,6 +14,8 @@ NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api
NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api
# When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1.
NEXT_PUBLIC_COOKIE_DOMAIN=
# WebSocket server URL.
NEXT_PUBLIC_SOCKET_URL=ws://localhost:5001
# Dev-only Hono proxy targets.
# The frontend keeps requesting http://localhost:5001 directly,

View File

@@ -1,5 +1,4 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { CLEAR_HIDE_MENU_TIMEOUT } from '../plugins/workflow-variable-block'
import SandboxPlaceholder from '../sandbox-placeholder'
const mocks = vi.hoisted(() => {
@@ -100,7 +99,6 @@ describe('SandboxPlaceholder', () => {
expect(mocks.selectEnd).toHaveBeenCalledTimes(1)
expect(mocks.createTextNode).toHaveBeenCalledWith('/')
expect(mocks.insertNodes).toHaveBeenCalledWith([{ text: '/' }])
expect(mocks.editor.dispatchCommand).toHaveBeenCalledWith(CLEAR_HIDE_MENU_TIMEOUT, undefined)
})
it('should insert at-sign and clear the hide timeout when clicking tools', () => {
@@ -113,7 +111,6 @@ describe('SandboxPlaceholder', () => {
expect(mocks.selectEnd).toHaveBeenCalledTimes(1)
expect(mocks.createTextNode).toHaveBeenCalledWith('@')
expect(mocks.insertNodes).toHaveBeenCalledWith([{ text: '@' }])
expect(mocks.editor.dispatchCommand).toHaveBeenCalledWith(CLEAR_HIDE_MENU_TIMEOUT, undefined)
})
it('should not trigger editor insertion when placeholder is not editable', () => {

View File

@@ -39,9 +39,9 @@ import {
Fragment,
memo,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useEffect,
useRef,
useState,
} from 'react'

View File

@@ -5,7 +5,6 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
import { $createCustomTextNode } from './plugins/custom-text/node'
import { CLEAR_HIDE_MENU_TIMEOUT } from './plugins/workflow-variable-block'
type SandboxPlaceholderTokenProps = {
actionLabel?: string
@@ -74,7 +73,6 @@ const SandboxPlaceholder: FC<SandboxPlaceholderProps> = ({
editor.update(() => {
$getRoot().selectEnd()
$insertNodes([$createCustomTextNode(trigger)])
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
})
})
}, [editor])

View File

@@ -14,6 +14,7 @@ import {
} from '@/app/components/base/ui/popover'
import { FilePickerPanel } from './file-picker-panel'
import { $createFileReferenceNode } from './file-reference-block/node'
import { useEditorBlur } from './hooks/use-editor-blur'
class FilePickerMenuOption extends MenuOption {
constructor() {
@@ -30,6 +31,8 @@ const FilePickerBlock = () => {
const options = useMemo(() => [new FilePickerMenuOption()], [])
const { blurHidden } = useEditorBlur(editor)
const insertFileReference = useCallback((resourceId: string) => {
editor.update(() => {
const match = checkForTriggerMatch('/', editor)
@@ -46,6 +49,8 @@ const FilePickerBlock = () => {
anchorElementRef: React.RefObject<HTMLElement | null>,
{ selectOptionAndCleanUp }: { selectOptionAndCleanUp: (option: MenuOption) => void },
) => {
if (blurHidden)
return null
if (!anchorElementRef.current)
return null
@@ -75,7 +80,7 @@ const FilePickerBlock = () => {
</PopoverContent>
</Popover>
)
}, [insertFileReference, options])
}, [blurHidden, insertFileReference, options])
return (
<LexicalTypeaheadMenuPlugin

View File

@@ -0,0 +1,48 @@
import type { LexicalEditor } from 'lexical'
import { BLUR_COMMAND, COMMAND_PRIORITY_EDITOR, FOCUS_COMMAND, mergeRegister } from 'lexical'
import { useCallback, useEffect, useRef, useState } from 'react'
export const useEditorBlur = (editor: LexicalEditor) => {
const [blurHidden, setBlurHidden] = useState(false)
const blurTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const clearBlurTimer = useCallback(() => {
if (blurTimerRef.current) {
clearTimeout(blurTimerRef.current)
blurTimerRef.current = null
}
}, [])
useEffect(() => {
const unregister = mergeRegister(
editor.registerCommand(
BLUR_COMMAND,
() => {
clearBlurTimer()
blurTimerRef.current = setTimeout(() => setBlurHidden(true), 200)
return false
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
FOCUS_COMMAND,
() => {
clearBlurTimer()
setBlurHidden(false)
return false
},
COMMAND_PRIORITY_EDITOR,
),
)
return () => {
if (blurTimerRef.current)
clearTimeout(blurTimerRef.current)
unregister()
}
}, [editor, clearBlurTimer])
return {
blurHidden,
}
}

View File

@@ -21,6 +21,7 @@ import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-for
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import { START_TAB_ID } from '@/app/components/workflow/skill/constants'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { useEditorBlur } from '../hooks/use-editor-blur'
import { $createToolBlockNode } from './node'
import { useToolBlockContext } from './tool-block-context'
import { $createToolGroupBlockNode } from './tool-group-block-node'
@@ -52,6 +53,8 @@ const ToolPickerBlock = ({ scope = 'all', enableAutoDefault = false }: ToolPicke
const isUsingExternalMetadata = Boolean(onMetadataChange)
const [queryString, setQueryString] = useState('')
const { blurHidden } = useEditorBlur(editor)
const canUseAutoByType = useCallback(
(type: string) => ![FormTypeEnum.modelSelector, FormTypeEnum.appSelector].includes(type as FormTypeEnum),
[],
@@ -159,6 +162,8 @@ const ToolPickerBlock = ({ scope = 'all', enableAutoDefault = false }: ToolPicke
anchorElementRef: React.RefObject<HTMLElement | null>,
{ selectOptionAndCleanUp }: { selectOptionAndCleanUp: (option: MenuOption) => void },
) => {
if (blurHidden)
return null
if (!anchorElementRef.current)
return null
@@ -199,7 +204,7 @@ const ToolPickerBlock = ({ scope = 'all', enableAutoDefault = false }: ToolPicke
/>,
anchorElementRef.current,
)
}, [insertTools, options, queryString, scope])
}, [blurHidden, insertTools, options, queryString, scope])
return (
<LexicalTypeaheadMenuPlugin

View File

@@ -148,20 +148,11 @@
"erasable-syntax-only/enums": {
"count": 1
},
"style/indent": {
"count": 175
},
"ts/no-explicit-any": {
"count": 5
},
"unused-imports/no-unused-imports": {
"count": 1
}
},
"app/account/(commonLayout)/account-page/index.tsx": {
"style/indent": {
"count": 93
},
"ts/no-explicit-any": {
"count": 1
}
@@ -369,9 +360,6 @@
},
"app/components/app/app-publisher/index.tsx": {
"no-restricted-imports": {
"count": 2
},
"ts/no-explicit-any": {
"count": 1
}
},
@@ -391,9 +379,6 @@
}
},
"app/components/app/configuration/config-prompt/advanced-prompt-input.tsx": {
"perfectionist/sort-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 2
}
@@ -412,19 +397,13 @@
}
},
"app/components/app/configuration/config-prompt/simple-prompt-input.tsx": {
"perfectionist/sort-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 3
}
},
"app/components/app/configuration/config-var/config-modal/index.tsx": {
"no-restricted-imports": {
"count": 2
},
"ts/no-explicit-any": {
"count": 6
"count": 1
}
},
"app/components/app/configuration/config-var/config-modal/type-select.tsx": {
@@ -435,9 +414,6 @@
"app/components/app/configuration/config-var/index.tsx": {
"no-restricted-imports": {
"count": 2
},
"react-refresh/only-export-components": {
"count": 1
}
},
"app/components/app/configuration/config-var/select-var-type.tsx": {
@@ -639,9 +615,6 @@
}
},
"app/components/app/configuration/dataset-config/settings-modal/index.tsx": {
"perfectionist/sort-imports": {
"count": 1
},
"react/set-state-in-effect": {
"count": 2
},
@@ -708,9 +681,6 @@
}
},
"app/components/app/configuration/debug/index.tsx": {
"perfectionist/sort-imports": {
"count": 1
},
"react/set-state-in-effect": {
"count": 2
},
@@ -723,26 +693,6 @@
"count": 1
}
},
"app/components/app/configuration/index.tsx": {
"no-restricted-imports": {
"count": 1
},
"perfectionist/sort-imports": {
"count": 1
},
"react/set-state-in-effect": {
"count": 1
},
"style/multiline-ternary": {
"count": 2
},
"ts/no-explicit-any": {
"count": 24
},
"unused-imports/no-unused-imports": {
"count": 1
}
},
"app/components/app/configuration/prompt-value-panel/index.spec.tsx": {
"ts/no-explicit-any": {
"count": 2
@@ -758,19 +708,6 @@
"count": 1
}
},
"app/components/app/configuration/tools/external-data-tool-modal.tsx": {
"perfectionist/sort-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 2
}
},
"app/components/app/configuration/tools/index.tsx": {
"perfectionist/sort-imports": {
"count": 1
}
},
"app/components/app/create-app-dialog/app-card/index.spec.tsx": {
"ts/no-explicit-any": {
"count": 1
@@ -798,17 +735,8 @@
"erasable-syntax-only/enums": {
"count": 1
},
"perfectionist/sort-imports": {
"count": 2
},
"react-refresh/only-export-components": {
"count": 1
},
"react/set-state-in-effect": {
"count": 2
},
"unused-imports/no-unused-imports": {
"count": 1
}
},
"app/components/app/duplicate-modal/index.tsx": {
@@ -830,9 +758,6 @@
"no-restricted-imports": {
"count": 1
},
"perfectionist/sort-imports": {
"count": 1
},
"react/set-state-in-effect": {
"count": 6
},
@@ -889,9 +814,6 @@
"no-restricted-imports": {
"count": 3
},
"perfectionist/sort-imports": {
"count": 1
},
"react/set-state-in-effect": {
"count": 3
},
@@ -908,25 +830,13 @@
"no-restricted-imports": {
"count": 2
},
"perfectionist/sort-imports": {
"count": 1
},
"react/set-state-in-effect": {
"count": 1
},
"unused-imports/no-unused-imports": {
"count": 1
}
},
"app/components/app/text-generate/item/index.tsx": {
"react-refresh/only-export-components": {
"count": 1
},
"react/set-state-in-effect": {
"count": 3
},
"ts/no-explicit-any": {
"count": 3
}
},
"app/components/app/text-generate/item/result-tab.tsx": {
@@ -964,11 +874,6 @@
"count": 1
}
},
"app/components/apps/app-card.tsx": {
"perfectionist/sort-imports": {
"count": 2
}
},
"app/components/apps/list.tsx": {
"react-hooks/exhaustive-deps": {
"count": 1
@@ -4022,11 +3927,6 @@
"count": 1
}
},
"app/components/tools/workflow-tool/index.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"app/components/tools/workflow-tool/method-selector.tsx": {
"no-restricted-imports": {
"count": 1