11"use client" ;
22
3- import CheckCircleIcon from "@mui/icons-material/CheckCircle" ;
43import CloudUploadIcon from "@mui/icons-material/CloudUpload" ;
5- import DeleteIcon from "@mui/icons-material/Delete" ;
6- import ErrorIcon from "@mui/icons-material/Error" ;
7- import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile" ;
8- import {
9- Box ,
10- Button ,
11- Chip ,
12- IconButton ,
13- LinearProgress ,
14- List ,
15- ListItem ,
16- ListItemIcon ,
17- ListItemText ,
18- Paper ,
19- Typography ,
20- } from "@mui/material" ;
21- import { formatBytes } from "@pelicanplatform/web-client" ;
4+ import { Box , Paper , Typography } from "@mui/material" ;
225import { DragEvent , Ref , useCallback , useImperativeHandle , useRef , useState } from "react" ;
236
24- interface UploadFile {
25- file : File ;
26- status : "pending" | "uploading" | "success" | "error" ;
27- progress : number ;
28- error ?: string ;
29- }
30-
317export interface ObjectUploadRef {
328 dragHandlers : {
339 onDragEnter : ( e : DragEvent ) => void ;
@@ -40,8 +16,8 @@ export interface ObjectUploadRef {
4016interface ObjectUploadProps {
4117 /** Whether upload functionality is enabled */
4218 disabled ?: boolean ;
43- /** Callback when files should be uploaded */
44- onUpload ?: ( files : File [ ] ) => Promise < void > ;
19+ /** Callback when files are dropped/selected */
20+ onUpload ?: ( files : File [ ] ) => void ;
4521 /** Current object path for context */
4622 currentPath ?: string ;
4723 /** Ref to expose drag handlers */
@@ -50,8 +26,6 @@ interface ObjectUploadProps {
5026
5127const ObjectUpload = ( { disabled = false , onUpload, currentPath, refs } : ObjectUploadProps ) => {
5228 const [ isDragging , setIsDragging ] = useState ( false ) ;
53- const [ files , setFiles ] = useState < UploadFile [ ] > ( [ ] ) ;
54- const fileInputRef = useRef < HTMLInputElement > ( null ) ;
5529 const dragCounterRef = useRef ( 0 ) ;
5630
5731 const handleDragEnter = useCallback ( ( e : DragEvent ) => {
@@ -87,16 +61,11 @@ const ObjectUpload = ({ disabled = false, onUpload, currentPath, refs }: ObjectU
8761 if ( disabled ) return ;
8862
8963 const droppedFiles = Array . from ( e . dataTransfer . files ) ;
90- if ( droppedFiles . length > 0 ) {
91- const newFiles : UploadFile [ ] = droppedFiles . map ( ( file ) => ( {
92- file,
93- status : "pending" ,
94- progress : 0 ,
95- } ) ) ;
96- setFiles ( ( prev ) => [ ...prev , ...newFiles ] ) ;
64+ if ( droppedFiles . length > 0 && onUpload ) {
65+ onUpload ( droppedFiles ) ;
9766 }
9867 } ,
99- [ disabled ]
68+ [ disabled , onUpload ]
10069 ) ;
10170
10271 // expose drag handlers via ref
@@ -113,97 +82,6 @@ const ObjectUpload = ({ disabled = false, onUpload, currentPath, refs }: ObjectU
11382 [ handleDragEnter , handleDragOver , handleDragLeave , handleDrop ]
11483 ) ;
11584
116- const handleFileSelect = useCallback (
117- ( e : React . ChangeEvent < HTMLInputElement > ) => {
118- if ( disabled ) return ;
119- const selectedFiles = e . target . files ? Array . from ( e . target . files ) : [ ] ;
120- if ( selectedFiles . length > 0 ) {
121- const newFiles : UploadFile [ ] = selectedFiles . map ( ( file ) => ( {
122- file,
123- status : "pending" ,
124- progress : 0 ,
125- } ) ) ;
126- setFiles ( ( prev ) => [ ...prev , ...newFiles ] ) ;
127- }
128- // reset input value to allow selecting the same file again
129- if ( fileInputRef . current ) {
130- fileInputRef . current . value = "" ;
131- }
132- } ,
133- [ disabled ]
134- ) ;
135-
136- const handleRemoveFile = useCallback ( ( index : number ) => {
137- setFiles ( ( prev ) => prev . filter ( ( _ , i ) => i !== index ) ) ;
138- } , [ ] ) ;
139-
140- const handleUploadClick = useCallback ( ( ) => {
141- if ( fileInputRef . current ) {
142- fileInputRef . current . click ( ) ;
143- }
144- } , [ ] ) ;
145-
146- const handleStartUpload = useCallback ( async ( ) => {
147- if ( ! onUpload ) {
148- console . warn ( "No upload handler provided" ) ;
149- return ;
150- }
151-
152- const pendingFiles = files . filter ( ( f ) => f . status === "pending" ) ;
153- if ( pendingFiles . length === 0 ) return ;
154-
155- // update all pending files to uploading
156- setFiles ( ( prev ) =>
157- prev . map ( ( f ) => ( f . status === "pending" ? { ...f , status : "uploading" as const , progress : 0 } : f ) )
158- ) ;
159-
160- // simulate upload progress (in real implementation, you'd track actual progress)
161- const uploadPromises = pendingFiles . map ( async ( uploadFile ) => {
162- try {
163- // Simulate progress updates
164- const progressInterval = setInterval ( ( ) => {
165- setFiles ( ( prev ) =>
166- prev . map ( ( f ) =>
167- f . file === uploadFile . file && f . status === "uploading"
168- ? { ...f , progress : Math . min ( f . progress + 10 , 90 ) }
169- : f
170- )
171- ) ;
172- } , 200 ) ;
173-
174- // Actual upload would happen here
175- await onUpload ( [ uploadFile . file ] ) ;
176-
177- clearInterval ( progressInterval ) ;
178-
179- // Mark as success
180- setFiles ( ( prev ) =>
181- prev . map ( ( f ) =>
182- f . file === uploadFile . file ? { ...f , status : "success" as const , progress : 100 } : f
183- )
184- ) ;
185- } catch ( error ) {
186- // mark as error
187- setFiles ( ( prev ) =>
188- prev . map ( ( f ) =>
189- f . file === uploadFile . file
190- ? {
191- ...f ,
192- status : "error" as const ,
193- error : error instanceof Error ? error . message : "Upload failed" ,
194- }
195- : f
196- )
197- ) ;
198- }
199- } ) ;
200-
201- await Promise . all ( uploadPromises ) ;
202- } , [ files , onUpload ] ) ;
203-
204- const hasPendingFiles = files . some ( ( f ) => f . status === "pending" ) ;
205- const hasUploadingFiles = files . some ( ( f ) => f . status === "uploading" ) ;
206-
20785 return (
20886 < >
20987 { /* Drag overlay - renders at top level */ }
@@ -240,89 +118,6 @@ const ObjectUpload = ({ disabled = false, onUpload, currentPath, refs }: ObjectU
240118 </ Box >
241119 </ Paper >
242120 ) }
243-
244- { /* Upload controls */ }
245- < Box sx = { { mb : 2 } } >
246- < input
247- ref = { fileInputRef }
248- type = "file"
249- multiple
250- style = { { display : "none" } }
251- onChange = { handleFileSelect }
252- disabled = { disabled }
253- />
254- < Box sx = { { display : "flex" , gap : 2 , alignItems : "center" } } >
255- < Button
256- variant = "contained"
257- startIcon = { < CloudUploadIcon /> }
258- onClick = { handleUploadClick }
259- disabled = { disabled }
260- >
261- Select Files
262- </ Button >
263- { hasPendingFiles && (
264- < Button
265- variant = "contained"
266- color = "success"
267- onClick = { handleStartUpload }
268- disabled = { disabled || hasUploadingFiles }
269- >
270- Upload { files . filter ( ( f ) => f . status === "pending" ) . length } File(s)
271- </ Button >
272- ) }
273- { disabled && < Chip label = "Read-only" color = "warning" size = "small" /> }
274- </ Box >
275- </ Box >
276-
277- { /* File list */ }
278- { files . length > 0 && (
279- < Paper variant = "outlined" sx = { { maxHeight : 400 , overflow : "auto" } } >
280- < List >
281- { files . map ( ( uploadFile , index ) => (
282- < ListItem
283- key = { index }
284- secondaryAction = {
285- uploadFile . status === "pending" || uploadFile . status === "error" ? (
286- < IconButton edge = "end" onClick = { ( ) => handleRemoveFile ( index ) } >
287- < DeleteIcon />
288- </ IconButton >
289- ) : null
290- }
291- >
292- < ListItemIcon >
293- { uploadFile . status === "success" ? (
294- < CheckCircleIcon color = "success" />
295- ) : uploadFile . status === "error" ? (
296- < ErrorIcon color = "error" />
297- ) : (
298- < InsertDriveFileIcon />
299- ) }
300- </ ListItemIcon >
301- < ListItemText
302- primary = { uploadFile . file . name }
303- secondary = {
304- < Box >
305- < Typography variant = "caption" component = "span" >
306- { formatBytes ( uploadFile . file . size ) }
307- </ Typography >
308- { uploadFile . status === "error" && uploadFile . error && (
309- < Typography variant = "caption" color = "error" component = "div" >
310- { uploadFile . error }
311- </ Typography >
312- ) }
313- { uploadFile . status === "uploading" && (
314- < Box sx = { { width : "100%" , mt : 1 } } >
315- < LinearProgress variant = "determinate" value = { uploadFile . progress } />
316- </ Box >
317- ) }
318- </ Box >
319- }
320- />
321- </ ListItem >
322- ) ) }
323- </ List >
324- </ Paper >
325- ) }
326121 </ >
327122 ) ;
328123} ;
0 commit comments