mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(studio): drag-drop assetfile/folder and asset import anywhere in the studio (#155)
## Summary - Add `/api/projects/:id/upload` endpoint for multipart file uploads with automatic dedup naming - Wire `onImportFiles` from Assets tab "Import media" button through to the upload API - Add global drag-drop overlay — drop media files **anywhere** in the studio, not just the Assets panel - Files that already exist get `(2)`, `(3)` suffixes instead of overwriting - Support uploading into subdirectories via `?dir=` param — dropping on a folder imports there - Make folders draggable in the file tree + support drop-to-root - Add `bodyLimit` middleware for early rejection of oversized payloads - Surface skipped/failed uploads via toast notification instead of console-only Addresses feedback: _"Wish I could upload/drag-drop assets directly in the Studio (music, images, video) like a CapCut media panel"_ ## Test plan - [x] Open studio, drag an image/video/audio file onto any part of the UI - [x] Verify the drop overlay appears with "Drop files to import" message - [x] Drop the file — verify it appears in the Assets tab and file tree - [x] Drop a file with the same name — verify it gets a `(2)` suffix - [x] Click "Import media" button in Assets tab — verify file picker works - [x] Import multiple files at once via drag-drop - [x] Drop a file onto a nested folder in the file tree — verify it lands in that folder - [x] Drag a folder in the file tree and drop it on another folder or root — verify it moves - [x] Drop a file >500MB — verify toast notification appears - [x] Verify drag overlay doesn't get stuck when dragging over nested UI elements
This commit is contained in:
@@ -39,6 +39,7 @@ export interface FileTreeProps {
|
||||
onRenameFile?: (oldPath: string, newPath: string) => void;
|
||||
onDuplicateFile?: (path: string) => void;
|
||||
onMoveFile?: (oldPath: string, newPath: string) => void;
|
||||
onImportFiles?: (files: FileList, dir?: string) => void;
|
||||
}
|
||||
|
||||
interface TreeNode {
|
||||
@@ -475,6 +476,8 @@ function TreeFolder({
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
draggable
|
||||
onDragStart={(e) => onDragStart(e, node.fullPath)}
|
||||
onClick={toggle}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -638,6 +641,7 @@ export const FileTree = memo(function FileTree({
|
||||
onRenameFile,
|
||||
onDuplicateFile,
|
||||
onMoveFile,
|
||||
onImportFiles,
|
||||
}: FileTreeProps) {
|
||||
const tree = useMemo(() => buildTree(files), [files]);
|
||||
const children = useMemo(() => sortChildren(tree.children), [tree]);
|
||||
@@ -770,7 +774,15 @@ export const FileTree = memo(function FileTree({
|
||||
}, []);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(_e: React.DragEvent, folderPath: string) => {
|
||||
(e: React.DragEvent, folderPath: string) => {
|
||||
// External files from desktop — import into the target folder
|
||||
if (e.dataTransfer.files.length > 0 && !dragSourceRef.current) {
|
||||
e.preventDefault();
|
||||
onImportFiles?.(e.dataTransfer.files, folderPath || undefined);
|
||||
setDragOverFolder(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const sourcePath = dragSourceRef.current;
|
||||
if (!sourcePath || !onMoveFile) {
|
||||
setDragOverFolder(null);
|
||||
@@ -788,7 +800,7 @@ export const FileTree = memo(function FileTree({
|
||||
setDragOverFolder(null);
|
||||
dragSourceRef.current = null;
|
||||
},
|
||||
[onMoveFile],
|
||||
[onMoveFile, onImportFiles],
|
||||
);
|
||||
|
||||
const handleDragLeave = useCallback(() => {
|
||||
@@ -836,7 +848,26 @@ export const FileTree = memo(function FileTree({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto py-1" onContextMenu={handleRootContextMenu}>
|
||||
<div
|
||||
className={`flex-1 overflow-y-auto py-1 transition-colors ${
|
||||
dragOverFolder === ""
|
||||
? "bg-[#3CE6AC]/5 outline outline-1 outline-[#3CE6AC]/30 -outline-offset-1"
|
||||
: ""
|
||||
}`}
|
||||
onContextMenu={handleRootContextMenu}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
// Show root highlight when dragging over the background (not a child folder)
|
||||
if (e.target === e.currentTarget) setDragOverFolder("");
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
if (e.target === e.currentTarget) setDragOverFolder(null);
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
handleDrop(e, "");
|
||||
}}
|
||||
>
|
||||
{/* Root-level inline input for new file/folder */}
|
||||
{inlineInput &&
|
||||
(inlineInput.mode === "new-file" || inlineInput.mode === "new-folder") &&
|
||||
|
||||
Reference in New Issue
Block a user