From 68499eb2f49d5dd35e7240f08273c710cd8f0f52 Mon Sep 17 00:00:00 2001 From: dotta Date: Sun, 5 Apr 2026 06:39:20 -0500 Subject: [PATCH] Support dropping non-image files onto markdown editor as attachments When dragging files like .zip onto the issue description editor, non-image files are now uploaded as attachments instead of being silently ignored. Images continue to be handled inline by MDXEditor's image plugin. Co-Authored-By: Paperclip --- ui/src/components/InlineEditor.tsx | 4 ++++ ui/src/components/MarkdownEditor.tsx | 34 +++++++++++++++++++++++----- ui/src/pages/IssueDetail.tsx | 3 +++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/ui/src/components/InlineEditor.tsx b/ui/src/components/InlineEditor.tsx index 8b4c5b27..f509fe7a 100644 --- a/ui/src/components/InlineEditor.tsx +++ b/ui/src/components/InlineEditor.tsx @@ -11,6 +11,8 @@ interface InlineEditorProps { placeholder?: string; multiline?: boolean; imageUploadHandler?: (file: File) => Promise; + /** Called when a non-image file is dropped onto the editor. */ + onDropFile?: (file: File) => Promise; mentions?: MentionOption[]; nullable?: boolean; } @@ -46,6 +48,7 @@ export function InlineEditor({ multiline = false, nullable = false, imageUploadHandler, + onDropFile, mentions, }: InlineEditorProps) { const [editing, setEditing] = useState(false); @@ -228,6 +231,7 @@ export function InlineEditor({ className="bg-transparent" contentClassName={cn("paperclip-edit-in-place-content", className)} imageUploadHandler={imageUploadHandler} + onDropFile={onDropFile} mentions={mentions} onSubmit={() => { finalizeMultilineBlurOrSubmit(); diff --git a/ui/src/components/MarkdownEditor.tsx b/ui/src/components/MarkdownEditor.tsx index 62f4010d..ff779926 100644 --- a/ui/src/components/MarkdownEditor.tsx +++ b/ui/src/components/MarkdownEditor.tsx @@ -62,6 +62,8 @@ interface MarkdownEditorProps { contentClassName?: string; onBlur?: () => void; imageUploadHandler?: (file: File) => Promise; + /** Called when a non-image file is dropped onto the editor (e.g. .zip). */ + onDropFile?: (file: File) => Promise; bordered?: boolean; /** List of mentionable entities. Enables @-mention autocomplete. */ mentions?: MentionOption[]; @@ -314,6 +316,7 @@ export const MarkdownEditor = forwardRef contentClassName, onBlur, imageUploadHandler, + onDropFile, bordered = true, mentions, onSubmit, @@ -668,6 +671,7 @@ export const MarkdownEditor = forwardRef } const canDropImage = Boolean(imageUploadHandler); + const canDropFile = Boolean(imageUploadHandler || onDropFile); const handlePasteCapture = useCallback((event: ClipboardEvent) => { const clipboard = event.clipboardData; if (!clipboard || !ref.current) return; @@ -747,23 +751,41 @@ export const MarkdownEditor = forwardRef } }} onDragEnter={(evt) => { - if (!canDropImage || !hasFilePayload(evt)) return; + if (!canDropFile || !hasFilePayload(evt)) return; dragDepthRef.current += 1; setIsDragOver(true); }} onDragOver={(evt) => { - if (!canDropImage || !hasFilePayload(evt)) return; + if (!canDropFile || !hasFilePayload(evt)) return; evt.preventDefault(); evt.dataTransfer.dropEffect = "copy"; }} onDragLeave={() => { - if (!canDropImage) return; + if (!canDropFile) return; dragDepthRef.current = Math.max(0, dragDepthRef.current - 1); if (dragDepthRef.current === 0) setIsDragOver(false); }} - onDrop={() => { + onDrop={(evt) => { dragDepthRef.current = 0; setIsDragOver(false); + if (!onDropFile) return; + const files = evt.dataTransfer?.files; + if (!files || files.length === 0) return; + const allFiles = Array.from(files); + const nonImageFiles = allFiles.filter( + (f) => !f.type.startsWith("image/"), + ); + if (nonImageFiles.length === 0) return; + // If all dropped files are non-image, prevent default so MDXEditor + // doesn't try to handle them. If mixed, let images flow through to + // the image plugin and only handle the non-image files ourselves. + if (nonImageFiles.length === allFiles.length) { + evt.preventDefault(); + evt.stopPropagation(); + } + for (const file of nonImageFiles) { + void onDropFile(file); + } }} onPasteCapture={handlePasteCapture} > @@ -854,14 +876,14 @@ export const MarkdownEditor = forwardRef document.body, )} - {isDragOver && canDropImage && ( + {isDragOver && canDropFile && (
- Drop image to upload + Drop file to upload
)} {uploadError && ( diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 1ab5b578..d2cf091f 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -1329,6 +1329,9 @@ export function IssueDetail() { const attachment = await uploadAttachment.mutateAsync(file); return attachment.contentPath; }} + onDropFile={async (file) => { + await uploadAttachment.mutateAsync(file); + }} />