Copy and paste as well as dragging files added
CI / build (push) Has been skipped
CI / deploy (push) Failing after 34s

This commit is contained in:
2026-03-12 18:09:45 -05:00
parent cd5fb71899
commit 55047b5d90
2 changed files with 85 additions and 2 deletions
+69 -1
View File
@@ -8,7 +8,10 @@ import { CustomImage as Image } from "@/lib/extensions/image";
import Underline from "@tiptap/extension-underline"; import Underline from "@tiptap/extension-underline";
import { X } from "lucide-solid"; import { X } from "lucide-solid";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { store } from "@/store"; import { store, activeTaskId, activeNoteId, uploadTaskAttachment, uploadNoteAttachment } from "@/store";
import { convertImageToWebpFile } from "@/lib/utils";
import { toast } from "solid-sonner";
import type { Editor } from "@tiptap/core";
import { SlashCommands } from "@/lib/slash-command"; import { SlashCommands } from "@/lib/slash-command";
import { suggestion } from "@/lib/slash-renderer"; import { suggestion } from "@/lib/slash-renderer";
@@ -57,6 +60,50 @@ export const TaskEditor: Component<TaskEditorProps> = (props) => {
setZoomOffset({ x: 0, y: 0 }); setZoomOffset({ x: 0, y: 0 });
}; };
const handleFileUploads = async (editor: Editor, files: File[]) => {
const taskId = activeTaskId();
const noteId = activeNoteId();
if (!taskId && !noteId) {
toast.error("Cannot upload files outside of a task or note.");
return;
}
for (const file of files) {
const isImage = file.type.startsWith('image/');
const isVideo = file.type.startsWith('video/');
toast.promise(
(async () => {
let fileToUpload = file;
if (isImage) {
fileToUpload = await convertImageToWebpFile(file);
}
if (taskId) {
return uploadTaskAttachment(taskId, fileToUpload);
} else {
return uploadNoteAttachment(noteId!, fileToUpload);
}
})(),
{
loading: `Uploading ${file.name}...`,
success: ({ url, filename }) => {
if (isImage) {
editor.chain().focus().setImage({ src: url, filename }).run();
} else if (isVideo) {
editor.chain().focus().setVideo({ src: url, filename }).run();
} else {
editor.chain().focus().insertFileAttachment({ src: url, filename, filesize: file.size.toString() }).run();
}
return `${file.name} uploaded.`;
},
error: `Failed to upload ${file.name}.`
}
);
}
};
const editor = createTiptapEditor(() => ({ const editor = createTiptapEditor(() => ({
element: editorRef!, element: editorRef!,
extensions: [ extensions: [
@@ -179,6 +226,27 @@ export const TaskEditor: Component<TaskEditorProps> = (props) => {
props.onUpdate(editor.getHTML()); props.onUpdate(editor.getHTML());
}, },
editorProps: { editorProps: {
handlePaste: (_view, event) => {
const items = Array.from(event.clipboardData?.items || []);
const files = items
.map(item => item.getAsFile())
.filter((file): file is File => file !== null);
if (files.length > 0) {
event.preventDefault();
handleFileUploads(editor()!, files);
return true;
}
return false;
},
handleDrop: (_view, event, _slice, moved) => {
if (!moved && event.dataTransfer?.files?.length) {
event.preventDefault();
handleFileUploads(editor()!, Array.from(event.dataTransfer.files));
return true;
}
return false;
},
attributes: { attributes: {
class: cn( class: cn(
"prose prose-sm dark:prose-invert max-w-none focus:outline-none min-h-[150px]", "prose prose-sm dark:prose-invert max-w-none focus:outline-none min-h-[150px]",
+16 -1
View File
@@ -24,6 +24,17 @@ export const CustomImage = Image.extend({
}; };
}, },
addCommands() {
return {
setImage: (options: { src: string, alt?: string, title?: string, filename?: string }) => ({ commands }) => {
return commands.insertContent({
type: this.name,
attrs: options,
});
},
};
},
addNodeView() { addNodeView() {
return ({ node, getPos, editor }) => { return ({ node, getPos, editor }) => {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
@@ -76,10 +87,14 @@ export const CustomImage = Image.extend({
}, },
}); });
import type { SetImageOptions } from "@tiptap/extension-image";
declare module '@tiptap/core' { declare module '@tiptap/core' {
interface Commands<ReturnType> { interface Commands<ReturnType> {
image: { image: {
setImage: (options: { src: string, alt?: string, title?: string, filename?: string }) => ReturnType; // @ts-ignore
setImage: (options: SetImageOptions & { filename?: string }) => ReturnType;
}; };
} }
} }
// @ts-ignore