@@ -27,6 +27,7 @@ interface FileBrowserProps {
2727 initialSelectedFile ?: string
2828 onDirectoryLoad ?: ( info : { workspaceRoot ?: string ; currentPath : string } ) => void
2929 onPreviewStateChange ?: ( isOpen : boolean ) => void
30+ allowNavigateAboveBase ?: boolean
3031}
3132
3233interface UploadItem {
@@ -122,7 +123,7 @@ function getUploadItemsFromFileList(fileList: FileList): UploadItem[] {
122123 return items
123124}
124125
125- export const FileBrowser = forwardRef < FileBrowserHandle , FileBrowserProps > ( function FileBrowser ( { basePath = '' , onFileSelect, embedded = false , initialSelectedFile, onDirectoryLoad, onPreviewStateChange } , ref ) {
126+ export const FileBrowser = forwardRef < FileBrowserHandle , FileBrowserProps > ( function FileBrowser ( { basePath = '' , onFileSelect, embedded = false , initialSelectedFile, onDirectoryLoad, onPreviewStateChange, allowNavigateAboveBase = false } , ref ) {
126127 const [ currentPath , setCurrentPath ] = useState ( basePath )
127128 const [ files , setFiles ] = useState < FileInfo | null > ( null )
128129 const [ selectedFile , setSelectedFile ] = useState < FileInfo | null > ( null )
@@ -137,7 +138,7 @@ export const FileBrowser = forwardRef<FileBrowserHandle, FileBrowserProps>(funct
137138 const uploadCancelledRef = useRef ( false )
138139 const isMobile = useMobile ( )
139140
140- const { data : initialFileData , error : initialFileError } = useFile ( initialSelectedFile )
141+ const { data : initialFileData , error : initialFileError } = useFile ( initialSelectedFile )
141142
142143useEffect ( ( ) => {
143144 if ( initialFileData ) {
@@ -155,12 +156,25 @@ useEffect(() => {
155156 }
156157} , [ initialFileError ] )
157158
159+ const getFileApiUrl = useCallback ( ( path : string ) => {
160+ if ( path . includes ( '..' ) ) {
161+ return `${ API_BASE_URL } /api/files/?path=${ encodeURIComponent ( path ) } `
162+ }
163+
164+ const encodedPath = path
165+ . split ( '/' )
166+ . map ( segment => encodeURIComponent ( segment ) )
167+ . join ( '/' )
168+
169+ return `${ API_BASE_URL } /api/files/${ encodedPath } `
170+ } , [ ] )
171+
158172 const loadFiles = useCallback ( async ( path : string ) => {
159173 setLoading ( true )
160174 setError ( null )
161175
162176 try {
163- const response = await fetch ( ` ${ API_BASE_URL } /api/files/ ${ path } ` )
177+ const response = await fetch ( getFileApiUrl ( path ) )
164178 if ( ! response . ok ) {
165179 throw new Error ( `Failed to load files: ${ response . statusText } ` )
166180 }
@@ -174,28 +188,64 @@ useEffect(() => {
174188 } finally {
175189 setLoading ( false )
176190 }
177- } , [ onDirectoryLoad ] )
191+ } , [ getFileApiUrl , onDirectoryLoad ] )
192+
193+ const normalizePath = useCallback ( ( path : string ) => {
194+ const normalized = path
195+ . trim ( )
196+ . replace ( / \\ / g, '/' )
197+ . replace ( / \/ + / g, '/' )
198+ . replace ( / \/ + $ / , '' )
199+
200+ if ( normalized === '.' || normalized === './' ) {
201+ return ''
202+ }
203+
204+ if ( normalized . startsWith ( './' ) ) {
205+ return normalized . slice ( 2 )
206+ }
207+
208+ return normalized
209+ } , [ ] )
210+
211+ const getPathParts = useCallback ( ( path : string ) => normalizePath ( path ) . split ( '/' ) . filter ( Boolean ) , [ normalizePath ] )
212+
213+ const canNavigateUp = useCallback ( ( ) => {
214+ if ( allowNavigateAboveBase && normalizePath ( currentPath ) === '' ) {
215+ return true
216+ }
217+
218+ if ( normalizePath ( currentPath ) === '..' ) {
219+ return false
220+ }
221+
222+ const pathParts = getPathParts ( currentPath )
223+ if ( allowNavigateAboveBase ) {
224+ return pathParts . length > 0
225+ }
178226
179- const getPathParts = useCallback ( ( path : string ) => path . split ( '/' ) . filter ( Boolean ) , [ ] )
227+ return pathParts . length > 0 && normalizePath ( currentPath ) !== normalizePath ( basePath )
228+ } , [ allowNavigateAboveBase , basePath , currentPath , getPathParts , normalizePath ] )
180229
181230 const goToParentDirectory = useCallback ( ( ) => {
231+ if ( allowNavigateAboveBase && normalizePath ( currentPath ) === '' ) {
232+ loadFiles ( '..' )
233+ return
234+ }
235+
182236 const pathParts = getPathParts ( currentPath )
183237 if ( pathParts . length > 0 ) {
184238 pathParts . pop ( )
185239 const parentPath = pathParts . join ( '/' )
186- loadFiles ( parentPath || basePath )
240+ loadFiles ( allowNavigateAboveBase ? parentPath : parentPath || basePath )
187241 }
188- } , [ currentPath , basePath , loadFiles , getPathParts ] )
242+ } , [ allowNavigateAboveBase , currentPath , basePath , loadFiles , getPathParts , normalizePath ] )
189243
190244 useImperativeHandle ( ref , ( ) => ( {
191245 goBack : goToParentDirectory ,
192- canGoBack : ( ) => {
193- const pathParts = getPathParts ( currentPath )
194- const joinedPath = pathParts . join ( '/' )
195- return pathParts . length > 0 && joinedPath !== basePath
196- } ,
246+ canGoBack : canNavigateUp ,
197247 getCurrentPath : ( ) => currentPath ,
198- } ) , [ currentPath , basePath , goToParentDirectory , getPathParts ] )
248+ } ) , [ currentPath , goToParentDirectory , canNavigateUp ] )
199249
200250 const handleFileSelect = useCallback ( async ( file : FileInfo ) => {
201251 if ( file . isDirectory ) {
@@ -206,7 +256,7 @@ useEffect(() => {
206256 // Fetch the full file content when selecting a file
207257 setLoading ( true )
208258 try {
209- const response = await fetch ( ` ${ API_BASE_URL } /api/files/ ${ file . path } ` )
259+ const response = await fetch ( getFileApiUrl ( file . path ) )
210260 if ( ! response . ok ) {
211261 throw new Error ( `Failed to load file: ${ response . statusText } ` )
212262 }
@@ -226,7 +276,7 @@ useEffect(() => {
226276 } finally {
227277 setLoading ( false )
228278 }
229- } , [ onFileSelect , isMobile , onPreviewStateChange ] )
279+ } , [ getFileApiUrl , onFileSelect , isMobile , onPreviewStateChange ] )
230280
231281 const handleCloseModal = useCallback ( ( ) => {
232282 setIsPreviewModalOpen ( false )
@@ -248,7 +298,7 @@ useEffect(() => {
248298 formData . append ( 'relativePath' , item . relativePath )
249299
250300 try {
251- const response = await fetch ( ` ${ API_BASE_URL } /api/files/ ${ currentPath } ` , {
301+ const response = await fetch ( getFileApiUrl ( currentPath ) , {
252302 method : 'POST' ,
253303 body : formData ,
254304 } )
@@ -262,7 +312,7 @@ useEffect(() => {
262312 } catch ( err ) {
263313 return err instanceof Error ? err . message : 'Upload failed'
264314 }
265- } , [ currentPath ] )
315+ } , [ currentPath , getFileApiUrl ] )
266316
267317 const handleUploadItems = useCallback ( async ( items : UploadItem [ ] ) => {
268318 if ( items . length === 0 ) return
@@ -318,7 +368,7 @@ useEffect(() => {
318368
319369 const handleCreateFile = useCallback ( async ( name : string , type : 'file' | 'folder' ) => {
320370 try {
321- const response = await fetch ( ` ${ API_BASE_URL } /api/files/ ${ currentPath } / ${ name } ` , {
371+ const response = await fetch ( getFileApiUrl ( [ currentPath , name ] . filter ( Boolean ) . join ( '/' ) ) , {
322372 method : 'PUT' ,
323373 headers : { 'Content-Type' : 'application/json' } ,
324374 body : JSON . stringify ( { type, content : type === 'file' ? '' : undefined } ) ,
@@ -332,11 +382,11 @@ useEffect(() => {
332382 } catch ( err ) {
333383 setError ( err instanceof Error ? err . message : 'Create failed' )
334384 }
335- } , [ currentPath , loadFiles ] )
385+ } , [ currentPath , getFileApiUrl , loadFiles ] )
336386
337387 const handleDelete = useCallback ( async ( path : string ) => {
338388 try {
339- const response = await fetch ( ` ${ API_BASE_URL } /api/files/ ${ path } ` , {
389+ const response = await fetch ( getFileApiUrl ( path ) , {
340390 method : 'DELETE' ,
341391 } )
342392
@@ -349,11 +399,11 @@ useEffect(() => {
349399 } catch ( err ) {
350400 setError ( err instanceof Error ? err . message : 'Delete failed' )
351401 }
352- } , [ currentPath , loadFiles ] )
402+ } , [ currentPath , getFileApiUrl , loadFiles ] )
353403
354404 const handleRename = useCallback ( async ( oldPath : string , newPath : string ) => {
355405 try {
356- const response = await fetch ( ` ${ API_BASE_URL } /api/files/ ${ oldPath } ` , {
406+ const response = await fetch ( getFileApiUrl ( oldPath ) , {
357407 method : 'PATCH' ,
358408 headers : { 'Content-Type' : 'application/json' } ,
359409 body : JSON . stringify ( { newPath } ) ,
@@ -367,7 +417,7 @@ useEffect(() => {
367417 } catch ( err ) {
368418 setError ( err instanceof Error ? err . message : 'Rename failed' )
369419 }
370- } , [ currentPath , loadFiles ] )
420+ } , [ currentPath , getFileApiUrl , loadFiles ] )
371421
372422 const handleDragEnter = ( e : React . DragEvent ) => {
373423 e . preventDefault ( )
@@ -427,6 +477,10 @@ useEffect(() => {
427477 return ( ) => document . removeEventListener ( 'keydown' , handleEscape )
428478 } , [ isPreviewModalOpen , handleCloseModal ] )
429479
480+ const showNavigateUp = allowNavigateAboveBase && normalizePath ( currentPath ) !== '..'
481+ ? true
482+ : canNavigateUp ( )
483+
430484 const filteredFiles = files ?. children ?. filter ( file =>
431485 file . name . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) )
432486 )
@@ -557,6 +611,7 @@ useEffect(() => {
557611 currentPath = { currentPath }
558612 basePath = { basePath }
559613 onNavigateUp = { goToParentDirectory }
614+ canNavigateUp = { showNavigateUp }
560615 />
561616 ) }
562617 </ div >
@@ -657,6 +712,7 @@ useEffect(() => {
657712 currentPath = { currentPath }
658713 basePath = { basePath }
659714 onNavigateUp = { goToParentDirectory }
715+ canNavigateUp = { showNavigateUp }
660716 />
661717 </ div >
662718 ) }
0 commit comments