mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
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 <noreply@paperclip.ing>
This commit is contained in:
parent
e9c8bd4805
commit
68499eb2f4
3 changed files with 35 additions and 6 deletions
|
|
@ -11,6 +11,8 @@ interface InlineEditorProps {
|
|||
placeholder?: string;
|
||||
multiline?: boolean;
|
||||
imageUploadHandler?: (file: File) => Promise<string>;
|
||||
/** Called when a non-image file is dropped onto the editor. */
|
||||
onDropFile?: (file: File) => Promise<void>;
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ interface MarkdownEditorProps {
|
|||
contentClassName?: string;
|
||||
onBlur?: () => void;
|
||||
imageUploadHandler?: (file: File) => Promise<string>;
|
||||
/** Called when a non-image file is dropped onto the editor (e.g. .zip). */
|
||||
onDropFile?: (file: File) => Promise<void>;
|
||||
bordered?: boolean;
|
||||
/** List of mentionable entities. Enables @-mention autocomplete. */
|
||||
mentions?: MentionOption[];
|
||||
|
|
@ -314,6 +316,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
|||
contentClassName,
|
||||
onBlur,
|
||||
imageUploadHandler,
|
||||
onDropFile,
|
||||
bordered = true,
|
||||
mentions,
|
||||
onSubmit,
|
||||
|
|
@ -668,6 +671,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
|||
}
|
||||
|
||||
const canDropImage = Boolean(imageUploadHandler);
|
||||
const canDropFile = Boolean(imageUploadHandler || onDropFile);
|
||||
const handlePasteCapture = useCallback((event: ClipboardEvent<HTMLDivElement>) => {
|
||||
const clipboard = event.clipboardData;
|
||||
if (!clipboard || !ref.current) return;
|
||||
|
|
@ -747,23 +751,41 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
|||
}
|
||||
}}
|
||||
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<MarkdownEditorRef, MarkdownEditorProps>
|
|||
document.body,
|
||||
)}
|
||||
|
||||
{isDragOver && canDropImage && (
|
||||
{isDragOver && canDropFile && (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-1 z-40 flex items-center justify-center rounded-md border border-dashed border-primary/80 bg-primary/10 text-xs font-medium text-primary",
|
||||
!bordered && "inset-0 rounded-sm",
|
||||
)}
|
||||
>
|
||||
Drop image to upload
|
||||
Drop file to upload
|
||||
</div>
|
||||
)}
|
||||
{uploadError && (
|
||||
|
|
|
|||
|
|
@ -1329,6 +1329,9 @@ export function IssueDetail() {
|
|||
const attachment = await uploadAttachment.mutateAsync(file);
|
||||
return attachment.contentPath;
|
||||
}}
|
||||
onDropFile={async (file) => {
|
||||
await uploadAttachment.mutateAsync(file);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue