mirror of
https://github.com/langgenius/dify.git
synced 2026-04-24 12:55:49 +08:00
Merge remote-tracking branch 'origin/main' into feat/support-agent-sandbox
This commit is contained in:
@ -0,0 +1,209 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useCommand, useFontSize } from '../hooks'
|
||||
|
||||
type MockSelectionParent = { isLink: boolean } | null
|
||||
|
||||
const {
|
||||
mockDispatchCommand,
|
||||
mockEditorUpdate,
|
||||
mockRegisterUpdateListener,
|
||||
mockRegisterCommand,
|
||||
mockRead,
|
||||
mockSetLinkAnchorElement,
|
||||
mockSelectionNode,
|
||||
mockSelection,
|
||||
mockPatchStyleText,
|
||||
mockSetSelection,
|
||||
mockSelectionFontSize,
|
||||
mockIsRangeSelection,
|
||||
mockSelectedIsBullet,
|
||||
mockSetBlocksType,
|
||||
} = vi.hoisted(() => ({
|
||||
mockDispatchCommand: vi.fn(),
|
||||
mockEditorUpdate: vi.fn(),
|
||||
mockRegisterUpdateListener: vi.fn(),
|
||||
mockRegisterCommand: vi.fn(),
|
||||
mockRead: vi.fn(),
|
||||
mockSetLinkAnchorElement: vi.fn(),
|
||||
mockSelectionNode: {
|
||||
getParent: vi.fn<() => MockSelectionParent>(() => null),
|
||||
},
|
||||
mockSelection: {
|
||||
anchor: {
|
||||
getNode: vi.fn(),
|
||||
},
|
||||
focus: {
|
||||
getNode: vi.fn(),
|
||||
},
|
||||
isBackward: vi.fn(() => false),
|
||||
clone: vi.fn(() => 'cloned-selection'),
|
||||
},
|
||||
mockPatchStyleText: vi.fn(),
|
||||
mockSetSelection: vi.fn(),
|
||||
mockSelectionFontSize: vi.fn(),
|
||||
mockIsRangeSelection: vi.fn(() => true),
|
||||
mockSelectedIsBullet: vi.fn(() => false),
|
||||
mockSetBlocksType: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@lexical/react/LexicalComposerContext', () => ({
|
||||
useLexicalComposerContext: () => ([{
|
||||
dispatchCommand: mockDispatchCommand,
|
||||
update: mockEditorUpdate,
|
||||
registerUpdateListener: mockRegisterUpdateListener,
|
||||
registerCommand: mockRegisterCommand,
|
||||
getEditorState: () => ({
|
||||
read: mockRead,
|
||||
}),
|
||||
}]),
|
||||
}))
|
||||
|
||||
vi.mock('@lexical/link', () => ({
|
||||
$isLinkNode: (node: unknown) => Boolean(node && typeof node === 'object' && 'isLink' in (node as object)),
|
||||
TOGGLE_LINK_COMMAND: 'toggle-link-command',
|
||||
}))
|
||||
|
||||
vi.mock('@lexical/list', () => ({
|
||||
INSERT_UNORDERED_LIST_COMMAND: 'insert-unordered-list-command',
|
||||
}))
|
||||
|
||||
vi.mock('@lexical/selection', () => ({
|
||||
$getSelectionStyleValueForProperty: () => mockSelectionFontSize(),
|
||||
$isAtNodeEnd: () => false,
|
||||
$patchStyleText: mockPatchStyleText,
|
||||
$setBlocksType: mockSetBlocksType,
|
||||
}))
|
||||
|
||||
vi.mock('@lexical/utils', () => ({
|
||||
mergeRegister: (...cleanups: Array<() => void>) => () => cleanups.forEach(cleanup => cleanup()),
|
||||
}))
|
||||
|
||||
vi.mock('lexical', () => ({
|
||||
$createParagraphNode: () => ({ type: 'paragraph' }),
|
||||
$getSelection: () => mockSelection,
|
||||
$isRangeSelection: () => mockIsRangeSelection(),
|
||||
$setSelection: mockSetSelection,
|
||||
COMMAND_PRIORITY_CRITICAL: 4,
|
||||
FORMAT_TEXT_COMMAND: 'format-text-command',
|
||||
SELECTION_CHANGE_COMMAND: 'selection-change-command',
|
||||
}))
|
||||
|
||||
vi.mock('../../store', () => ({
|
||||
useNoteEditorStore: () => ({
|
||||
getState: () => ({
|
||||
selectedIsBullet: mockSelectedIsBullet(),
|
||||
setLinkAnchorElement: mockSetLinkAnchorElement,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('note toolbar hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockEditorUpdate.mockImplementation((callback) => {
|
||||
callback()
|
||||
})
|
||||
mockRegisterUpdateListener.mockImplementation((listener) => {
|
||||
listener({})
|
||||
return vi.fn()
|
||||
})
|
||||
mockRegisterCommand.mockImplementation((_command, listener) => {
|
||||
listener()
|
||||
return vi.fn()
|
||||
})
|
||||
mockRead.mockImplementation((callback) => {
|
||||
callback()
|
||||
})
|
||||
mockSelectionFontSize.mockReturnValue('16px')
|
||||
mockIsRangeSelection.mockReturnValue(true)
|
||||
mockSelectedIsBullet.mockReturnValue(false)
|
||||
mockSelection.anchor.getNode.mockReturnValue(mockSelectionNode)
|
||||
mockSelection.focus.getNode.mockReturnValue(mockSelectionNode)
|
||||
mockSelectionNode.getParent.mockReturnValue(null)
|
||||
})
|
||||
|
||||
describe('useCommand', () => {
|
||||
it('should dispatch text formatting commands directly', () => {
|
||||
const { result } = renderHook(() => useCommand())
|
||||
|
||||
result.current.handleCommand('bold')
|
||||
result.current.handleCommand('italic')
|
||||
result.current.handleCommand('strikethrough')
|
||||
|
||||
expect(mockDispatchCommand).toHaveBeenNthCalledWith(1, 'format-text-command', 'bold')
|
||||
expect(mockDispatchCommand).toHaveBeenNthCalledWith(2, 'format-text-command', 'italic')
|
||||
expect(mockDispatchCommand).toHaveBeenNthCalledWith(3, 'format-text-command', 'strikethrough')
|
||||
})
|
||||
|
||||
it('should open link editing when current selection is not already a link', () => {
|
||||
const { result } = renderHook(() => useCommand())
|
||||
|
||||
result.current.handleCommand('link')
|
||||
|
||||
expect(mockDispatchCommand).toHaveBeenCalledWith('toggle-link-command', '')
|
||||
expect(mockSetLinkAnchorElement).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should remove the link when the current selection is already within a link node', () => {
|
||||
mockSelectionNode.getParent.mockReturnValue({ isLink: true })
|
||||
const { result } = renderHook(() => useCommand())
|
||||
|
||||
result.current.handleCommand('link')
|
||||
|
||||
expect(mockDispatchCommand).toHaveBeenCalledWith('toggle-link-command', null)
|
||||
expect(mockSetLinkAnchorElement).toHaveBeenCalledWith()
|
||||
})
|
||||
|
||||
it('should ignore link commands when the selection is not a range', () => {
|
||||
mockIsRangeSelection.mockReturnValue(false)
|
||||
const { result } = renderHook(() => useCommand())
|
||||
|
||||
result.current.handleCommand('link')
|
||||
|
||||
expect(mockDispatchCommand).not.toHaveBeenCalled()
|
||||
expect(mockSetLinkAnchorElement).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should toggle bullet formatting on and off', () => {
|
||||
const { result, rerender } = renderHook(() => useCommand())
|
||||
|
||||
result.current.handleCommand('bullet')
|
||||
expect(mockDispatchCommand).toHaveBeenCalledWith('insert-unordered-list-command', undefined)
|
||||
|
||||
mockSelectedIsBullet.mockReturnValue(true)
|
||||
rerender()
|
||||
|
||||
result.current.handleCommand('bullet')
|
||||
expect(mockSetBlocksType).toHaveBeenCalledWith(mockSelection, expect.any(Function))
|
||||
})
|
||||
})
|
||||
|
||||
describe('useFontSize', () => {
|
||||
it('should expose font size state and update selection styling', () => {
|
||||
const { result } = renderHook(() => useFontSize())
|
||||
|
||||
expect(result.current.fontSize).toBe('16px')
|
||||
|
||||
result.current.handleFontSize('20px')
|
||||
expect(mockPatchStyleText).toHaveBeenCalledWith(mockSelection, { 'font-size': '20px' })
|
||||
})
|
||||
|
||||
it('should preserve the current selection when opening the selector', () => {
|
||||
const { result } = renderHook(() => useFontSize())
|
||||
|
||||
result.current.handleOpenFontSizeSelector(true)
|
||||
|
||||
expect(mockSetSelection).toHaveBeenCalledWith('cloned-selection')
|
||||
})
|
||||
|
||||
it('should keep the default font size and avoid patching styles when the selection is not a range', () => {
|
||||
mockIsRangeSelection.mockReturnValue(false)
|
||||
const { result } = renderHook(() => useFontSize())
|
||||
|
||||
expect(result.current.fontSize).toBe('12px')
|
||||
|
||||
result.current.handleFontSize('20px')
|
||||
expect(mockPatchStyleText).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -27,55 +27,72 @@ import {
|
||||
import { useNoteEditorStore } from '../store'
|
||||
import { getSelectedNode } from '../utils'
|
||||
|
||||
const DEFAULT_FONT_SIZE = '12px'
|
||||
|
||||
const updateFontSizeFromSelection = (setFontSize: (fontSize: string) => void) => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection))
|
||||
setFontSize($getSelectionStyleValueForProperty(selection, 'font-size', DEFAULT_FONT_SIZE))
|
||||
}
|
||||
|
||||
const toggleLink = (
|
||||
editor: ReturnType<typeof useLexicalComposerContext>[0],
|
||||
noteEditorStore: ReturnType<typeof useNoteEditorStore>,
|
||||
) => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
|
||||
if (!$isRangeSelection(selection))
|
||||
return
|
||||
|
||||
const node = getSelectedNode(selection)
|
||||
const parent = node.getParent()
|
||||
const { setLinkAnchorElement } = noteEditorStore.getState()
|
||||
|
||||
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
||||
setLinkAnchorElement()
|
||||
return
|
||||
}
|
||||
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, '')
|
||||
setLinkAnchorElement(true)
|
||||
})
|
||||
}
|
||||
|
||||
const toggleBullet = (
|
||||
editor: ReturnType<typeof useLexicalComposerContext>[0],
|
||||
selectedIsBullet: boolean,
|
||||
) => {
|
||||
if (!selectedIsBullet) {
|
||||
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)
|
||||
return
|
||||
}
|
||||
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection))
|
||||
$setBlocksType(selection, () => $createParagraphNode())
|
||||
})
|
||||
}
|
||||
|
||||
export const useCommand = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const noteEditorStore = useNoteEditorStore()
|
||||
|
||||
const handleCommand = useCallback((type: string) => {
|
||||
if (type === 'bold')
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')
|
||||
|
||||
if (type === 'italic')
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')
|
||||
|
||||
if (type === 'strikethrough')
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough')
|
||||
if (type === 'bold' || type === 'italic' || type === 'strikethrough') {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, type)
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'link') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
|
||||
if ($isRangeSelection(selection)) {
|
||||
const node = getSelectedNode(selection)
|
||||
const parent = node.getParent()
|
||||
const { setLinkAnchorElement } = noteEditorStore.getState()
|
||||
|
||||
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
||||
setLinkAnchorElement()
|
||||
}
|
||||
else {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, '')
|
||||
setLinkAnchorElement(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
toggleLink(editor, noteEditorStore)
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'bullet') {
|
||||
const { selectedIsBullet } = noteEditorStore.getState()
|
||||
|
||||
if (selectedIsBullet) {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if ($isRangeSelection(selection))
|
||||
$setBlocksType(selection, () => $createParagraphNode())
|
||||
})
|
||||
}
|
||||
else {
|
||||
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)
|
||||
}
|
||||
}
|
||||
if (type === 'bullet')
|
||||
toggleBullet(editor, noteEditorStore.getState().selectedIsBullet)
|
||||
}, [editor, noteEditorStore])
|
||||
|
||||
return {
|
||||
@ -85,7 +102,7 @@ export const useCommand = () => {
|
||||
|
||||
export const useFontSize = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const [fontSize, setFontSize] = useState('12px')
|
||||
const [fontSize, setFontSize] = useState(DEFAULT_FONT_SIZE)
|
||||
const [fontSizeSelectorShow, setFontSizeSelectorShow] = useState(false)
|
||||
|
||||
const handleFontSize = useCallback((fontSize: string) => {
|
||||
@ -113,24 +130,13 @@ export const useFontSize = () => {
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(() => {
|
||||
editor.getEditorState().read(() => {
|
||||
const selection = $getSelection()
|
||||
|
||||
if ($isRangeSelection(selection)) {
|
||||
const fontSize = $getSelectionStyleValueForProperty(selection, 'font-size', '12px')
|
||||
setFontSize(fontSize)
|
||||
}
|
||||
updateFontSizeFromSelection(setFontSize)
|
||||
})
|
||||
}),
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
() => {
|
||||
const selection = $getSelection()
|
||||
|
||||
if ($isRangeSelection(selection)) {
|
||||
const fontSize = $getSelectionStyleValueForProperty(selection, 'font-size', '12px')
|
||||
setFontSize(fontSize)
|
||||
}
|
||||
|
||||
updateFontSizeFromSelection(setFontSize)
|
||||
return false
|
||||
},
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
|
||||
Reference in New Issue
Block a user