2
2
3
3
import { useFile } from '@onlook/file-system/hooks' ;
4
4
import type { ImageContentData } from '@onlook/models' ;
5
+ import {
6
+ AlertDialog ,
7
+ AlertDialogAction ,
8
+ AlertDialogCancel ,
9
+ AlertDialogContent ,
10
+ AlertDialogDescription ,
11
+ AlertDialogFooter ,
12
+ AlertDialogHeader ,
13
+ AlertDialogTitle
14
+ } from '@onlook/ui/alert-dialog' ;
15
+ import { Button } from '@onlook/ui/button' ;
16
+ import {
17
+ DropdownMenu ,
18
+ DropdownMenuContent ,
19
+ DropdownMenuItem ,
20
+ DropdownMenuTrigger
21
+ } from '@onlook/ui/dropdown-menu' ;
5
22
import { Icons } from '@onlook/ui/icons' ;
23
+ import { Input } from '@onlook/ui/input' ;
24
+ import { getMimeType } from '@onlook/utility' ;
6
25
import { useEffect , useState } from 'react' ;
26
+ import { toast } from 'sonner' ;
7
27
8
28
interface ImageItemProps {
9
29
image : {
@@ -17,12 +37,19 @@ interface ImageItemProps {
17
37
onImageDragEnd : ( ) => void ;
18
38
onImageMouseDown : ( ) => void ;
19
39
onImageMouseUp : ( ) => void ;
40
+ onRename : ( oldPath : string , newName : string ) => Promise < void > ;
41
+ onDelete : ( filePath : string ) => Promise < void > ;
42
+ onAddToChat : ( imagePath : string ) => void ;
20
43
}
21
44
22
- export const ImageItem = ( { image, projectId, branchId, onImageDragStart, onImageDragEnd, onImageMouseDown, onImageMouseUp } : ImageItemProps ) => {
45
+ export const ImageItem = ( { image, projectId, branchId, onImageDragStart, onImageDragEnd, onImageMouseDown, onImageMouseUp, onRename , onDelete , onAddToChat } : ImageItemProps ) => {
23
46
const { content, loading } = useFile ( projectId , branchId , image . path ) ;
24
47
const [ imageUrl , setImageUrl ] = useState < string | null > ( null ) ;
25
48
const [ isDisabled , setIsDisabled ] = useState ( false ) ;
49
+ const [ isRenaming , setIsRenaming ] = useState ( false ) ;
50
+ const [ newName , setNewName ] = useState ( image . name ) ;
51
+ const [ showDeleteDialog , setShowDeleteDialog ] = useState ( false ) ;
52
+ const [ dropdownOpen , setDropdownOpen ] = useState ( false ) ;
26
53
27
54
// Convert content to data URL for display
28
55
useEffect ( ( ) => {
@@ -56,6 +83,13 @@ export const ImageItem = ({ image, projectId, branchId, onImageDragStart, onImag
56
83
} ;
57
84
} , [ content , image . mimeType , image . name ] ) ;
58
85
86
+ // Close dropdown when entering rename mode or showing delete dialog
87
+ useEffect ( ( ) => {
88
+ if ( isRenaming || showDeleteDialog ) {
89
+ setDropdownOpen ( false ) ;
90
+ }
91
+ } , [ isRenaming , showDeleteDialog ] ) ;
92
+
59
93
if ( loading ) {
60
94
return (
61
95
< div className = "aspect-square bg-background-secondary rounded-md border border-border-primary flex items-center justify-center" >
@@ -81,30 +115,166 @@ export const ImageItem = ({ image, projectId, branchId, onImageDragStart, onImag
81
115
const imageContentData : ImageContentData = {
82
116
fileName : image . name ,
83
117
content : content as string ,
84
- mimeType : imageUrl ,
118
+ mimeType : getMimeType ( image . name ) ,
85
119
originPath : image . path ,
86
120
} ;
87
121
onImageDragStart ( e , imageContentData ) ;
88
122
} ;
89
123
124
+ const handleRename = async ( ) => {
125
+ if ( newName . trim ( ) && newName !== image . name ) {
126
+ try {
127
+ await onRename ( image . path , newName . trim ( ) ) ;
128
+ setIsRenaming ( false ) ;
129
+ } catch ( error ) {
130
+ toast . error ( 'Failed to rename file' , {
131
+ description : error instanceof Error ? error . message : 'Unknown error' ,
132
+ } ) ;
133
+ console . error ( 'Failed to rename file:' , error ) ;
134
+ setNewName ( image . name ) ; // Reset on error
135
+ }
136
+ } else {
137
+ setIsRenaming ( false ) ;
138
+ }
139
+ } ;
140
+
141
+ const handleDelete = async ( ) => {
142
+ try {
143
+ await onDelete ( image . path ) ;
144
+ setShowDeleteDialog ( false ) ;
145
+ } catch ( error ) {
146
+ toast . error ( 'Failed to delete file' , {
147
+ description : error instanceof Error ? error . message : 'Unknown error' ,
148
+ } ) ;
149
+ console . error ( 'Failed to delete file:' , error ) ;
150
+ }
151
+ } ;
152
+
153
+ const handleAddToChat = ( ) => {
154
+ onAddToChat ( image . path ) ;
155
+ } ;
156
+
157
+ const handleKeyDown = ( e : React . KeyboardEvent ) => {
158
+ if ( e . key === 'Enter' ) {
159
+ void handleRename ( ) ;
160
+ } else if ( e . key === 'Escape' ) {
161
+ setNewName ( image . name ) ;
162
+ setIsRenaming ( false ) ;
163
+ }
164
+ } ;
165
+
90
166
return (
91
- < div className = "aspect-square bg-background-secondary rounded-md border border-border-primary overflow-hidden cursor-pointer hover:border-border-onlook transition-colors"
92
- onDragStart = { handleDragStart }
93
- onDragEnd = { onImageDragEnd }
94
- onMouseDown = { onImageMouseDown }
95
- onMouseUp = { onImageMouseUp }
96
- >
97
- < img
98
- src = { imageUrl }
99
- alt = { image . name }
100
- className = "w-full h-full object-cover"
101
- loading = "lazy"
102
- />
103
- < div className = "p-1 bg-background-primary/80 backdrop-blur-sm" >
104
- < div className = "text-xs text-foreground-primary truncate" title = { image . name } >
105
- { image . name }
106
- </ div >
167
+ < div className = "group" >
168
+ < div
169
+ className = "aspect-square bg-background-secondary rounded-md border border-border-primary overflow-hidden cursor-pointer hover:border-border-onlook transition-colors relative"
170
+ onDragStart = { handleDragStart }
171
+ onDragEnd = { onImageDragEnd }
172
+ onMouseDown = { onImageMouseDown }
173
+ onMouseUp = { onImageMouseUp }
174
+ >
175
+ < img
176
+ src = { imageUrl }
177
+ alt = { image . name }
178
+ className = "w-full h-full object-cover"
179
+ loading = "lazy"
180
+ />
181
+
182
+ { /* Action menu */ }
183
+ { ! isRenaming && (
184
+ < div className = "absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity" >
185
+ < DropdownMenu open = { dropdownOpen } onOpenChange = { setDropdownOpen } >
186
+ < DropdownMenuTrigger asChild >
187
+ < Button
188
+ size = "icon"
189
+ variant = "secondary"
190
+ className = "h-6 w-6 bg-background-secondary/90 hover:bg-background-onlook"
191
+ onClick = { ( e ) => {
192
+ e . preventDefault ( ) ;
193
+ e . stopPropagation ( ) ;
194
+ } }
195
+ >
196
+ < Icons . DotsHorizontal className = "h-3 w-3" />
197
+ </ Button >
198
+ </ DropdownMenuTrigger >
199
+ < DropdownMenuContent align = "end" className = "w-40" >
200
+ < DropdownMenuItem
201
+ onClick = { ( e ) => {
202
+ e . preventDefault ( ) ;
203
+ e . stopPropagation ( ) ;
204
+ handleAddToChat ( ) ;
205
+ } }
206
+ className = "flex items-center gap-2"
207
+ >
208
+ < Icons . Plus className = "h-3 w-3" />
209
+ Add to Chat
210
+ </ DropdownMenuItem >
211
+ < DropdownMenuItem
212
+ onClick = { ( e ) => {
213
+ e . preventDefault ( ) ;
214
+ e . stopPropagation ( ) ;
215
+ setIsRenaming ( true ) ;
216
+ } }
217
+ className = "flex items-center gap-2"
218
+ >
219
+ < Icons . Edit className = "h-3 w-3" />
220
+ Rename
221
+ </ DropdownMenuItem >
222
+ < DropdownMenuItem
223
+ onClick = { ( e ) => {
224
+ e . preventDefault ( ) ;
225
+ e . stopPropagation ( ) ;
226
+ setShowDeleteDialog ( true ) ;
227
+ } }
228
+ className = "flex items-center gap-2 text-red-500 hover:text-red-600 focus:text-red-600"
229
+ >
230
+ < Icons . Trash className = "h-3 w-3" />
231
+ Delete
232
+ </ DropdownMenuItem >
233
+ </ DropdownMenuContent >
234
+ </ DropdownMenu >
235
+ </ div >
236
+ ) }
237
+ </ div >
238
+
239
+ { /* Name section with rename functionality */ }
240
+ < div className = "mt-1 px-1" >
241
+ { isRenaming ? (
242
+ < Input
243
+ value = { newName }
244
+ onChange = { ( e ) => setNewName ( e . target . value ) }
245
+ onKeyDown = { handleKeyDown }
246
+ onBlur = { ( ) => void handleRename ( ) }
247
+ className = "h-6 text-xs p-1 border-0 bg-transparent focus-visible:ring-1 focus-visible:ring-ring"
248
+ autoFocus
249
+ onClick = { ( e ) => e . stopPropagation ( ) }
250
+ />
251
+ ) : (
252
+ < div className = "text-xs text-foreground-primary truncate" title = { image . name } >
253
+ { image . name }
254
+ </ div >
255
+ ) }
107
256
</ div >
257
+
258
+ { /* Delete confirmation dialog */ }
259
+ < AlertDialog open = { showDeleteDialog } onOpenChange = { setShowDeleteDialog } >
260
+ < AlertDialogContent >
261
+ < AlertDialogHeader >
262
+ < AlertDialogTitle > Delete Image</ AlertDialogTitle >
263
+ < AlertDialogDescription >
264
+ Are you sure you want to delete { image . name } ? This action cannot be undone.
265
+ </ AlertDialogDescription >
266
+ </ AlertDialogHeader >
267
+ < AlertDialogFooter >
268
+ < AlertDialogCancel > Cancel</ AlertDialogCancel >
269
+ < AlertDialogAction
270
+ onClick = { ( ) => void handleDelete ( ) }
271
+ className = "bg-destructive text-white hover:bg-destructive/90"
272
+ >
273
+ Delete
274
+ </ AlertDialogAction >
275
+ </ AlertDialogFooter >
276
+ </ AlertDialogContent >
277
+ </ AlertDialog >
108
278
</ div >
109
279
) ;
110
280
} ;
0 commit comments