feat(studio): full IDE-like file management (#147)

## Summary

- **API**: POST (create), DELETE (delete), PATCH (rename/move), POST duplicate endpoints with null-byte sanitization
- **FileTree**: right-click context menu with New File, New Folder, Rename, Duplicate, Delete
- **Drag-and-drop**: move files between folders with visual feedback and subtree guard
- **Inline editing**: rename/create inputs with filename validation
- **Header actions**: quick New File / New Folder buttons in the FILES header

Stacks on top of `feat/studio-code-quality`.

## Test plan

- [x] Right-click file → Rename, Delete, Duplicate all work
- [x] Right-click folder → New File, New Folder, Delete work
- [x] Drag file from one folder to another
- [x] Create file with invalid name (`../foo`, `a/b`) → rejected client-side
- [x] Delete currently-edited file → editor clears
- [x] Studio build succeeds
This commit is contained in:
Miguel Ángel
2026-03-31 20:03:05 +02:00
committed by GitHub
parent 256c7a74fe
commit ecb590d444
5 changed files with 1293 additions and 115 deletions
@@ -26,6 +26,12 @@ interface LeftSidebarProps {
fileTree?: string[];
editingFile?: { path: string; content: string | null } | null;
onSelectFile?: (path: string) => void;
onCreateFile?: (path: string) => void;
onCreateFolder?: (path: string) => void;
onDeleteFile?: (path: string) => void;
onRenameFile?: (oldPath: string, newPath: string) => void;
onDuplicateFile?: (path: string) => void;
onMoveFile?: (oldPath: string, newPath: string) => void;
codeChildren?: ReactNode;
onLint?: () => void;
linting?: boolean;
@@ -42,6 +48,12 @@ export const LeftSidebar = memo(function LeftSidebar({
fileTree: fileProp,
editingFile,
onSelectFile,
onCreateFile,
onCreateFolder,
onDeleteFile,
onRenameFile,
onDuplicateFile,
onMoveFile,
codeChildren,
onLint,
linting,
@@ -122,16 +134,28 @@ export const LeftSidebar = memo(function LeftSidebar({
/>
)}
{tab === "assets" && (
<AssetsTab projectId={projectId} assets={assets} onImport={onImportFiles} />
<AssetsTab
projectId={projectId}
assets={assets}
onImport={onImportFiles}
onDelete={onDeleteFile}
onRename={onRenameFile}
/>
)}
{tab === "code" && (
<div className="flex flex-1 min-h-0">
{(fileProp?.length ?? 0) > 0 && (
<div className="w-[140px] flex-shrink-0 border-r border-neutral-800 overflow-y-auto">
<div className="w-[160px] flex-shrink-0 border-r border-neutral-800 overflow-y-auto">
<FileTree
files={fileProp ?? []}
activeFile={editingFile?.path ?? null}
onSelectFile={onSelectFile ?? (() => {})}
onCreateFile={onCreateFile}
onCreateFolder={onCreateFolder}
onDeleteFile={onDeleteFile}
onRenameFile={onRenameFile}
onDuplicateFile={onDuplicateFile}
onMoveFile={onMoveFile}
/>
</div>
)}