mirror of
https://github.com/langgenius/dify.git
synced 2026-04-05 20:09:20 +08:00
fix(prompt-editor): fix unexpected blur effect in prompt editor (#34069)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -3,13 +3,10 @@ import { LexicalComposer } from '@lexical/react/LexicalComposer'
|
|||||||
import { act, render, waitFor } from '@testing-library/react'
|
import { act, render, waitFor } from '@testing-library/react'
|
||||||
import {
|
import {
|
||||||
BLUR_COMMAND,
|
BLUR_COMMAND,
|
||||||
COMMAND_PRIORITY_EDITOR,
|
|
||||||
FOCUS_COMMAND,
|
FOCUS_COMMAND,
|
||||||
KEY_ESCAPE_COMMAND,
|
|
||||||
} from 'lexical'
|
} from 'lexical'
|
||||||
import OnBlurBlock from '../on-blur-or-focus-block'
|
import OnBlurBlock from '../on-blur-or-focus-block'
|
||||||
import { CaptureEditorPlugin } from '../test-utils'
|
import { CaptureEditorPlugin } from '../test-utils'
|
||||||
import { CLEAR_HIDE_MENU_TIMEOUT } from '../workflow-variable-block'
|
|
||||||
|
|
||||||
const renderOnBlurBlock = (props?: {
|
const renderOnBlurBlock = (props?: {
|
||||||
onBlur?: () => void
|
onBlur?: () => void
|
||||||
@@ -75,7 +72,7 @@ describe('OnBlurBlock', () => {
|
|||||||
expect(onFocus).toHaveBeenCalledTimes(1)
|
expect(onFocus).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should call onBlur and dispatch escape after delay when blur target is not var-search-input', async () => {
|
it('should call onBlur when blur target is not var-search-input', async () => {
|
||||||
const onBlur = vi.fn()
|
const onBlur = vi.fn()
|
||||||
const { getEditor } = renderOnBlurBlock({ onBlur })
|
const { getEditor } = renderOnBlurBlock({ onBlur })
|
||||||
|
|
||||||
@@ -85,14 +82,6 @@ describe('OnBlurBlock', () => {
|
|||||||
|
|
||||||
const editor = getEditor()
|
const editor = getEditor()
|
||||||
expect(editor).not.toBeNull()
|
expect(editor).not.toBeNull()
|
||||||
vi.useFakeTimers()
|
|
||||||
|
|
||||||
const onEscape = vi.fn(() => true)
|
|
||||||
const unregister = editor!.registerCommand(
|
|
||||||
KEY_ESCAPE_COMMAND,
|
|
||||||
onEscape,
|
|
||||||
COMMAND_PRIORITY_EDITOR,
|
|
||||||
)
|
|
||||||
|
|
||||||
let handled = false
|
let handled = false
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -101,18 +90,9 @@ describe('OnBlurBlock', () => {
|
|||||||
|
|
||||||
expect(handled).toBe(true)
|
expect(handled).toBe(true)
|
||||||
expect(onBlur).toHaveBeenCalledTimes(1)
|
expect(onBlur).toHaveBeenCalledTimes(1)
|
||||||
expect(onEscape).not.toHaveBeenCalled()
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
vi.advanceTimersByTime(200)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(onEscape).toHaveBeenCalledTimes(1)
|
it('should handle blur when onBlur callback is not provided', async () => {
|
||||||
unregister()
|
|
||||||
vi.useRealTimers()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should dispatch delayed escape when onBlur callback is not provided', async () => {
|
|
||||||
const { getEditor } = renderOnBlurBlock()
|
const { getEditor } = renderOnBlurBlock()
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -121,28 +101,16 @@ describe('OnBlurBlock', () => {
|
|||||||
|
|
||||||
const editor = getEditor()
|
const editor = getEditor()
|
||||||
expect(editor).not.toBeNull()
|
expect(editor).not.toBeNull()
|
||||||
vi.useFakeTimers()
|
|
||||||
|
|
||||||
const onEscape = vi.fn(() => true)
|
|
||||||
const unregister = editor!.registerCommand(
|
|
||||||
KEY_ESCAPE_COMMAND,
|
|
||||||
onEscape,
|
|
||||||
COMMAND_PRIORITY_EDITOR,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
let handled = false
|
||||||
act(() => {
|
act(() => {
|
||||||
editor!.dispatchCommand(BLUR_COMMAND, createBlurEvent(document.createElement('div')))
|
handled = editor!.dispatchCommand(BLUR_COMMAND, createBlurEvent(document.createElement('div')))
|
||||||
})
|
|
||||||
act(() => {
|
|
||||||
vi.advanceTimersByTime(200)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(onEscape).toHaveBeenCalledTimes(1)
|
expect(handled).toBe(true)
|
||||||
unregister()
|
|
||||||
vi.useRealTimers()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should skip onBlur and delayed escape when blur target is var-search-input', async () => {
|
it('should skip onBlur when blur target is var-search-input', async () => {
|
||||||
const onBlur = vi.fn()
|
const onBlur = vi.fn()
|
||||||
const { getEditor } = renderOnBlurBlock({ onBlur })
|
const { getEditor } = renderOnBlurBlock({ onBlur })
|
||||||
|
|
||||||
@@ -152,31 +120,17 @@ describe('OnBlurBlock', () => {
|
|||||||
|
|
||||||
const editor = getEditor()
|
const editor = getEditor()
|
||||||
expect(editor).not.toBeNull()
|
expect(editor).not.toBeNull()
|
||||||
vi.useFakeTimers()
|
|
||||||
|
|
||||||
const target = document.createElement('input')
|
const target = document.createElement('input')
|
||||||
target.classList.add('var-search-input')
|
target.classList.add('var-search-input')
|
||||||
|
|
||||||
const onEscape = vi.fn(() => true)
|
|
||||||
const unregister = editor!.registerCommand(
|
|
||||||
KEY_ESCAPE_COMMAND,
|
|
||||||
onEscape,
|
|
||||||
COMMAND_PRIORITY_EDITOR,
|
|
||||||
)
|
|
||||||
|
|
||||||
let handled = false
|
let handled = false
|
||||||
act(() => {
|
act(() => {
|
||||||
handled = editor!.dispatchCommand(BLUR_COMMAND, createBlurEvent(target))
|
handled = editor!.dispatchCommand(BLUR_COMMAND, createBlurEvent(target))
|
||||||
})
|
})
|
||||||
act(() => {
|
|
||||||
vi.advanceTimersByTime(200)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(handled).toBe(true)
|
expect(handled).toBe(true)
|
||||||
expect(onBlur).not.toHaveBeenCalled()
|
expect(onBlur).not.toHaveBeenCalled()
|
||||||
expect(onEscape).not.toHaveBeenCalled()
|
|
||||||
unregister()
|
|
||||||
vi.useRealTimers()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle focus command when onFocus callback is not provided', async () => {
|
it('should handle focus command when onFocus callback is not provided', async () => {
|
||||||
@@ -198,59 +152,6 @@ describe('OnBlurBlock', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Clear timeout command', () => {
|
|
||||||
it('should clear scheduled escape timeout when clear command is dispatched', async () => {
|
|
||||||
const { getEditor } = renderOnBlurBlock({ onBlur: vi.fn() })
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(getEditor()).not.toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
const editor = getEditor()
|
|
||||||
expect(editor).not.toBeNull()
|
|
||||||
vi.useFakeTimers()
|
|
||||||
|
|
||||||
const onEscape = vi.fn(() => true)
|
|
||||||
const unregister = editor!.registerCommand(
|
|
||||||
KEY_ESCAPE_COMMAND,
|
|
||||||
onEscape,
|
|
||||||
COMMAND_PRIORITY_EDITOR,
|
|
||||||
)
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
editor!.dispatchCommand(BLUR_COMMAND, createBlurEvent(document.createElement('div')))
|
|
||||||
})
|
|
||||||
act(() => {
|
|
||||||
editor!.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
|
|
||||||
})
|
|
||||||
act(() => {
|
|
||||||
vi.advanceTimersByTime(200)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(onEscape).not.toHaveBeenCalled()
|
|
||||||
unregister()
|
|
||||||
vi.useRealTimers()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle clear command when no timeout is scheduled', async () => {
|
|
||||||
const { getEditor } = renderOnBlurBlock()
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(getEditor()).not.toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
const editor = getEditor()
|
|
||||||
expect(editor).not.toBeNull()
|
|
||||||
|
|
||||||
let handled = false
|
|
||||||
act(() => {
|
|
||||||
handled = editor!.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(handled).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Lifecycle cleanup', () => {
|
describe('Lifecycle cleanup', () => {
|
||||||
it('should unregister commands when component unmounts', async () => {
|
it('should unregister commands when component unmounts', async () => {
|
||||||
const { getEditor, unmount } = renderOnBlurBlock()
|
const { getEditor, unmount } = renderOnBlurBlock()
|
||||||
@@ -266,16 +167,13 @@ describe('OnBlurBlock', () => {
|
|||||||
|
|
||||||
let blurHandled = true
|
let blurHandled = true
|
||||||
let focusHandled = true
|
let focusHandled = true
|
||||||
let clearHandled = true
|
|
||||||
act(() => {
|
act(() => {
|
||||||
blurHandled = editor!.dispatchCommand(BLUR_COMMAND, createBlurEvent(document.createElement('div')))
|
blurHandled = editor!.dispatchCommand(BLUR_COMMAND, createBlurEvent(document.createElement('div')))
|
||||||
focusHandled = editor!.dispatchCommand(FOCUS_COMMAND, createFocusEvent())
|
focusHandled = editor!.dispatchCommand(FOCUS_COMMAND, createFocusEvent())
|
||||||
clearHandled = editor!.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(blurHandled).toBe(false)
|
expect(blurHandled).toBe(false)
|
||||||
expect(focusHandled).toBe(false)
|
expect(focusHandled).toBe(false)
|
||||||
expect(clearHandled).toBe(false)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import type { LexicalEditor } from 'lexical'
|
import type { LexicalEditor } from 'lexical'
|
||||||
import { LexicalComposer } from '@lexical/react/LexicalComposer'
|
import { LexicalComposer } from '@lexical/react/LexicalComposer'
|
||||||
import { act, render, waitFor } from '@testing-library/react'
|
import { act, render, waitFor } from '@testing-library/react'
|
||||||
import { $getRoot, COMMAND_PRIORITY_EDITOR } from 'lexical'
|
import { $getRoot } from 'lexical'
|
||||||
import { CustomTextNode } from '../custom-text/node'
|
import { CustomTextNode } from '../custom-text/node'
|
||||||
import { CaptureEditorPlugin } from '../test-utils'
|
import { CaptureEditorPlugin } from '../test-utils'
|
||||||
import UpdateBlock, {
|
import UpdateBlock, {
|
||||||
PROMPT_EDITOR_INSERT_QUICKLY,
|
PROMPT_EDITOR_INSERT_QUICKLY,
|
||||||
PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER,
|
PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER,
|
||||||
} from '../update-block'
|
} from '../update-block'
|
||||||
import { CLEAR_HIDE_MENU_TIMEOUT } from '../workflow-variable-block'
|
|
||||||
|
|
||||||
const { mockUseEventEmitterContextContext } = vi.hoisted(() => ({
|
const { mockUseEventEmitterContextContext } = vi.hoisted(() => ({
|
||||||
mockUseEventEmitterContextContext: vi.fn(),
|
mockUseEventEmitterContextContext: vi.fn(),
|
||||||
@@ -157,7 +156,7 @@ describe('UpdateBlock', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Quick insert event', () => {
|
describe('Quick insert event', () => {
|
||||||
it('should insert slash and dispatch clear command when quick insert event matches instance id', async () => {
|
it('should insert slash when quick insert event matches instance id', async () => {
|
||||||
const { emit, getEditor } = setup({ instanceId: 'instance-1' })
|
const { emit, getEditor } = setup({ instanceId: 'instance-1' })
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -168,13 +167,6 @@ describe('UpdateBlock', () => {
|
|||||||
|
|
||||||
selectRootEnd(editor!)
|
selectRootEnd(editor!)
|
||||||
|
|
||||||
const clearCommandHandler = vi.fn(() => true)
|
|
||||||
const unregister = editor!.registerCommand(
|
|
||||||
CLEAR_HIDE_MENU_TIMEOUT,
|
|
||||||
clearCommandHandler,
|
|
||||||
COMMAND_PRIORITY_EDITOR,
|
|
||||||
)
|
|
||||||
|
|
||||||
emit({
|
emit({
|
||||||
type: PROMPT_EDITOR_INSERT_QUICKLY,
|
type: PROMPT_EDITOR_INSERT_QUICKLY,
|
||||||
instanceId: 'instance-1',
|
instanceId: 'instance-1',
|
||||||
@@ -183,9 +175,6 @@ describe('UpdateBlock', () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(readEditorText(editor!)).toBe('/')
|
expect(readEditorText(editor!)).toBe('/')
|
||||||
})
|
})
|
||||||
expect(clearCommandHandler).toHaveBeenCalledTimes(1)
|
|
||||||
|
|
||||||
unregister()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should ignore quick insert event when instance id does not match', async () => {
|
it('should ignore quick insert event when instance id does not match', async () => {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import {
|
|||||||
$createTextNode,
|
$createTextNode,
|
||||||
$getRoot,
|
$getRoot,
|
||||||
$setSelection,
|
$setSelection,
|
||||||
|
BLUR_COMMAND,
|
||||||
|
FOCUS_COMMAND,
|
||||||
KEY_ESCAPE_COMMAND,
|
KEY_ESCAPE_COMMAND,
|
||||||
} from 'lexical'
|
} from 'lexical'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
@@ -631,4 +633,180 @@ describe('ComponentPicker (component-picker-block/index.tsx)', () => {
|
|||||||
// With a single option group, the only divider should be the workflow-var/options separator.
|
// With a single option group, the only divider should be the workflow-var/options separator.
|
||||||
expect(document.querySelectorAll('.bg-divider-subtle')).toHaveLength(1)
|
expect(document.querySelectorAll('.bg-divider-subtle')).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('blur/focus menu visibility', () => {
|
||||||
|
it('hides the menu after a 200ms delay when blur command is dispatched', async () => {
|
||||||
|
const captures: Captures = { editor: null, eventEmitter: null }
|
||||||
|
|
||||||
|
render((
|
||||||
|
<MinimalEditor
|
||||||
|
triggerString="{"
|
||||||
|
contextBlock={makeContextBlock()}
|
||||||
|
captures={captures}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
const editor = await waitForEditor(captures)
|
||||||
|
await setEditorText(editor, '{', true)
|
||||||
|
expect(await screen.findByText('common.promptEditor.context.item.title')).toBeInTheDocument()
|
||||||
|
|
||||||
|
vi.useFakeTimers()
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
editor.dispatchCommand(BLUR_COMMAND, new FocusEvent('blur', { relatedTarget: document.createElement('button') }))
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.queryByText('common.promptEditor.context.item.title')).toBeInTheDocument()
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
vi.advanceTimersByTime(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.queryByText('common.promptEditor.context.item.title')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('restores menu visibility when focus command is dispatched after blur hides it', async () => {
|
||||||
|
const captures: Captures = { editor: null, eventEmitter: null }
|
||||||
|
|
||||||
|
render((
|
||||||
|
<MinimalEditor
|
||||||
|
triggerString="{"
|
||||||
|
contextBlock={makeContextBlock()}
|
||||||
|
captures={captures}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
const editor = await waitForEditor(captures)
|
||||||
|
await setEditorText(editor, '{', true)
|
||||||
|
expect(await screen.findByText('common.promptEditor.context.item.title')).toBeInTheDocument()
|
||||||
|
|
||||||
|
vi.useFakeTimers()
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
editor.dispatchCommand(BLUR_COMMAND, new FocusEvent('blur', { relatedTarget: document.createElement('button') }))
|
||||||
|
})
|
||||||
|
act(() => {
|
||||||
|
vi.advanceTimersByTime(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.queryByText('common.promptEditor.context.item.title')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
editor.dispatchCommand(FOCUS_COMMAND, new FocusEvent('focus'))
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.useRealTimers()
|
||||||
|
|
||||||
|
await setEditorText(editor, '{', true)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('common.promptEditor.context.item.title')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancels the blur timer when focus arrives before the 200ms timeout', async () => {
|
||||||
|
const captures: Captures = { editor: null, eventEmitter: null }
|
||||||
|
|
||||||
|
render((
|
||||||
|
<MinimalEditor
|
||||||
|
triggerString="{"
|
||||||
|
contextBlock={makeContextBlock()}
|
||||||
|
captures={captures}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
const editor = await waitForEditor(captures)
|
||||||
|
await setEditorText(editor, '{', true)
|
||||||
|
expect(await screen.findByText('common.promptEditor.context.item.title')).toBeInTheDocument()
|
||||||
|
|
||||||
|
vi.useFakeTimers()
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
editor.dispatchCommand(BLUR_COMMAND, new FocusEvent('blur', { relatedTarget: document.createElement('button') }))
|
||||||
|
})
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
editor.dispatchCommand(FOCUS_COMMAND, new FocusEvent('focus'))
|
||||||
|
})
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
vi.advanceTimersByTime(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.queryByText('common.promptEditor.context.item.title')).toBeInTheDocument()
|
||||||
|
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancels a pending blur timer when a subsequent blur targets var-search-input', async () => {
|
||||||
|
const captures: Captures = { editor: null, eventEmitter: null }
|
||||||
|
|
||||||
|
render((
|
||||||
|
<MinimalEditor
|
||||||
|
triggerString="{"
|
||||||
|
contextBlock={makeContextBlock()}
|
||||||
|
captures={captures}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
const editor = await waitForEditor(captures)
|
||||||
|
await setEditorText(editor, '{', true)
|
||||||
|
expect(await screen.findByText('common.promptEditor.context.item.title')).toBeInTheDocument()
|
||||||
|
|
||||||
|
vi.useFakeTimers()
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
editor.dispatchCommand(BLUR_COMMAND, new FocusEvent('blur', { relatedTarget: document.createElement('button') }))
|
||||||
|
})
|
||||||
|
|
||||||
|
const varInput = document.createElement('input')
|
||||||
|
varInput.classList.add('var-search-input')
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
editor.dispatchCommand(BLUR_COMMAND, new FocusEvent('blur', { relatedTarget: varInput }))
|
||||||
|
})
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
vi.advanceTimersByTime(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.queryByText('common.promptEditor.context.item.title')).toBeInTheDocument()
|
||||||
|
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not hide the menu when blur target is var-search-input', async () => {
|
||||||
|
const captures: Captures = { editor: null, eventEmitter: null }
|
||||||
|
|
||||||
|
render((
|
||||||
|
<MinimalEditor
|
||||||
|
triggerString="{"
|
||||||
|
contextBlock={makeContextBlock()}
|
||||||
|
captures={captures}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
const editor = await waitForEditor(captures)
|
||||||
|
await setEditorText(editor, '{', true)
|
||||||
|
expect(await screen.findByText('common.promptEditor.context.item.title')).toBeInTheDocument()
|
||||||
|
|
||||||
|
vi.useFakeTimers()
|
||||||
|
|
||||||
|
const target = document.createElement('input')
|
||||||
|
target.classList.add('var-search-input')
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
editor.dispatchCommand(BLUR_COMMAND, new FocusEvent('blur', { relatedTarget: target }))
|
||||||
|
})
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
vi.advanceTimersByTime(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.queryByText('common.promptEditor.context.item.title')).toBeInTheDocument()
|
||||||
|
|
||||||
|
vi.useRealTimers()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,11 +21,19 @@ import {
|
|||||||
} from '@floating-ui/react'
|
} from '@floating-ui/react'
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||||
import { KEY_ESCAPE_COMMAND } from 'lexical'
|
import { mergeRegister } from '@lexical/utils'
|
||||||
|
import {
|
||||||
|
BLUR_COMMAND,
|
||||||
|
COMMAND_PRIORITY_EDITOR,
|
||||||
|
FOCUS_COMMAND,
|
||||||
|
KEY_ESCAPE_COMMAND,
|
||||||
|
} from 'lexical'
|
||||||
import {
|
import {
|
||||||
Fragment,
|
Fragment,
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
@@ -87,6 +95,46 @@ const ComponentPicker = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [queryString, setQueryString] = useState<string | null>(null)
|
const [queryString, setQueryString] = useState<string | null>(null)
|
||||||
|
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,
|
||||||
|
(event) => {
|
||||||
|
clearBlurTimer()
|
||||||
|
const target = event?.relatedTarget as HTMLElement
|
||||||
|
if (!target?.classList?.contains('var-search-input'))
|
||||||
|
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])
|
||||||
|
|
||||||
eventEmitter?.useSubscription((v: any) => {
|
eventEmitter?.useSubscription((v: any) => {
|
||||||
if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND)
|
if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND)
|
||||||
@@ -159,6 +207,8 @@ const ComponentPicker = ({
|
|||||||
anchorElementRef,
|
anchorElementRef,
|
||||||
{ options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
|
{ options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
|
||||||
) => {
|
) => {
|
||||||
|
if (blurHidden)
|
||||||
|
return null
|
||||||
if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show)))
|
if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show)))
|
||||||
return null
|
return null
|
||||||
|
|
||||||
@@ -240,7 +290,7 @@ const ComponentPicker = ({
|
|||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}, [allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, workflowVariableOptions, isSupportFileVar, handleClose, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField])
|
}, [blurHidden, allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, workflowVariableOptions, isSupportFileVar, handleClose, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LexicalTypeaheadMenuPlugin
|
<LexicalTypeaheadMenuPlugin
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import {
|
|||||||
BLUR_COMMAND,
|
BLUR_COMMAND,
|
||||||
COMMAND_PRIORITY_EDITOR,
|
COMMAND_PRIORITY_EDITOR,
|
||||||
FOCUS_COMMAND,
|
FOCUS_COMMAND,
|
||||||
KEY_ESCAPE_COMMAND,
|
|
||||||
} from 'lexical'
|
} from 'lexical'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block'
|
|
||||||
|
|
||||||
type OnBlurBlockProps = {
|
type OnBlurBlockProps = {
|
||||||
onBlur?: () => void
|
onBlur?: () => void
|
||||||
@@ -20,35 +18,13 @@ const OnBlurBlock: FC<OnBlurBlockProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
const ref = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const clearHideMenuTimeout = () => {
|
return mergeRegister(
|
||||||
if (ref.current) {
|
|
||||||
clearTimeout(ref.current)
|
|
||||||
ref.current = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unregister = mergeRegister(
|
|
||||||
editor.registerCommand(
|
|
||||||
CLEAR_HIDE_MENU_TIMEOUT,
|
|
||||||
() => {
|
|
||||||
clearHideMenuTimeout()
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
COMMAND_PRIORITY_EDITOR,
|
|
||||||
),
|
|
||||||
editor.registerCommand(
|
editor.registerCommand(
|
||||||
BLUR_COMMAND,
|
BLUR_COMMAND,
|
||||||
(event) => {
|
(event) => {
|
||||||
// Check if the clicked target element is var-search-input
|
|
||||||
const target = event?.relatedTarget as HTMLElement
|
const target = event?.relatedTarget as HTMLElement
|
||||||
if (!target?.classList?.contains('var-search-input')) {
|
if (!target?.classList?.contains('var-search-input')) {
|
||||||
clearHideMenuTimeout()
|
|
||||||
ref.current = setTimeout(() => {
|
|
||||||
editor.dispatchCommand(KEY_ESCAPE_COMMAND, new KeyboardEvent('keydown', { key: 'Escape' }))
|
|
||||||
}, 200)
|
|
||||||
if (onBlur)
|
if (onBlur)
|
||||||
onBlur()
|
onBlur()
|
||||||
}
|
}
|
||||||
@@ -66,11 +42,6 @@ const OnBlurBlock: FC<OnBlurBlockProps> = ({
|
|||||||
COMMAND_PRIORITY_EDITOR,
|
COMMAND_PRIORITY_EDITOR,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearHideMenuTimeout()
|
|
||||||
unregister()
|
|
||||||
}
|
|
||||||
}, [editor, onBlur, onFocus])
|
}, [editor, onBlur, onFocus])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { $insertNodes } from 'lexical'
|
|||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
import { textToEditorState } from '../utils'
|
import { textToEditorState } from '../utils'
|
||||||
import { CustomTextNode } from './custom-text/node'
|
import { CustomTextNode } from './custom-text/node'
|
||||||
import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block'
|
|
||||||
|
|
||||||
export const PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER = 'PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER'
|
export const PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER = 'PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER'
|
||||||
export const PROMPT_EDITOR_INSERT_QUICKLY = 'PROMPT_EDITOR_INSERT_QUICKLY'
|
export const PROMPT_EDITOR_INSERT_QUICKLY = 'PROMPT_EDITOR_INSERT_QUICKLY'
|
||||||
@@ -30,8 +29,6 @@ const UpdateBlock = ({
|
|||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const textNode = new CustomTextNode('/')
|
const textNode = new CustomTextNode('/')
|
||||||
$insertNodes([textNode])
|
$insertNodes([textNode])
|
||||||
|
|
||||||
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { $insertNodes, COMMAND_PRIORITY_EDITOR } from 'lexical'
|
|||||||
import { Type } from '@/app/components/workflow/nodes/llm/types'
|
import { Type } from '@/app/components/workflow/nodes/llm/types'
|
||||||
import { BlockEnum } from '@/app/components/workflow/types'
|
import { BlockEnum } from '@/app/components/workflow/types'
|
||||||
import {
|
import {
|
||||||
CLEAR_HIDE_MENU_TIMEOUT,
|
|
||||||
DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
||||||
INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
||||||
UPDATE_WORKFLOW_NODES_MAP,
|
UPDATE_WORKFLOW_NODES_MAP,
|
||||||
@@ -134,7 +133,6 @@ describe('WorkflowVariableBlock', () => {
|
|||||||
const insertHandler = mockRegisterCommand.mock.calls[0][1] as (variables: string[]) => boolean
|
const insertHandler = mockRegisterCommand.mock.calls[0][1] as (variables: string[]) => boolean
|
||||||
const result = insertHandler(['node-1', 'answer'])
|
const result = insertHandler(['node-1', 'answer'])
|
||||||
|
|
||||||
expect(mockDispatchCommand).toHaveBeenCalledWith(CLEAR_HIDE_MENU_TIMEOUT, undefined)
|
|
||||||
expect($createWorkflowVariableBlockNode).toHaveBeenCalledWith(
|
expect($createWorkflowVariableBlockNode).toHaveBeenCalledWith(
|
||||||
['node-1', 'answer'],
|
['node-1', 'answer'],
|
||||||
workflowNodesMap,
|
workflowNodesMap,
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
|
|
||||||
export const INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND = createCommand('INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND')
|
export const INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND = createCommand('INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND')
|
||||||
export const DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND = createCommand('DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND')
|
export const DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND = createCommand('DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND')
|
||||||
export const CLEAR_HIDE_MENU_TIMEOUT = createCommand('CLEAR_HIDE_MENU_TIMEOUT')
|
|
||||||
export const UPDATE_WORKFLOW_NODES_MAP = createCommand('UPDATE_WORKFLOW_NODES_MAP')
|
export const UPDATE_WORKFLOW_NODES_MAP = createCommand('UPDATE_WORKFLOW_NODES_MAP')
|
||||||
|
|
||||||
export type WorkflowVariableBlockProps = {
|
export type WorkflowVariableBlockProps = {
|
||||||
@@ -49,7 +48,6 @@ const WorkflowVariableBlock = memo(({
|
|||||||
editor.registerCommand(
|
editor.registerCommand(
|
||||||
INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND,
|
||||||
(variables: string[]) => {
|
(variables: string[]) => {
|
||||||
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined)
|
|
||||||
const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap, getVarType)
|
const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap, getVarType)
|
||||||
|
|
||||||
$insertNodes([workflowVariableBlockNode])
|
$insertNodes([workflowVariableBlockNode])
|
||||||
|
|||||||
@@ -2768,7 +2768,7 @@
|
|||||||
},
|
},
|
||||||
"app/components/base/prompt-editor/plugins/workflow-variable-block/index.tsx": {
|
"app/components/base/prompt-editor/plugins/workflow-variable-block/index.tsx": {
|
||||||
"react-refresh/only-export-components": {
|
"react-refresh/only-export-components": {
|
||||||
"count": 4
|
"count": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx": {
|
"app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx": {
|
||||||
|
|||||||
Reference in New Issue
Block a user