fix: prevent editor handling note wikilink drops
This commit is contained in:
94
src/hooks/useNoteWikilinkDrop.test.tsx
Normal file
94
src/hooks/useNoteWikilinkDrop.test.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useRef } from 'react'
|
||||
import { clearDraggedNotePath } from '../components/note-retargeting/noteDragData'
|
||||
import { useNoteWikilinkDrop } from './useNoteWikilinkDrop'
|
||||
|
||||
const NOTE_DRAG_MIME = 'application/x-laputa-note-path'
|
||||
|
||||
function createMockDataTransfer(seedData: Record<string, string>): DataTransfer {
|
||||
const data = new Map(Object.entries(seedData))
|
||||
const types = Array.from(data.keys())
|
||||
|
||||
return {
|
||||
dropEffect: 'none',
|
||||
effectAllowed: 'move',
|
||||
setData(type: string, value: string) {
|
||||
data.set(type, value)
|
||||
if (!types.includes(type)) types.push(type)
|
||||
},
|
||||
getData(type: string) {
|
||||
return data.get(type) ?? ''
|
||||
},
|
||||
clearData() {
|
||||
data.clear()
|
||||
types.splice(0, types.length)
|
||||
},
|
||||
get types() {
|
||||
return types
|
||||
},
|
||||
} as DataTransfer
|
||||
}
|
||||
|
||||
function createDragEvent(type: 'dragover' | 'drop', dataTransfer: DataTransfer): DragEvent {
|
||||
const event = new Event(type, { bubbles: true, cancelable: true }) as DragEvent
|
||||
Object.defineProperty(event, 'dataTransfer', { value: dataTransfer })
|
||||
return event
|
||||
}
|
||||
|
||||
function NoteDropHarness({
|
||||
onInsertTarget,
|
||||
vaultPath = '/vault',
|
||||
}: {
|
||||
onInsertTarget: (target: string) => void
|
||||
vaultPath?: string
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
useNoteWikilinkDrop({ containerRef, onInsertTarget, vaultPath })
|
||||
|
||||
return (
|
||||
<div data-testid="note-drop-container" ref={containerRef}>
|
||||
<div data-testid="note-drop-child">Editor body</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
describe('useNoteWikilinkDrop', () => {
|
||||
afterEach(() => {
|
||||
clearDraggedNotePath()
|
||||
})
|
||||
|
||||
it('claims note dragover events before editor internals handle them', () => {
|
||||
render(<NoteDropHarness onInsertTarget={vi.fn()} />)
|
||||
const container = screen.getByTestId('note-drop-container')
|
||||
const child = screen.getByTestId('note-drop-child')
|
||||
const editorDragOver = vi.fn()
|
||||
container.addEventListener('dragover', editorDragOver)
|
||||
const dataTransfer = createMockDataTransfer({
|
||||
[NOTE_DRAG_MIME]: '/vault/Projects/Alpha.md',
|
||||
'text/plain': '/vault/Projects/Alpha.md',
|
||||
})
|
||||
|
||||
child.dispatchEvent(createDragEvent('dragover', dataTransfer))
|
||||
|
||||
expect(editorDragOver).not.toHaveBeenCalled()
|
||||
expect(dataTransfer.dropEffect).toBe('link')
|
||||
})
|
||||
|
||||
it('claims note drops before editor internals handle them', () => {
|
||||
const onInsertTarget = vi.fn()
|
||||
render(<NoteDropHarness onInsertTarget={onInsertTarget} />)
|
||||
const container = screen.getByTestId('note-drop-container')
|
||||
const child = screen.getByTestId('note-drop-child')
|
||||
const editorDrop = vi.fn()
|
||||
container.addEventListener('drop', editorDrop)
|
||||
|
||||
child.dispatchEvent(createDragEvent('drop', createMockDataTransfer({
|
||||
[NOTE_DRAG_MIME]: '/vault/Projects/Alpha.md',
|
||||
'text/plain': '/vault/Projects/Alpha.md',
|
||||
})))
|
||||
|
||||
expect(editorDrop).not.toHaveBeenCalled()
|
||||
expect(onInsertTarget).toHaveBeenCalledWith('Projects/Alpha')
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useEffectEvent, type RefObject } from 'react'
|
||||
import { readDraggedNotePath } from '../components/note-retargeting/noteDragData'
|
||||
import { clearDraggedNotePath, readDraggedNotePath } from '../components/note-retargeting/noteDragData'
|
||||
import { relativePathStem } from '../utils/wikilink'
|
||||
|
||||
const MARKDOWN_NOTE_PATH = /\.md$/i
|
||||
const NOTE_DROP_EVENT_OPTIONS = { capture: true }
|
||||
|
||||
interface UseNoteWikilinkDropOptions {
|
||||
containerRef: RefObject<HTMLElement | null>
|
||||
@@ -19,6 +20,12 @@ function droppedNoteWikilinkTarget(dataTransfer: DataTransfer | null, vaultPath?
|
||||
return relativePathStem(notePath, vaultPath)
|
||||
}
|
||||
|
||||
function claimNoteDropEvent(event: DragEvent): void {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = 'link'
|
||||
}
|
||||
|
||||
export function useNoteWikilinkDrop({
|
||||
containerRef,
|
||||
onInsertTarget,
|
||||
@@ -27,28 +34,33 @@ export function useNoteWikilinkDrop({
|
||||
const handleDragOver = useEffectEvent((event: DragEvent) => {
|
||||
if (!droppedNoteWikilinkTarget(event.dataTransfer, vaultPath)) return
|
||||
|
||||
event.preventDefault()
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = 'link'
|
||||
claimNoteDropEvent(event)
|
||||
})
|
||||
|
||||
const handleDrop = useEffectEvent((event: DragEvent) => {
|
||||
const target = droppedNoteWikilinkTarget(event.dataTransfer, vaultPath)
|
||||
if (!target) return
|
||||
|
||||
event.preventDefault()
|
||||
onInsertTarget(target)
|
||||
claimNoteDropEvent(event)
|
||||
try {
|
||||
onInsertTarget(target)
|
||||
} finally {
|
||||
clearDraggedNotePath()
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
|
||||
container.addEventListener('dragover', handleDragOver)
|
||||
container.addEventListener('drop', handleDrop)
|
||||
container.addEventListener('dragenter', handleDragOver, NOTE_DROP_EVENT_OPTIONS)
|
||||
container.addEventListener('dragover', handleDragOver, NOTE_DROP_EVENT_OPTIONS)
|
||||
container.addEventListener('drop', handleDrop, NOTE_DROP_EVENT_OPTIONS)
|
||||
|
||||
return () => {
|
||||
container.removeEventListener('dragover', handleDragOver)
|
||||
container.removeEventListener('drop', handleDrop)
|
||||
container.removeEventListener('dragenter', handleDragOver, NOTE_DROP_EVENT_OPTIONS)
|
||||
container.removeEventListener('dragover', handleDragOver, NOTE_DROP_EVENT_OPTIONS)
|
||||
container.removeEventListener('drop', handleDrop, NOTE_DROP_EVENT_OPTIONS)
|
||||
}
|
||||
}, [containerRef])
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ describe('main entrypoint', () => {
|
||||
rootOptions().onCaughtError?.(error, { componentStack: '\n in App' })
|
||||
|
||||
expect(mocks.sentryHandler).toHaveBeenCalledWith(error, { componentStack: '\n in App' })
|
||||
}, 10_000)
|
||||
}, 20_000)
|
||||
|
||||
it('normalizes missing React component stacks before handing errors to Sentry', async () => {
|
||||
await importEntrypoint()
|
||||
|
||||
Reference in New Issue
Block a user