feat(workspace): add file creation functionality in FileTree and NewFileDialog

- Introduced a button in the FileTree component to create new files within directories.
- Updated NewFileDialog to accept an optional folderPath prop, allowing file creation in specified directories.
- Enhanced WorkspacePage to manage folder paths for new file creation, improving user experience when organizing files.
This commit is contained in:
luccast
2026-02-04 15:01:23 -05:00
parent a0e8fecc1b
commit 529273997f
3 changed files with 57 additions and 10 deletions
+22 -4
View File
@@ -1,5 +1,5 @@
import { useState, useCallback, useEffect } from 'react'
import { ChevronRight, ChevronDown, Folder, FolderOpen, FileText } from 'lucide-react'
import { ChevronRight, ChevronDown, Folder, FolderOpen, FileText, Plus } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
import type { DirectoryEntry } from '~/lib/workspace-fs'
@@ -19,6 +19,7 @@ interface FileTreeProps {
onSelect: (path: string, type: 'file' | 'directory') => void
onLoadDirectory?: (path: string) => Promise<DirectoryEntry[]>
onContextMenu?: (e: React.MouseEvent, path: string) => void
onCreateFile?: (folderPath: string) => void
level?: number
}
@@ -28,10 +29,11 @@ interface FileTreeItemProps {
onSelect: (path: string, type: 'file' | 'directory') => void
onLoadDirectory?: (path: string) => Promise<DirectoryEntry[]>
onContextMenu?: (e: React.MouseEvent, path: string) => void
onCreateFile?: (folderPath: string) => void
level: number
}
function FileTreeItem({ entry, selectedPath, onSelect, onLoadDirectory, onContextMenu, level }: FileTreeItemProps) {
function FileTreeItem({ entry, selectedPath, onSelect, onLoadDirectory, onContextMenu, onCreateFile, level }: FileTreeItemProps) {
const [expanded, setExpanded] = useState(false)
const [children, setChildren] = useState<DirectoryEntry[]>([])
const [loading, setLoading] = useState(false)
@@ -102,7 +104,7 @@ function FileTreeItem({ entry, selectedPath, onSelect, onLoadDirectory, onContex
onClick={handleClick}
onContextMenu={handleContextMenuFn}
style={{ paddingLeft }}
className={`flex items-center gap-2 py-1.5 pr-2 text-left transition-all duration-150 rounded-md mr-1 cursor-pointer ${
className={`group flex items-center gap-2 py-1.5 pr-2 text-left transition-all duration-150 rounded-md mr-1 cursor-pointer ${
isSelected
? 'bg-crab-500/20 text-crab-400 border-l-2 border-crab-400'
: 'text-gray-300 hover:bg-shell-800 hover:text-gray-100 border-l-2 border-transparent'
@@ -164,6 +166,20 @@ function FileTreeItem({ entry, selectedPath, onSelect, onLoadDirectory, onContex
{entry.size !== undefined && formatFileSize(entry.size)}
</span>
)}
{/* Add file button for directories */}
{isDirectory && onCreateFile && (
<button
onClick={(e) => {
e.stopPropagation()
onCreateFile(entry.path)
}}
className="p-1 opacity-0 group-hover:opacity-100 hover:bg-shell-700 rounded transition-all flex-shrink-0"
title="New file in this folder"
>
<Plus size={14} className="text-shell-500 hover:text-neon-mint" />
</button>
)}
</motion.div>
{/* Children */}
@@ -185,6 +201,7 @@ function FileTreeItem({ entry, selectedPath, onSelect, onLoadDirectory, onContex
onSelect={onSelect}
onLoadDirectory={onLoadDirectory}
onContextMenu={onContextMenu}
onCreateFile={onCreateFile}
level={level + 1}
/>
))
@@ -200,7 +217,7 @@ function FileTreeItem({ entry, selectedPath, onSelect, onLoadDirectory, onContex
)
}
export function FileTree({ entries, selectedPath, onSelect, onLoadDirectory, onContextMenu, level = 0 }: FileTreeProps) {
export function FileTree({ entries, selectedPath, onSelect, onLoadDirectory, onContextMenu, onCreateFile, level = 0 }: FileTreeProps) {
return (
<div className="py-1">
{entries.map((entry) => (
@@ -211,6 +228,7 @@ export function FileTree({ entries, selectedPath, onSelect, onLoadDirectory, onC
onSelect={onSelect}
onLoadDirectory={onLoadDirectory}
onContextMenu={onContextMenu}
onCreateFile={onCreateFile}
level={level}
/>
))}
+10 -2
View File
@@ -6,9 +6,10 @@ interface NewFileDialogProps {
open: boolean
onClose: () => void
onCreate: (fileName: string, content: string) => void
folderPath?: string // If set, file is created in this folder
}
export function NewFileDialog({ open, onClose, onCreate }: NewFileDialogProps) {
export function NewFileDialog({ open, onClose, onCreate, folderPath }: NewFileDialogProps) {
const [fileName, setFileName] = useState('')
const [content, setContent] = useState('')
const [error, setError] = useState<string | null>(null)
@@ -97,7 +98,14 @@ export function NewFileDialog({ open, onClose, onCreate }: NewFileDialogProps) {
<div className="p-2 rounded-lg bg-neon-mint/10 border border-neon-mint/30">
<Plus size={20} className="text-neon-mint" />
</div>
<h3 className="font-display text-lg text-gray-200">Create New File</h3>
<div className="flex-1 min-w-0">
<h3 className="font-display text-lg text-gray-200">Create New File</h3>
{folderPath && (
<p className="font-console text-xs text-shell-500 truncate">
in {folderPath.split('/').pop()}
</p>
)}
</div>
</div>
{/* Content */}
+25 -4
View File
@@ -369,6 +369,7 @@ function WorkspacePage() {
// New file dialog state
const [newFileDialogOpen, setNewFileDialogOpen] = useState(false)
const [newFileFolderPath, setNewFileFolderPath] = useState<string | null>(null)
// Context menu state
const [contextMenuOpen, setContextMenuOpen] = useState(false)
@@ -435,13 +436,20 @@ function WorkspacePage() {
}
}, [fileToDelete, workspacePath, selectedPath, handleRefresh])
// Handle create file
// Handle create file (uses newFileFolderPath if set)
const handleCreateFile = useCallback(
async (fileName: string, content: string) => {
try {
// If creating in a subfolder, prepend the relative path
let finalFileName = fileName
if (newFileFolderPath && newFileFolderPath !== workspacePath) {
const relativePath = newFileFolderPath.replace(workspacePath + '/', '')
finalFileName = `${relativePath}/${fileName}`
}
const result = await trpc.workspace.createFile.mutate({
workspaceRoot: workspacePath,
fileName,
fileName: finalFileName,
content,
})
@@ -459,9 +467,11 @@ function WorkspacePage() {
} catch (error) {
console.error('Failed to create file:', error)
throw error
} finally {
setNewFileFolderPath(null)
}
},
[workspacePath, loadFile, handleRefresh]
[workspacePath, newFileFolderPath, loadFile, handleRefresh]
)
// Handle context menu
@@ -476,6 +486,12 @@ function WorkspacePage() {
[]
)
// Handle create file in folder (from + button in tree)
const handleCreateFileInFolder = useCallback((folderPath: string) => {
setNewFileFolderPath(folderPath)
setNewFileDialogOpen(true)
}, [])
// Context menu items
const contextMenuItems = useMemo(() => [
{
@@ -721,6 +737,7 @@ function WorkspacePage() {
onSelect={handleSelect}
onLoadDirectory={handleLoadDirectory}
onContextMenu={handleContextMenu}
onCreateFile={handleCreateFileInFolder}
/>
) : (
<div className="p-4 text-center">
@@ -815,8 +832,12 @@ function WorkspacePage() {
{/* New file dialog */}
<NewFileDialog
open={newFileDialogOpen}
onClose={() => setNewFileDialogOpen(false)}
onClose={() => {
setNewFileDialogOpen(false)
setNewFileFolderPath(null)
}}
onCreate={handleCreateFile}
folderPath={newFileFolderPath ?? undefined}
/>
{/* Context menu */}