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 ( [
12+ "workflow_preference.json" ,
13+ ".ds_store" ,
14+ "thumbs.db" ,
15+ ] ) ;
16+ const IMAGE_EXTENSIONS = new Set ( [
17+ ".png" ,
18+ ".jpg" ,
19+ ".jpeg" ,
20+ ".gif" ,
21+ ".bmp" ,
22+ ".tif" ,
23+ ".tiff" ,
24+ ".webp" ,
25+ ] ) ;
26+
627const FilePickerModal = ( {
728 visible,
829 onCancel,
@@ -13,47 +34,18 @@ const FilePickerModal = ({
1334 const [ currentPath , setCurrentPath ] = useState ( "root" ) ;
1435 const [ items , setItems ] = useState ( [ ] ) ;
1536 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- } ;
37+ const [ previewStatus , setPreviewStatus ] = useState ( { } ) ;
38+ const [ onlyImages , setOnlyImages ] = useState ( false ) ;
39+ const previewBaseUrl =
40+ apiClient . defaults . baseURL || "http://localhost:4242" ;
5141
5242 // Refactored fetch to get all files once
5343 const [ allData , setAllData ] = useState ( [ ] ) ;
5444
5545 useEffect ( ( ) => {
5646 if ( visible ) {
47+ setCurrentPath ( "root" ) ;
48+ setOnlyImages ( false ) ;
5749 loadAllData ( ) ;
5850 }
5951 } , [ visible ] ) ;
@@ -73,17 +65,23 @@ const FilePickerModal = ({
7365 // Derive items for current view
7466 useEffect ( ( ) => {
7567 const filtered = allData . filter ( ( f ) => {
68+ const nameLower = String ( f . name || "" ) . toLowerCase ( ) ;
69+ if ( ! f . is_folder && HIDDEN_SYSTEM_FILES . has ( nameLower ) ) return false ;
7670 if ( currentPath === "root" ) return f . path === "root" || ! f . path ;
7771 return String ( f . path ) === currentPath ;
7872 } ) ;
7973
80- filtered . sort ( ( a , b ) => {
74+ const filteredByType = onlyImages
75+ ? filtered . filter ( ( f ) => f . is_folder || isImageFile ( f ) )
76+ : filtered ;
77+
78+ filteredByType . sort ( ( a , b ) => {
8179 if ( a . is_folder === b . is_folder ) return a . name . localeCompare ( b . name ) ;
8280 return a . is_folder ? - 1 : 1 ;
8381 } ) ;
8482
85- setItems ( filtered ) ;
86- } , [ currentPath , allData ] ) ;
83+ setItems ( filteredByType ) ;
84+ } , [ currentPath , allData , onlyImages ] ) ;
8785
8886 const getParentPath = ( ) => {
8987 if ( currentPath === "root" ) return null ;
@@ -108,7 +106,7 @@ const FilePickerModal = ({
108106 break ;
109107 }
110108 }
111- parts . unshift ( { id : "root" , name : "Home " } ) ;
109+ parts . unshift ( { id : "root" , name : "Projects " } ) ;
112110 return parts ;
113111 } ;
114112
@@ -147,6 +145,56 @@ const FilePickerModal = ({
147145 }
148146 } ;
149147
148+ const handleUploadFromLocal = ( ) => {
149+ const input = document . createElement ( "input" ) ;
150+ input . type = "file" ;
151+ input . multiple = true ;
152+ input . onchange = async ( event ) => {
153+ const selectedFiles = Array . from ( event . target . files || [ ] ) ;
154+ if ( ! selectedFiles . length ) return ;
155+ let uploaded = 0 ;
156+ for ( const file of selectedFiles ) {
157+ const form = new FormData ( ) ;
158+ form . append ( "file" , file ) ;
159+ form . append ( "path" , currentPath ) ;
160+ try {
161+ await apiClient . post ( "/files/upload" , form , {
162+ headers : { "Content-Type" : "multipart/form-data" } ,
163+ } ) ;
164+ uploaded += 1 ;
165+ } catch ( error ) {
166+ console . error ( "Failed to upload file from picker:" , error ) ;
167+ message . error ( `Failed to upload ${ file . name } ` ) ;
168+ }
169+ }
170+ if ( uploaded > 0 ) {
171+ message . success (
172+ `Uploaded ${ uploaded } file${ uploaded > 1 ? "s" : "" } to this folder` ,
173+ ) ;
174+ await loadAllData ( ) ;
175+ }
176+ } ;
177+ input . click ( ) ;
178+ } ;
179+
180+ const isImageFile = ( item ) => {
181+ if ( ! item || item . is_folder ) return false ;
182+ if ( item . type && item . type . startsWith ( "image/" ) ) return true ;
183+ const ext = `.${ String ( item . name || "" ) . split ( "." ) . pop ( ) } ` . toLowerCase ( ) ;
184+ return IMAGE_EXTENSIONS . has ( ext ) ;
185+ } ;
186+
187+ const getPreviewUrl = ( item ) =>
188+ `${ previewBaseUrl } /files/preview/${ item . id } ` ;
189+
190+ const markPreviewLoaded = ( id ) => {
191+ setPreviewStatus ( ( prev ) => ( { ...prev , [ id ] : "loaded" } ) ) ;
192+ } ;
193+
194+ const markPreviewError = ( id ) => {
195+ setPreviewStatus ( ( prev ) => ( { ...prev , [ id ] : "error" } ) ) ;
196+ } ;
197+
150198 return (
151199 < Modal
152200 title = { title }
@@ -182,7 +230,7 @@ const FilePickerModal = ({
182230 } }
183231 >
184232 < Button
185- icon = { < ArrowUpOutlined /> }
233+ icon = { < ArrowLeftOutlined /> }
186234 onClick = { goUp }
187235 disabled = { currentPath === "root" }
188236 style = { { marginRight : "12px" } }
@@ -194,6 +242,20 @@ const FilePickerModal = ({
194242 </ Breadcrumb . Item >
195243 ) ) }
196244 </ Breadcrumb >
245+ < Button
246+ icon = { < UploadOutlined /> }
247+ onClick = { handleUploadFromLocal }
248+ style = { { marginLeft : "auto" } }
249+ >
250+ Upload from Local
251+ </ Button >
252+ < Button
253+ type = { onlyImages ? "primary" : "default" }
254+ onClick = { ( ) => setOnlyImages ( ( prev ) => ! prev ) }
255+ style = { { marginLeft : 8 } }
256+ >
257+ Images Only
258+ </ Button >
197259 </ div >
198260
199261 < div style = { { height : "400px" , overflow : "auto" } } >
@@ -263,11 +325,70 @@ const FilePickerModal = ({
263325 < FolderFilled
264326 style = { { color : "#1890ff" , fontSize : "20px" } }
265327 />
328+ ) : isImageFile ( item ) ? (
329+ < div
330+ style = { {
331+ width : 32 ,
332+ height : 32 ,
333+ borderRadius : 4 ,
334+ border : "1px solid #f0f0f0" ,
335+ display : "flex" ,
336+ alignItems : "center" ,
337+ justifyContent : "center" ,
338+ overflow : "hidden" ,
339+ position : "relative" ,
340+ background : "#fafafa" ,
341+ } }
342+ >
343+ { previewStatus [ item . id ] !== "loaded" && (
344+ < Spin size = "small" />
345+ ) }
346+ { previewStatus [ item . id ] !== "error" && (
347+ < img
348+ src = { getPreviewUrl ( item ) }
349+ alt = { item . name }
350+ loading = "lazy"
351+ onLoad = { ( ) => markPreviewLoaded ( item . id ) }
352+ onError = { ( ) => markPreviewError ( item . id ) }
353+ style = { {
354+ position : "absolute" ,
355+ inset : 0 ,
356+ width : "100%" ,
357+ height : "100%" ,
358+ objectFit : "cover" ,
359+ opacity :
360+ previewStatus [ item . id ] === "loaded" ? 1 : 0 ,
361+ transition : "opacity 0.2s ease" ,
362+ } }
363+ />
364+ ) }
365+ </ div >
266366 ) : (
267367 < FileOutlined style = { { fontSize : "20px" } } />
268368 )
269369 }
270- title = { item . name }
370+ title = {
371+ < span >
372+ { item . name }
373+ { item . is_folder &&
374+ ( item . path === "root" || ! item . path ) &&
375+ item . physical_path && (
376+ < span
377+ style = { {
378+ marginLeft : 8 ,
379+ padding : "2px 6px" ,
380+ fontSize : 10 ,
381+ borderRadius : 10 ,
382+ background : "#f0f5ff" ,
383+ color : "#2f54eb" ,
384+ border : "1px solid #adc6ff" ,
385+ } }
386+ >
387+ Mounted
388+ </ span >
389+ ) }
390+ </ span >
391+ }
271392 description = { item . size ? item . size : null }
272393 />
273394 </ List . Item >
0 commit comments