Combined upload slash commands

This commit is contained in:
2026-03-17 18:47:53 -05:00
parent 94fce5652a
commit 8f8bd8dcac
7 changed files with 90 additions and 32 deletions
+5 -22
View File
@@ -1,5 +1,5 @@
import { type Component, createSignal, For, createEffect } from "solid-js";
import { Type, Code, Quote, ImageIcon, Heading1, Heading2, Heading3, List, ListTodo, Film, FileText, AppWindow, Minus, Grid3X3, Trash2 } from "lucide-solid";
import { Type, Code, Quote, Heading1, Heading2, Heading3, List, ListTodo, FileText, AppWindow, Minus, Grid3X3, Trash2 } from "lucide-solid";
import { cn } from "@/lib/utils";
export interface CommandItem {
@@ -80,29 +80,12 @@ export const getSuggestionItems = ({ query }: { query: string }): CommandItem[]
},
},
{
title: "Image",
description: "Upload an image.",
icon: ImageIcon,
command: ({ editor, range }: { editor: any, range: any }) => {
editor.chain().focus().deleteRange(range).uploadImage().run();
},
},
{
title: "Video",
description: "Upload a video.",
aliases: ["movie", "mp4", "mov"],
icon: Film,
command: ({ editor, range }: { editor: any, range: any }) => {
editor.chain().focus().deleteRange(range).uploadVideo().run();
},
},
{
title: "File",
description: "Upload a document or file.",
aliases: ["file", "pdf", "doc", "upload"],
title: "Media / File",
description: "Upload an image, video, or document.",
aliases: ["image", "video", "file", "upload", "pdf", "movie"],
icon: FileText,
command: ({ editor, range }: { editor: any, range: any }) => {
editor.chain().focus().deleteRange(range).uploadFile().run();
editor.chain().focus().deleteRange(range).uploadMedia().run();
},
},
{
+2 -6
View File
@@ -30,11 +30,9 @@ import { Dropcursor } from "@tiptap/extension-dropcursor";
import { Mention } from "@tiptap/extension-mention";
import { userSuggestion, noteSuggestion } from "@/lib/mention-suggestion";
import { FileViewer } from "@/lib/extensions/file-viewer";
import { ImageUpload } from "@/lib/extensions/image-upload";
import { Video } from "@/lib/extensions/video";
import { VideoUpload } from "@/lib/extensions/video-upload";
import { FileAttachment } from "@/lib/extensions/file-attachment";
import { FileUpload } from "@/lib/extensions/file-upload";
import { MediaUpload } from "@/lib/extensions/media-upload";
interface TaskEditorProps {
content?: string;
@@ -214,11 +212,9 @@ export const TaskEditor: Component<TaskEditorProps> = (props) => {
width: 2,
}),
FileViewer,
ImageUpload,
Video,
VideoUpload,
FileAttachment,
FileUpload,
MediaUpload,
],
// Use untrack so the editor doesn't re-initialize when props.content changes
content: untrack(() => props.content) || "",
+1 -1
View File
@@ -182,7 +182,7 @@ export const FileAttachment = Node.create<FileAttachmentOptions>({
// Delete button overlay
const overlay = document.createElement('div');
overlay.classList.add('absolute', 'top-1.5', 'right-1.5', 'flex', '[@media(hover:hover)]:hidden', '[@media(hover:hover)]:group-hover:flex', 'bg-background/90', 'backdrop-blur-sm', 'p-1', 'rounded-lg', 'border', 'border-border', 'shadow-sm', 'z-10');
overlay.classList.add('absolute', 'top-1.5', 'left-1.5', 'flex', '[@media(hover:hover)]:hidden', '[@media(hover:hover)]:group-hover:flex', 'bg-background/90', 'backdrop-blur-sm', 'p-1', 'rounded-lg', 'border', 'border-border', 'shadow-sm', 'z-10');
const deleteBtn = document.createElement('button');
deleteBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground hover:text-destructive transition-colors"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg>';
+1 -1
View File
@@ -95,7 +95,7 @@ export const FileViewer = Node.create<FileViewerOptions>({
// Delete button overlay
const overlay = document.createElement('div');
overlay.classList.add('absolute', 'top-2', 'right-2', 'flex', '[@media(hover:hover)]:hidden', '[@media(hover:hover)]:group-hover:flex', 'bg-background/80', 'backdrop-blur-sm', 'p-1', 'rounded-lg', 'border', 'border-border', 'shadow-sm', 'z-10');
overlay.classList.add('absolute', 'top-2', 'left-2', 'flex', '[@media(hover:hover)]:hidden', '[@media(hover:hover)]:group-hover:flex', 'bg-background/80', 'backdrop-blur-sm', 'p-1', 'rounded-lg', 'border', 'border-border', 'shadow-sm', 'z-10');
const deleteBtn = document.createElement('button');
deleteBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground hover:text-destructive transition-colors"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg>';
+1 -1
View File
@@ -159,7 +159,7 @@ export const CustomImage = Image.extend({
// Delete button overlay
const overlay = document.createElement('div');
overlay.classList.add('absolute', 'top-2', 'right-2', 'flex', '[@media(hover:hover)]:hidden', '[@media(hover:hover)]:group-hover:flex', 'bg-background/80', 'backdrop-blur-sm', 'p-1', 'rounded-lg', 'border', 'border-border', 'shadow-sm', 'z-10');
overlay.classList.add('absolute', 'top-2', 'left-2', 'flex', '[@media(hover:hover)]:hidden', '[@media(hover:hover)]:group-hover:flex', 'bg-background/80', 'backdrop-blur-sm', 'p-1', 'rounded-lg', 'border', 'border-border', 'shadow-sm', 'z-10');
const deleteBtn = document.createElement('button');
deleteBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground hover:text-destructive transition-colors"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg>';
+79
View File
@@ -0,0 +1,79 @@
import { Extension } from '@tiptap/core';
import { activeTaskId, uploadTaskAttachment, activeNoteId, uploadNoteAttachment } from "@/store";
import { convertImageToWebpFile } from "@/lib/utils";
import { toast } from "solid-sonner";
declare module '@tiptap/core' {
interface Commands<ReturnType> {
mediaUpload: {
uploadMedia: () => ReturnType;
};
}
}
export const MediaUpload = Extension.create({
name: 'mediaUpload',
addCommands() {
return {
uploadMedia: () => ({ editor }) => {
const taskId = activeTaskId();
const noteId = activeNoteId();
if (!taskId && !noteId) {
toast.error("Cannot upload media outside of a task or note.");
return false;
}
const input = document.createElement("input");
input.type = "file";
input.onchange = async () => {
if (input.files?.length) {
const originalFile = input.files[0];
const isImage = originalFile.type.startsWith('image/');
const isVideo = originalFile.type.startsWith('video/');
toast.promise(
(async () => {
let fileToUpload = originalFile;
if (isImage && !originalFile.type.includes('svg') && !originalFile.type.includes('gif')) {
fileToUpload = await convertImageToWebpFile(originalFile);
}
if (taskId) {
return uploadTaskAttachment(taskId, fileToUpload);
} else {
return uploadNoteAttachment(noteId!, fileToUpload);
}
})(),
{
loading: "Processing & uploading media...",
success: ({ url, filename }) => {
if (isImage) {
// @ts-ignore
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: originalFile.size.toString() }).run();
}
// Manually force a new paragraph at the end so the user can keep typing
editor.chain()
.focus('end')
.insertContent('<p></p>')
.run();
return "Media uploaded.";
},
error: "Failed to upload media."
}
);
}
};
input.click();
return true;
},
};
},
});
+1 -1
View File
@@ -213,7 +213,7 @@ export const Video = Node.create<VideoOptions>({
// Delete button overlay
const overlay = document.createElement('div');
overlay.classList.add('absolute', 'top-2', 'right-2', 'flex', '[@media(hover:hover)]:hidden', '[@media(hover:hover)]:group-hover:flex', 'bg-background/80', 'backdrop-blur-sm', 'p-1', 'rounded-lg', 'border', 'border-border', 'shadow-sm', 'z-10');
overlay.classList.add('absolute', 'top-2', 'left-2', 'flex', '[@media(hover:hover)]:hidden', '[@media(hover:hover)]:group-hover:flex', 'bg-background/80', 'backdrop-blur-sm', 'p-1', 'rounded-lg', 'border', 'border-border', 'shadow-sm', 'z-10');
const deleteBtn = document.createElement('button');
deleteBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-muted-foreground hover:text-destructive transition-colors"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg>';