mirror of
https://github.com/crabwise-ai/crabwalk.git
synced 2026-08-14 00:57:52 +00:00
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:
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user