fix: prevent editor handling note wikilink drops

This commit is contained in:
lucaronin
2026-04-26 14:37:11 +02:00
parent 1136c1948e
commit 292d3739df
3 changed files with 116 additions and 10 deletions

View 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')
})
})

View File

@@ -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])
}

View File

@@ -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()