11import React , { useState , useEffect } from "react" ;
22import { Modal , List , Breadcrumb , Button , Spin , message } from "antd" ;
3- import { FolderFilled , FileOutlined , ArrowUpOutlined } from "@ant-design/icons" ;
3+ import {
4+ FolderFilled ,
5+ FileOutlined ,
6+ ArrowLeftOutlined ,
7+ UploadOutlined ,
8+ } from "@ant-design/icons" ;
49import { apiClient } from "../api" ;
510
11+ const HIDDEN_SYSTEM_FILES = new Set ( [ "workflow_preference.json" ] ) ;
12+ const IMAGE_EXTENSIONS = new Set ( [
13+ ".png" ,
14+ ".jpg" ,
15+ ".jpeg" ,
16+ ".gif" ,
17+ ".bmp" ,
18+ ".tif" ,
19+ ".tiff" ,
20+ ".webp" ,
21+ ] ) ;
22+
623const FilePickerModal = ( {
724 visible,
825 onCancel,
@@ -13,47 +30,16 @@ const FilePickerModal = ({
1330 const [ currentPath , setCurrentPath ] = useState ( "root" ) ;
1431 const [ items , setItems ] = useState ( [ ] ) ;
1532 const [ loading , setLoading ] = useState ( false ) ;
16-
17- useEffect ( ( ) => {
18- if ( visible ) {
19- fetchFiles ( ) ;
20- }
21- } , [ visible , currentPath ] ) ;
22-
23- const fetchFiles = async ( ) => {
24- setLoading ( true ) ;
25- try {
26- const res = await apiClient . get ( "/files" ) ;
27- const allFiles = res . data ;
28-
29- // Filter items for current path
30- const currentItems = allFiles . filter ( ( f ) => {
31- if ( currentPath === "root" ) {
32- return f . path === "root" || ! f . path ;
33- }
34- return f . path === currentPath ;
35- } ) ;
36-
37- // Sort: Folders first, then files
38- currentItems . sort ( ( a , b ) => {
39- if ( a . is_folder === b . is_folder ) return a . name . localeCompare ( b . name ) ;
40- return a . is_folder ? - 1 : 1 ;
41- } ) ;
42-
43- setItems ( currentItems ) ;
44- } catch ( error ) {
45- console . error ( "Failed to load files:" , error ) ;
46- message . error ( "Failed to load files" ) ;
47- } finally {
48- setLoading ( false ) ;
49- }
50- } ;
33+ const [ previewStatus , setPreviewStatus ] = useState ( { } ) ;
34+ const previewBaseUrl =
35+ apiClient . defaults . baseURL || "http://localhost:4242" ;
5136
5237 // Refactored fetch to get all files once
5338 const [ allData , setAllData ] = useState ( [ ] ) ;
5439
5540 useEffect ( ( ) => {
5641 if ( visible ) {
42+ setCurrentPath ( "root" ) ;
5743 loadAllData ( ) ;
5844 }
5945 } , [ visible ] ) ;
@@ -73,6 +59,7 @@ const FilePickerModal = ({
7359 // Derive items for current view
7460 useEffect ( ( ) => {
7561 const filtered = allData . filter ( ( f ) => {
62+ if ( ! f . is_folder && HIDDEN_SYSTEM_FILES . has ( f . name ) ) return false ;
7663 if ( currentPath === "root" ) return f . path === "root" || ! f . path ;
7764 return String ( f . path ) === currentPath ;
7865 } ) ;
@@ -108,7 +95,7 @@ const FilePickerModal = ({
10895 break ;
10996 }
11097 }
111- parts . unshift ( { id : "root" , name : "Home " } ) ;
98+ parts . unshift ( { id : "root" , name : "Projects " } ) ;
11299 return parts ;
113100 } ;
114101
@@ -147,6 +134,56 @@ const FilePickerModal = ({
147134 }
148135 } ;
149136
137+ const handleUploadFromLocal = ( ) => {
138+ const input = document . createElement ( "input" ) ;
139+ input . type = "file" ;
140+ input . multiple = true ;
141+ input . onchange = async ( event ) => {
142+ const selectedFiles = Array . from ( event . target . files || [ ] ) ;
143+ if ( ! selectedFiles . length ) return ;
144+ let uploaded = 0 ;
145+ for ( const file of selectedFiles ) {
146+ const form = new FormData ( ) ;
147+ form . append ( "file" , file ) ;
148+ form . append ( "path" , currentPath ) ;
149+ try {
150+ await apiClient . post ( "/files/upload" , form , {
151+ headers : { "Content-Type" : "multipart/form-data" } ,
152+ } ) ;
153+ uploaded += 1 ;
154+ } catch ( error ) {
155+ console . error ( "Failed to upload file from picker:" , error ) ;
156+ message . error ( `Failed to upload ${ file . name } ` ) ;
157+ }
158+ }
159+ if ( uploaded > 0 ) {
160+ message . success (
161+ `Uploaded ${ uploaded } file${ uploaded > 1 ? "s" : "" } to this folder` ,
162+ ) ;
163+ await loadAllData ( ) ;
164+ }
165+ } ;
166+ input . click ( ) ;
167+ } ;
168+
169+ const isImageFile = ( item ) => {
170+ if ( ! item || item . is_folder ) return false ;
171+ if ( item . type && item . type . startsWith ( "image/" ) ) return true ;
172+ const ext = `.${ String ( item . name || "" ) . split ( "." ) . pop ( ) } ` . toLowerCase ( ) ;
173+ return IMAGE_EXTENSIONS . has ( ext ) ;
174+ } ;
175+
176+ const getPreviewUrl = ( item ) =>
177+ `${ previewBaseUrl } /files/preview/${ item . id } ` ;
178+
179+ const markPreviewLoaded = ( id ) => {
180+ setPreviewStatus ( ( prev ) => ( { ...prev , [ id ] : "loaded" } ) ) ;
181+ } ;
182+
183+ const markPreviewError = ( id ) => {
184+ setPreviewStatus ( ( prev ) => ( { ...prev , [ id ] : "error" } ) ) ;
185+ } ;
186+
150187 return (
151188 < Modal
152189 title = { title }
@@ -182,7 +219,7 @@ const FilePickerModal = ({
182219 } }
183220 >
184221 < Button
185- icon = { < ArrowUpOutlined /> }
222+ icon = { < ArrowLeftOutlined /> }
186223 onClick = { goUp }
187224 disabled = { currentPath === "root" }
188225 style = { { marginRight : "12px" } }
@@ -194,6 +231,13 @@ const FilePickerModal = ({
194231 </ Breadcrumb . Item >
195232 ) ) }
196233 </ Breadcrumb >
234+ < Button
235+ icon = { < UploadOutlined /> }
236+ onClick = { handleUploadFromLocal }
237+ style = { { marginLeft : "auto" } }
238+ >
239+ Upload from Local
240+ </ Button >
197241 </ div >
198242
199243 < div style = { { height : "400px" , overflow : "auto" } } >
@@ -263,6 +307,44 @@ const FilePickerModal = ({
263307 < FolderFilled
264308 style = { { color : "#1890ff" , fontSize : "20px" } }
265309 />
310+ ) : isImageFile ( item ) ? (
311+ < div
312+ style = { {
313+ width : 32 ,
314+ height : 32 ,
315+ borderRadius : 4 ,
316+ border : "1px solid #f0f0f0" ,
317+ display : "flex" ,
318+ alignItems : "center" ,
319+ justifyContent : "center" ,
320+ overflow : "hidden" ,
321+ position : "relative" ,
322+ background : "#fafafa" ,
323+ } }
324+ >
325+ { previewStatus [ item . id ] !== "loaded" && (
326+ < Spin size = "small" />
327+ ) }
328+ { previewStatus [ item . id ] !== "error" && (
329+ < img
330+ src = { getPreviewUrl ( item ) }
331+ alt = { item . name }
332+ loading = "lazy"
333+ onLoad = { ( ) => markPreviewLoaded ( item . id ) }
334+ onError = { ( ) => markPreviewError ( item . id ) }
335+ style = { {
336+ position : "absolute" ,
337+ inset : 0 ,
338+ width : "100%" ,
339+ height : "100%" ,
340+ objectFit : "cover" ,
341+ opacity :
342+ previewStatus [ item . id ] === "loaded" ? 1 : 0 ,
343+ transition : "opacity 0.2s ease" ,
344+ } }
345+ />
346+ ) }
347+ </ div >
266348 ) : (
267349 < FileOutlined style = { { fontSize : "20px" } } />
268350 )
0 commit comments