Skip to content

Commit ca5fea4

Browse files
Update components/file-tree.tsx
Co-authored-by: Copilot <[email protected]>
1 parent a2530e2 commit ca5fea4

File tree

1 file changed

+184
-185
lines changed

1 file changed

+184
-185
lines changed

components/file-tree.tsx

Lines changed: 184 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
import { useState } from 'react'
2-
import {
3-
ChevronDown,
4-
ChevronRight,
5-
File as FileIcon,
6-
Folder,
7-
Plus,
8-
Trash2,
9-
FolderPlus,
10-
} from 'lucide-react'
11-
import { Button } from './ui/button'
12-
import {
13-
Dialog,
14-
DialogContent,
15-
DialogHeader,
16-
DialogTitle,
17-
DialogTrigger,
18-
} from './ui/dialog'
1+
import { useState } from 'react'
2+
import {
3+
ChevronDown,
4+
ChevronRight,
5+
File as FileIcon,
6+
Folder,
7+
Plus,
8+
Trash2,
9+
FolderPlus,
10+
} from 'lucide-react'
11+
import { Button } from './ui/button'
12+
import {
13+
Dialog,
14+
DialogContent,
15+
DialogHeader,
16+
DialogTitle,
17+
DialogTrigger,
18+
} from './ui/dialog'
1919
import { Input } from './ui/input'
2020

2121
export interface FileSystemNode {
@@ -24,173 +24,172 @@ export interface FileSystemNode {
2424
children?: FileSystemNode[]
2525
}
2626

27-
interface FileTreeProps {
28-
files: FileSystemNode[]
29-
onSelectFile: (path: string) => void
30-
onCreateFile?: (path: string, isDirectory: boolean) => void
31-
onDeleteFile?: (path: string) => void
32-
}
33-
34-
export function FileTree({ files, onSelectFile, onCreateFile, onDeleteFile }: FileTreeProps) {
35-
const [newFileName, setNewFileName] = useState('')
36-
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
37-
const [createType, setCreateType] = useState<'file' | 'folder'>('file')
38-
39-
const handleCreateFile = () => {
40-
if (newFileName.trim() && onCreateFile) {
41-
onCreateFile(newFileName.trim(), createType === 'folder')
42-
setNewFileName('')
43-
setIsCreateDialogOpen(false)
44-
}
45-
}
46-
47-
return (
48-
<div className="p-2">
49-
<div className="flex items-center justify-between mb-2">
50-
<span className="text-sm font-medium text-muted-foreground">Files</span>
51-
<div className="flex gap-1">
52-
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
53-
<DialogTrigger asChild>
54-
<Button
55-
variant="ghost"
56-
size="icon"
57-
className="h-6 w-6"
58-
onClick={() => setCreateType('file')}
59-
>
60-
<Plus className="h-3 w-3" />
61-
</Button>
62-
</DialogTrigger>
63-
<DialogContent className="sm:max-w-md">
64-
<DialogHeader>
65-
<DialogTitle>Create New {createType === 'file' ? 'File' : 'Folder'}</DialogTitle>
66-
</DialogHeader>
67-
<div className="flex items-center space-x-2">
68-
<Input
69-
placeholder={createType === 'file' ? 'filename.ts' : 'folder-name'}
70-
value={newFileName}
71-
onChange={(e) => setNewFileName(e.target.value)}
72-
onKeyDown={(e) => {
73-
if (e.key === 'Enter') {
74-
handleCreateFile()
75-
}
76-
}}
77-
/>
78-
<Button onClick={handleCreateFile}>Create</Button>
79-
</div>
80-
<div className="flex gap-2 mt-2">
81-
<Button
82-
variant={createType === 'file' ? 'default' : 'outline'}
83-
size="sm"
84-
onClick={() => setCreateType('file')}
85-
>
86-
<FileIcon className="h-3 w-3 mr-1" />
87-
File
88-
</Button>
89-
<Button
90-
variant={createType === 'folder' ? 'default' : 'outline'}
91-
size="sm"
92-
onClick={() => setCreateType('folder')}
93-
>
94-
<FolderPlus className="h-3 w-3 mr-1" />
95-
Folder
96-
</Button>
97-
</div>
98-
</DialogContent>
99-
</Dialog>
100-
</div>
101-
</div>
102-
{files.map(file => (
103-
<FileTreeNode
104-
key={file.name}
105-
node={file}
106-
onSelectFile={onSelectFile}
107-
onDeleteFile={onDeleteFile}
108-
/>
109-
))}
110-
</div>
111-
)
27+
interface FileTreeProps {
28+
files: FileSystemNode[]
29+
onSelectFile: (path: string) => void
30+
onCreateFile?: (path: string, isDirectory: boolean) => void
31+
onDeleteFile?: (path: string) => void
11232
}
11333

114-
interface FileTreeNodeProps {
115-
node: FileSystemNode
116-
onSelectFile: (path: string) => void
117-
onDeleteFile?: (path: string) => void
118-
level?: number
119-
}
120-
121-
function FileTreeNode({
122-
node,
123-
onSelectFile,
124-
onDeleteFile,
125-
level = 0,
126-
path = '',
127-
}: FileTreeNodeProps & { path?: string }) {
128-
const [isOpen, setIsOpen] = useState(false)
129-
130-
const isDirectory = node.isDirectory
131-
const hasChildren = node.children && node.children.length > 0
132-
const newPath = `${path}/${node.name}`
133-
134-
const handleToggle = () => {
135-
if (isDirectory) {
136-
setIsOpen(!isOpen)
137-
} else {
138-
onSelectFile(newPath)
139-
}
140-
}
141-
142-
const handleDelete = () => {
143-
if (onDeleteFile) {
144-
onDeleteFile(newPath)
145-
}
146-
}
147-
148-
return (
149-
<div>
150-
<div
151-
className="flex items-center cursor-pointer hover:bg-muted/50 rounded-sm p-1 group"
152-
style={{ paddingLeft: level * 16 + 4 }}
153-
onClick={handleToggle}
154-
>
155-
{isDirectory ? (
156-
<>
157-
{isOpen ? (
158-
<ChevronDown size={16} className="mr-1 text-muted-foreground" />
159-
) : (
160-
<ChevronRight size={16} className="mr-1 text-muted-foreground" />
161-
)}
162-
<Folder size={16} className="mr-2 text-blue-500" />
163-
</>
164-
) : (
165-
<FileIcon size={16} className="mr-2 ml-4 text-muted-foreground" />
166-
)}
167-
<span className="text-sm truncate flex-1">{node.name}</span>
168-
{onDeleteFile && (
169-
<Button
170-
variant="ghost"
171-
size="icon"
172-
className="h-4 w-4 opacity-0 group-hover:opacity-100 ml-1"
173-
onClick={(e) => {
174-
e.stopPropagation()
175-
handleDelete()
176-
}}
177-
>
178-
<Trash2 className="h-3 w-3 text-red-500" />
179-
</Button>
180-
)}
181-
</div>
182-
{isOpen &&
183-
hasChildren &&
184-
node.children?.map(child => (
185-
<FileTreeNode
186-
key={child.name}
187-
node={child}
188-
onSelectFile={onSelectFile}
189-
onDeleteFile={onDeleteFile}
190-
level={level + 1}
191-
path={newPath}
192-
/>
193-
))}
194-
</div>
195-
)
34+
export function FileTree({ files, onSelectFile, onCreateFile, onDeleteFile }: FileTreeProps) {
35+
const [newFileName, setNewFileName] = useState('')
36+
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
37+
const [createType, setCreateType] = useState<'file' | 'folder'>('file')
38+
39+
const handleCreateFile = () => {
40+
if (newFileName.trim() && onCreateFile) {
41+
onCreateFile(newFileName.trim(), createType === 'folder')
42+
setNewFileName('')
43+
setIsCreateDialogOpen(false)
44+
}
45+
}
46+
47+
return (
48+
<div className="p-2">
49+
<div className="flex items-center justify-between mb-2">
50+
<span className="text-sm font-medium text-muted-foreground">Files</span>
51+
<div className="flex gap-1">
52+
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
53+
<DialogTrigger asChild>
54+
<Button
55+
variant="ghost"
56+
size="icon"
57+
className="h-6 w-6"
58+
>
59+
<Plus className="h-3 w-3" />
60+
</Button>
61+
</DialogTrigger>
62+
<DialogContent className="sm:max-w-md">
63+
<DialogHeader>
64+
<DialogTitle>Create New {createType === 'file' ? 'File' : 'Folder'}</DialogTitle>
65+
</DialogHeader>
66+
<div className="flex items-center space-x-2">
67+
<Input
68+
placeholder={createType === 'file' ? 'filename.ts' : 'folder-name'}
69+
value={newFileName}
70+
onChange={(e) => setNewFileName(e.target.value)}
71+
onKeyDown={(e) => {
72+
if (e.key === 'Enter') {
73+
handleCreateFile()
74+
}
75+
}}
76+
/>
77+
<Button onClick={handleCreateFile}>Create</Button>
78+
</div>
79+
<div className="flex gap-2 mt-2">
80+
<Button
81+
variant={createType === 'file' ? 'default' : 'outline'}
82+
size="sm"
83+
onClick={() => setCreateType('file')}
84+
>
85+
<FileIcon className="h-3 w-3 mr-1" />
86+
File
87+
</Button>
88+
<Button
89+
variant={createType === 'folder' ? 'default' : 'outline'}
90+
size="sm"
91+
onClick={() => setCreateType('folder')}
92+
>
93+
<FolderPlus className="h-3 w-3 mr-1" />
94+
Folder
95+
</Button>
96+
</div>
97+
</DialogContent>
98+
</Dialog>
99+
</div>
100+
</div>
101+
{files.map(file => (
102+
<FileTreeNode
103+
key={file.name}
104+
node={file}
105+
onSelectFile={onSelectFile}
106+
onDeleteFile={onDeleteFile}
107+
/>
108+
))}
109+
</div>
110+
)
111+
}
112+
113+
interface FileTreeNodeProps {
114+
node: FileSystemNode
115+
onSelectFile: (path: string) => void
116+
onDeleteFile?: (path: string) => void
117+
level?: number
118+
}
119+
120+
function FileTreeNode({
121+
node,
122+
onSelectFile,
123+
onDeleteFile,
124+
level = 0,
125+
path = '',
126+
}: FileTreeNodeProps & { path?: string }) {
127+
const [isOpen, setIsOpen] = useState(false)
128+
129+
const isDirectory = node.isDirectory
130+
const hasChildren = node.children && node.children.length > 0
131+
const newPath = `${path}/${node.name}`
132+
133+
const handleToggle = () => {
134+
if (isDirectory) {
135+
setIsOpen(!isOpen)
136+
} else {
137+
onSelectFile(newPath)
138+
}
139+
}
140+
141+
const handleDelete = () => {
142+
if (onDeleteFile) {
143+
onDeleteFile(newPath)
144+
}
145+
}
146+
147+
return (
148+
<div>
149+
<div
150+
className="flex items-center cursor-pointer hover:bg-muted/50 rounded-sm p-1 group"
151+
style={{ paddingLeft: level * 16 + 4 }}
152+
onClick={handleToggle}
153+
>
154+
{isDirectory ? (
155+
<>
156+
{isOpen ? (
157+
<ChevronDown size={16} className="mr-1 text-muted-foreground" />
158+
) : (
159+
<ChevronRight size={16} className="mr-1 text-muted-foreground" />
160+
)}
161+
<Folder size={16} className="mr-2 text-blue-500" />
162+
</>
163+
) : (
164+
<FileIcon size={16} className="mr-2 ml-4 text-muted-foreground" />
165+
)}
166+
<span className="text-sm truncate flex-1">{node.name}</span>
167+
{onDeleteFile && (
168+
<Button
169+
variant="ghost"
170+
size="icon"
171+
className="h-4 w-4 opacity-0 group-hover:opacity-100 ml-1"
172+
onClick={(e) => {
173+
e.stopPropagation()
174+
handleDelete()
175+
}}
176+
>
177+
<Trash2 className="h-3 w-3 text-red-500" />
178+
</Button>
179+
)}
180+
</div>
181+
{isOpen &&
182+
hasChildren &&
183+
node.children?.map(child => (
184+
<FileTreeNode
185+
key={child.name}
186+
node={child}
187+
onSelectFile={onSelectFile}
188+
onDeleteFile={onDeleteFile}
189+
level={level + 1}
190+
path={newPath}
191+
/>
192+
))}
193+
</div>
194+
)
196195
}

0 commit comments

Comments
 (0)