mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 11:40: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;
|
placeholder?: string;
|
||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
imageUploadHandler?: (file: File) => Promise<string>;
|
imageUploadHandler?: (file: File) => Promise<string>;
|
||||||
|
/** Called when a non-image file is dropped onto the editor. */
|
||||||
|
onDropFile?: (file: File) => Promise<void>;
|
||||||
mentions?: MentionOption[];
|
mentions?: MentionOption[];
|
||||||
nullable?: boolean;
|
nullable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -46,6 +48,7 @@ export function InlineEditor({
|
||||||
multiline = false,
|
multiline = false,
|
||||||
nullable = false,
|
nullable = false,
|
||||||
imageUploadHandler,
|
imageUploadHandler,
|
||||||
|
onDropFile,
|
||||||
mentions,
|
mentions,
|
||||||
}: InlineEditorProps) {
|
}: InlineEditorProps) {
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
@ -228,6 +231,7 @@ export function InlineEditor({
|
||||||
className="bg-transparent"
|
className="bg-transparent"
|
||||||
contentClassName={cn("paperclip-edit-in-place-content", className)}
|
contentClassName={cn("paperclip-edit-in-place-content", className)}
|
||||||
imageUploadHandler={imageUploadHandler}
|
imageUploadHandler={imageUploadHandler}
|
||||||
|
onDropFile={onDropFile}
|
||||||
mentions={mentions}
|
mentions={mentions}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
finalizeMultilineBlurOrSubmit();
|
finalizeMultilineBlurOrSubmit();
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,8 @@ interface MarkdownEditorProps {
|
||||||
contentClassName?: string;
|
contentClassName?: string;
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
imageUploadHandler?: (file: File) => Promise<string>;
|
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;
|
bordered?: boolean;
|
||||||
/** List of mentionable entities. Enables @-mention autocomplete. */
|
/** List of mentionable entities. Enables @-mention autocomplete. */
|
||||||
mentions?: MentionOption[];
|
mentions?: MentionOption[];
|
||||||
|
|
@ -314,6 +316,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
||||||
contentClassName,
|
contentClassName,
|
||||||
onBlur,
|
onBlur,
|
||||||
imageUploadHandler,
|
imageUploadHandler,
|
||||||
|
onDropFile,
|
||||||
bordered = true,
|
bordered = true,
|
||||||
mentions,
|
mentions,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
|
@ -668,6 +671,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
||||||
}
|
}
|
||||||
|
|
||||||
const canDropImage = Boolean(imageUploadHandler);
|
const canDropImage = Boolean(imageUploadHandler);
|
||||||
|
const canDropFile = Boolean(imageUploadHandler || onDropFile);
|
||||||
const handlePasteCapture = useCallback((event: ClipboardEvent<HTMLDivElement>) => {
|
const handlePasteCapture = useCallback((event: ClipboardEvent<HTMLDivElement>) => {
|
||||||
const clipboard = event.clipboardData;
|
const clipboard = event.clipboardData;
|
||||||
if (!clipboard || !ref.current) return;
|
if (!clipboard || !ref.current) return;
|
||||||
|
|
@ -747,23 +751,41 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onDragEnter={(evt) => {
|
onDragEnter={(evt) => {
|
||||||
if (!canDropImage || !hasFilePayload(evt)) return;
|
if (!canDropFile || !hasFilePayload(evt)) return;
|
||||||
dragDepthRef.current += 1;
|
dragDepthRef.current += 1;
|
||||||
setIsDragOver(true);
|
setIsDragOver(true);
|
||||||
}}
|
}}
|
||||||
onDragOver={(evt) => {
|
onDragOver={(evt) => {
|
||||||
if (!canDropImage || !hasFilePayload(evt)) return;
|
if (!canDropFile || !hasFilePayload(evt)) return;
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.dataTransfer.dropEffect = "copy";
|
evt.dataTransfer.dropEffect = "copy";
|
||||||
}}
|
}}
|
||||||
onDragLeave={() => {
|
onDragLeave={() => {
|
||||||
if (!canDropImage) return;
|
if (!canDropFile) return;
|
||||||
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
|
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
|
||||||
if (dragDepthRef.current === 0) setIsDragOver(false);
|
if (dragDepthRef.current === 0) setIsDragOver(false);
|
||||||
}}
|
}}
|
||||||
onDrop={() => {
|
onDrop={(evt) => {
|
||||||
dragDepthRef.current = 0;
|
dragDepthRef.current = 0;
|
||||||
setIsDragOver(false);
|
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}
|
onPasteCapture={handlePasteCapture}
|
||||||
>
|
>
|
||||||
|
|
@ -854,14 +876,14 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
||||||
document.body,
|
document.body,
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isDragOver && canDropImage && (
|
{isDragOver && canDropFile && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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",
|
"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",
|
!bordered && "inset-0 rounded-sm",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Drop image to upload
|
Drop file to upload
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{uploadError && (
|
{uploadError && (
|
||||||
|
|
|
||||||
|
|
@ -1329,6 +1329,9 @@ export function IssueDetail() {
|
||||||
const attachment = await uploadAttachment.mutateAsync(file);
|
const attachment = await uploadAttachment.mutateAsync(file);
|
||||||
return attachment.contentPath;
|
return attachment.contentPath;
|
||||||
}}
|
}}
|
||||||
|
onDropFile={async (file) => {
|
||||||
|
await uploadAttachment.mutateAsync(file);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue