mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-19 07:42:43 +00:00
feat: add global file drag-and-drop to open Add Memory modal
- Dropping files anywhere on the main page opens the modal with files pre-loaded - Full-screen overlay with 'Drop files to add as memories' appears during drag - Unsupported file types show error toast, accepted files are queued in file tab - Settings page excluded (separate page component, not affected) - initialFiles prop threaded through AddDocumentModal → AddDocument - Consumed-once ref prevents re-seeding on re-renders
This commit is contained in:
parent
24aca7ae72
commit
56118cacec
2 changed files with 112 additions and 2 deletions
|
|
@ -24,6 +24,7 @@ import { RaycastDetail } from "@/components/integrations/raycast-detail"
|
|||
import { PluginsDetail } from "@/components/integrations/plugins-detail"
|
||||
import { AnimatedGradientBackground } from "@/components/animated-gradient-background"
|
||||
import { AddDocumentModal } from "@/components/add-document"
|
||||
import { isAcceptedFile } from "@/components/add-document/file"
|
||||
import { DocumentModal } from "@/components/document-modal"
|
||||
import { DocumentsCommandPalette } from "@/components/documents-command-palette"
|
||||
import { FullscreenNoteModal } from "@/components/fullscreen-note-modal"
|
||||
|
|
@ -49,6 +50,7 @@ import type { z } from "zod"
|
|||
import { useViewMode } from "@/lib/view-mode-context"
|
||||
import type { MemoryOfDay } from "@/components/dashboard-view"
|
||||
import { ErrorBoundary } from "@/components/error-boundary"
|
||||
import { FileTextIcon } from "lucide-react"
|
||||
import { cn } from "@lib/utils"
|
||||
import {
|
||||
addDocumentParam,
|
||||
|
|
@ -381,6 +383,64 @@ export default function NewPage() {
|
|||
enabled: !!user,
|
||||
})
|
||||
|
||||
// Global file drag-and-drop: dropping files anywhere on the page opens the Add Memory modal
|
||||
const [globalDroppedFiles, setGlobalDroppedFiles] = useState<File[]>([])
|
||||
const [isGlobalDragging, setIsGlobalDragging] = useState(false)
|
||||
const globalDragCounter = useRef(0)
|
||||
|
||||
const handleGlobalDragEnter = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
globalDragCounter.current++
|
||||
if (e.dataTransfer.types.includes("Files")) {
|
||||
setIsGlobalDragging(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleGlobalDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
globalDragCounter.current--
|
||||
if (globalDragCounter.current === 0) {
|
||||
setIsGlobalDragging(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleGlobalDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
}, [])
|
||||
|
||||
const handleGlobalDrop = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
globalDragCounter.current = 0
|
||||
setIsGlobalDragging(false)
|
||||
if (addDoc !== null) return // Modal already open, let it handle its own drops
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
const accepted = files.filter(isAcceptedFile)
|
||||
if (accepted.length === 0) {
|
||||
if (files.length > 0) {
|
||||
toast.error(
|
||||
files.length === 1
|
||||
? "This file type is not supported"
|
||||
: `${files.length} files are not supported`,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
const rejected = files.length - accepted.length
|
||||
if (rejected > 0) {
|
||||
toast.error(
|
||||
rejected === 1
|
||||
? "One file type is not supported"
|
||||
: `${rejected} files are not supported`,
|
||||
)
|
||||
}
|
||||
setGlobalDroppedFiles(accepted)
|
||||
analytics.addDocumentModalOpened()
|
||||
setAddDoc("file")
|
||||
},
|
||||
[addDoc, setAddDoc],
|
||||
)
|
||||
|
||||
useHotkeys("c", () => {
|
||||
analytics.addDocumentModalOpened()
|
||||
setAddDoc("note")
|
||||
|
|
@ -540,7 +600,24 @@ export default function NewPage() {
|
|||
"relative flex min-h-dvh flex-col bg-[#05080D]",
|
||||
isGraphMode && "h-dvh overflow-hidden",
|
||||
)}
|
||||
onDragEnter={handleGlobalDragEnter}
|
||||
onDragLeave={handleGlobalDragLeave}
|
||||
onDragOver={handleGlobalDragOver}
|
||||
onDrop={handleGlobalDrop}
|
||||
>
|
||||
{isGlobalDragging && addDoc === null && (
|
||||
<div className="pointer-events-none fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="flex flex-col items-center gap-3 rounded-2xl border-2 border-dashed border-[#4BA0FA] bg-[#4BA0FA]/10 px-12 py-10">
|
||||
<FileTextIcon className="size-10 text-[#4BA0FA]" />
|
||||
<p className="text-lg font-medium text-white">
|
||||
Drop files to add as memories
|
||||
</p>
|
||||
<p className="text-sm text-[#737373]">
|
||||
PDFs, images, documents, and more
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showNovaBackdrop && (
|
||||
<>
|
||||
<AnimatedGradientBackground
|
||||
|
|
@ -719,7 +796,11 @@ export default function NewPage() {
|
|||
|
||||
<AddDocumentModal
|
||||
isOpen={addDoc !== null}
|
||||
onClose={() => setAddDoc(null)}
|
||||
onClose={() => {
|
||||
setAddDoc(null)
|
||||
setGlobalDroppedFiles([])
|
||||
}}
|
||||
initialFiles={globalDroppedFiles}
|
||||
/>
|
||||
<DocumentsCommandPalette
|
||||
open={isSearchOpen}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,14 @@ type TabType = "note" | "link" | "file" | "connect"
|
|||
interface AddDocumentModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
initialFiles?: File[]
|
||||
}
|
||||
|
||||
export function AddDocumentModal({ isOpen, onClose }: AddDocumentModalProps) {
|
||||
export function AddDocumentModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
initialFiles,
|
||||
}: AddDocumentModalProps) {
|
||||
const isMobile = useIsMobile()
|
||||
const hasUnsavedContentRef = useRef<() => boolean>(() => false)
|
||||
const isSubmittingRef = useRef(false)
|
||||
|
|
@ -74,6 +79,7 @@ export function AddDocumentModal({ isOpen, onClose }: AddDocumentModalProps) {
|
|||
isOpen={isOpen}
|
||||
hasUnsavedContentRef={hasUnsavedContentRef}
|
||||
isSubmittingRef={isSubmittingRef}
|
||||
initialFiles={initialFiles}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
|
@ -115,12 +121,14 @@ export function AddDocument({
|
|||
isOpen,
|
||||
hasUnsavedContentRef,
|
||||
isSubmittingRef,
|
||||
initialFiles,
|
||||
}: {
|
||||
onClose: () => void
|
||||
onRequestClose?: () => void
|
||||
isOpen?: boolean
|
||||
hasUnsavedContentRef?: React.MutableRefObject<() => boolean>
|
||||
isSubmittingRef?: React.MutableRefObject<boolean>
|
||||
initialFiles?: File[]
|
||||
}) {
|
||||
const isMobile = useIsMobile()
|
||||
const [addParam, setAddParam] = useQueryState("add", addDocumentParam)
|
||||
|
|
@ -182,6 +190,27 @@ export function AddDocument({
|
|||
}
|
||||
}, [isOpen])
|
||||
|
||||
// Seed file queue from global drag-and-drop (only once per modal open)
|
||||
const initialFilesConsumed = useRef(false)
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
initialFilesConsumed.current = false
|
||||
return
|
||||
}
|
||||
if (initialFilesConsumed.current) return
|
||||
if (!initialFiles || initialFiles.length === 0) return
|
||||
initialFilesConsumed.current = true
|
||||
const items: FileQueueItem[] = initialFiles.map((file) => ({
|
||||
id: crypto.randomUUID(),
|
||||
file,
|
||||
status: "pending" as const,
|
||||
}))
|
||||
setFileData((prev) => ({
|
||||
...prev,
|
||||
items: [...prev.items, ...items],
|
||||
}))
|
||||
}, [isOpen, initialFiles])
|
||||
|
||||
// Submit handlers
|
||||
const handleNoteSubmit = useCallback(
|
||||
(content: string, contentType: "note" | "link") => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue