@@ -34,7 +34,7 @@ import remarkGfm from 'remark-gfm';
3434import { Badge } from '@/components/ui/badge' ;
3535import NextImage from 'next/image' ;
3636import { useForm } from 'react-hook-form' ;
37- import { useActionState } from 'react' ;
37+ import { useActionState , startTransition } from 'react' ;
3838import { zodResolver } from '@hookform/resolvers/zod' ;
3939import * as z from 'zod' ;
4040
@@ -104,32 +104,31 @@ function FileExplorerContent() {
104104 const newFileForm = useForm < NewFileFormValues > ( { resolver : zodResolver ( newFileFormSchema ) , defaultValues : { fileName : '' , initialContent : '' } } ) ;
105105 const newFolderForm = useForm < NewFolderFormValues > ( { resolver : zodResolver ( newFolderFormSchema ) , defaultValues : { folderName : '' } } ) ;
106106
107-
108- const forceReloadContent = useCallback ( async ( newPath ?: string ) => {
109- const pathToLoad = newPath !== undefined ? newPath : currentPath ;
107+ const loadContent = useCallback ( async ( pathToLoad : string ) => {
110108 if ( ! project || ! project . githubRepoName || authLoading || isLoadingProject ) {
111- return ;
109+ return ;
112110 }
113111 setIsLoadingPathContent ( true ) ;
114112 setError ( null ) ;
115-
113+ setFileData ( null ) ;
114+ setIsViewingFile ( false ) ;
115+
116116 try {
117- const isFileViewCandidate = ( newPath ? newPath . includes ( '.' ) : ( filePathArray . length > 0 && ( isViewingFile || filePathArray [ filePathArray . length - 1 ] . includes ( '.' ) ) ) ) && ( ! newPath || ! newPath . endsWith ( '/' ) ) ;
117+ const extension = pathToLoad . includes ( '.' ) ? getFileExtension ( pathToLoad . split ( '/' ) . pop ( ) ! ) : null ;
118+ const isFileCandidate = extension && ! pathToLoad . endsWith ( '/' ) ;
118119
119- if ( isFileViewCandidate && ( fileData || newPath ?. includes ( '.' ) ) ) {
120- const actualFilePath = newPath || fileData ! . path ;
121- const fetchedFileData = await getFileContentAction ( projectUuid , actualFilePath ) ;
120+ if ( isFileCandidate ) {
121+ const fetchedFileData = await getFileContentAction ( projectUuid , pathToLoad ) ;
122122 if ( 'content' in fetchedFileData && fetchedFileData . sha ) {
123- const extension = getFileExtension ( actualFilePath . split ( '/' ) . pop ( ) ! ) ;
124123 let fileDisplayType : 'md' | 'image' | 'html' | 'text' | 'other' = 'other' ;
125124 if ( MARKDOWN_EXTENSIONS . includes ( `.${ extension } ` ) ) fileDisplayType = 'md' ;
126125 else if ( IMAGE_EXTENSIONS . includes ( `.${ extension } ` ) ) fileDisplayType = 'image' ;
127126 else if ( extension === 'html' ) fileDisplayType = 'html' ;
128127 else if ( TEXT_EXTENSIONS . includes ( `.${ extension } ` ) || fetchedFileData . encoding === 'utf-8' || ! fetchedFileData . encoding ) fileDisplayType = 'text' ;
129128
130129 setFileData ( {
131- name : actualFilePath . split ( '/' ) . pop ( ) ! ,
132- path : actualFilePath ,
130+ name : pathToLoad . split ( '/' ) . pop ( ) ! ,
131+ path : pathToLoad ,
133132 content : fetchedFileData . content ,
134133 type : fileDisplayType ,
135134 downloadUrl : fetchedFileData . download_url ,
@@ -138,34 +137,29 @@ function FileExplorerContent() {
138137 } ) ;
139138 setEditingContent ( fetchedFileData . content ) ;
140139 setIsViewingFile ( true ) ;
141- if ( newPath ) router . push ( `/projects/${ projectUuid } /codespace/files/${ newPath } ` ) ;
142140 } else {
143- setError ( fetchedFileData . error || "Failed to refresh file content." ) ;
141+ setError ( fetchedFileData . error || "Failed to load file content." ) ;
144142 setIsViewingFile ( false ) ;
145- setFileData ( null ) ;
146143 }
147- } else {
148- setIsViewingFile ( false ) ;
149- setFileData ( null ) ;
144+ } else { // Is a directory or root
150145 const dirData = await getRepoContentsAction ( projectUuid , pathToLoad ) ;
151146 if ( Array . isArray ( dirData ) ) {
152147 setContents ( dirData . sort ( ( a , b ) => {
153148 if ( a . type === 'dir' && b . type !== 'dir' ) return - 1 ;
154149 if ( a . type !== 'dir' && b . type === 'dir' ) return 1 ;
155150 return a . name . localeCompare ( b . name ) ;
156151 } ) ) ;
157- if ( newPath !== undefined && newPath !== currentPath ) router . push ( `/projects/${ projectUuid } /codespace/files${ newPath ? '/' + newPath : '' } ` ) ;
158152 } else {
159153 setError ( dirData . error || "Failed to fetch repository contents." ) ;
160154 }
161155 }
162156 } catch ( e : any ) {
163- setError ( e . message || 'An unexpected error occurred while reloading content.' ) ;
157+ setError ( e . message || 'An unexpected error occurred while loading content.' ) ;
164158 toast ( { variant : 'destructive' , title : 'Error' , description : e . message } ) ;
165159 } finally {
166160 setIsLoadingPathContent ( false ) ;
167161 }
168- } , [ project , currentPath , projectUuid , authLoading , isLoadingProject , toast , filePathArray , isViewingFile , fileData , router ] ) ;
162+ } , [ project , projectUuid , authLoading , isLoadingProject , toast ] ) ;
169163
170164
171165 useEffect ( ( ) => {
@@ -184,6 +178,7 @@ function FileExplorerContent() {
184178 setProject ( fetchedProject ) ;
185179 if ( ! fetchedProject . githubRepoName ) {
186180 setError ( "Project is not linked to a GitHub repository." ) ;
181+ setIsLoadingPathContent ( false ) ;
187182 }
188183 }
189184 } )
@@ -197,13 +192,12 @@ function FileExplorerContent() {
197192 } ) ;
198193 } , [ projectUuid , user , authLoading , router ] ) ;
199194
195+
200196 useEffect ( ( ) => {
201- if ( project && project . githubRepoName && ! authLoading && ! isLoadingProject ) {
202- forceReloadContent ( currentPath ) ;
203- } else if ( project && ! project . githubRepoName && ! isLoadingProject ) {
204- setIsLoadingPathContent ( false ) ;
197+ if ( project && project . githubRepoName && ! isLoadingProject ) {
198+ loadContent ( currentPath ) ;
205199 }
206- } , [ project , authLoading , isLoadingProject , currentPath , forceReloadContent ] ) ;
200+ } , [ project , isLoadingProject , currentPath , loadContent ] ) ;
207201
208202
209203 const handleSaveFile = async ( ) => {
@@ -230,7 +224,7 @@ function FileExplorerContent() {
230224 toast ( { title : "File Created" , description : `${ values . fileName } created successfully.` } ) ;
231225 setIsCreateFileModalOpen ( false ) ;
232226 newFileForm . reset ( ) ;
233- await forceReloadContent ( ) ;
227+ loadContent ( currentPath ) ;
234228 } else {
235229 toast ( { variant : "destructive" , title : "Creation Error" , description : result . error || "Failed to create file." } ) ;
236230 }
@@ -246,7 +240,7 @@ function FileExplorerContent() {
246240 toast ( { title : "Folder Created" , description : `${ values . folderName } created successfully.` } ) ;
247241 setIsCreateFolderModalOpen ( false ) ;
248242 newFolderForm . reset ( ) ;
249- await forceReloadContent ( ) ;
243+ loadContent ( currentPath ) ;
250244 } else {
251245 toast ( { variant : "destructive" , title : "Creation Error" , description : result . error || "Failed to create folder." } ) ;
252246 }
@@ -262,10 +256,10 @@ function FileExplorerContent() {
262256 setContentToDelete ( null ) ;
263257 if ( isViewingFile && fileData ?. path === contentToDelete . path ) {
264258 const parentPath = currentPath . substring ( 0 , currentPath . lastIndexOf ( '/' ) ) ;
265- await forceReloadContent ( parentPath ) ;
266259 router . push ( `/projects/${ projectUuid } /codespace/files${ parentPath ? '/' + parentPath : '' } ` ) ;
260+ // loadContent will be called by useEffect due to currentPath change
267261 } else {
268- await forceReloadContent ( ) ;
262+ loadContent ( currentPath ) ;
269263 }
270264 } else {
271265 toast ( { variant : "destructive" , title : "Deletion Error" , description : result . error || "Failed to delete file." } ) ;
@@ -278,7 +272,7 @@ function FileExplorerContent() {
278272 formData . append ( 'projectUuid' , project . uuid ) ;
279273 formData . append ( 'prompt' , values . prompt ) ;
280274 formData . append ( 'basePath' , currentPath ) ;
281- ReactStartTransition ( ( ) => {
275+ startTransition ( ( ) => {
282276 aiScaffoldFormAction ( formData ) ;
283277 } ) ;
284278 } ;
@@ -289,13 +283,13 @@ function FileExplorerContent() {
289283 toast ( { title : "AI Scaffold Success" , description : aiScaffoldState . message } ) ;
290284 setIsAiScaffoldModalOpen ( false ) ;
291285 aiScaffoldForm . reset ( ) ;
292- forceReloadContent ( ) ;
286+ loadContent ( currentPath ) ;
293287 }
294288 if ( aiScaffoldState . error ) {
295289 toast ( { variant : "destructive" , title : "AI Scaffold Error" , description : aiScaffoldState . error } ) ;
296290 }
297291 }
298- } , [ aiScaffoldState , isAiScaffolding , toast , forceReloadContent , aiScaffoldForm ] ) ;
292+ } , [ aiScaffoldState , isAiScaffolding , toast , loadContent , currentPath , aiScaffoldForm ] ) ;
299293
300294
301295 const getBreadcrumbs = ( ) => {
@@ -310,9 +304,10 @@ function FileExplorerContent() {
310304
311305 const breadcrumbs = getBreadcrumbs ( ) ;
312306 const canEditCurrentFile = fileData && ( fileData . type === 'text' || fileData . type === 'md' || fileData . type === 'html' ) ;
313- const isLoading = authLoading || isLoadingProject || isLoadingPathContent ;
307+ const isLoadingPage = authLoading || isLoadingProject || isLoadingPathContent ;
308+
314309
315- if ( isLoading ) {
310+ if ( isLoadingPage && ! error && ! project ?. githubRepoName ) { // Initial loading state before project details are known
316311 return (
317312 < div className = "space-y-6" >
318313 < div className = "flex justify-between items-center" >
@@ -345,7 +340,7 @@ function FileExplorerContent() {
345340 < div className = "flex items-center gap-2 flex-wrap" >
346341 < Dialog open = { isCreateFileModalOpen } onOpenChange = { setIsCreateFileModalOpen } >
347342 < DialogTrigger asChild >
348- < Button variant = "outline" size = "sm" disabled = { isViewingFile || ! project ?. githubRepoName } >
343+ < Button variant = "outline" size = "sm" disabled = { isViewingFile || ! project ?. githubRepoName || isLoadingPathContent } >
349344 < FilePlus className = "mr-2 h-4 w-4" /> Create File
350345 </ Button >
351346 </ DialogTrigger >
@@ -372,7 +367,7 @@ function FileExplorerContent() {
372367 </ Dialog >
373368 < Dialog open = { isCreateFolderModalOpen } onOpenChange = { setIsCreateFolderModalOpen } >
374369 < DialogTrigger asChild >
375- < Button variant = "outline" size = "sm" disabled = { isViewingFile || ! project ?. githubRepoName } >
370+ < Button variant = "outline" size = "sm" disabled = { isViewingFile || ! project ?. githubRepoName || isLoadingPathContent } >
376371 < FolderPlus className = "mr-2 h-4 w-4" /> Create Folder
377372 </ Button >
378373 </ DialogTrigger >
@@ -396,7 +391,7 @@ function FileExplorerContent() {
396391 </ Dialog >
397392 < Dialog open = { isAiScaffoldModalOpen } onOpenChange = { setIsAiScaffoldModalOpen } >
398393 < DialogTrigger asChild >
399- < Button variant = "outline" size = "sm" disabled = { isViewingFile || ! project ?. githubRepoName } >
394+ < Button variant = "outline" size = "sm" disabled = { isViewingFile || ! project ?. githubRepoName || isLoadingPathContent } >
400395 < Sparkles className = "mr-2 h-4 w-4 text-primary" /> Scaffold with AI
401396 </ Button >
402397 </ DialogTrigger >
@@ -420,7 +415,7 @@ function FileExplorerContent() {
420415 </ form >
421416 </ DialogContent >
422417 </ Dialog >
423- < Button variant = "outline" size = "sm" onClick = { ( ) => forceReloadContent ( ) } title = "Refresh content" disabled = { isLoadingPathContent || ! project ?. githubRepoName } >
418+ < Button variant = "outline" size = "sm" onClick = { ( ) => loadContent ( currentPath ) } title = "Refresh content" disabled = { isLoadingPathContent || ! project ?. githubRepoName } >
424419 { isLoadingPathContent ? < Loader2 className = "mr-2 h-4 w-4 animate-spin" /> : < RefreshCw className = "mr-2 h-4 w-4" /> } Refresh
425420 </ Button >
426421 </ div >
@@ -442,7 +437,7 @@ function FileExplorerContent() {
442437 { project ?. githubRepoName && (
443438 < div className = "flex items-center space-x-1 text-sm text-muted-foreground mt-2 overflow-x-auto whitespace-nowrap pb-1 border-t pt-2" >
444439 { breadcrumbs . map ( ( crumb , index ) => (
445- < span key = { index } className = "inline-flex items-center" >
440+ < span key = { crumb . path } className = "inline-flex items-center" >
446441 { index > 0 && < ChevronRight className = "h-4 w-4 inline-block mx-1 flex-shrink-0" /> }
447442 { ( index === breadcrumbs . length - 1 && ( isViewingFile || currentPath === crumb . path ) ) ? (
448443 < span className = "font-medium text-foreground flex items-center" >
@@ -460,14 +455,17 @@ function FileExplorerContent() {
460455 ) }
461456 </ CardHeader >
462457 < CardContent >
463- { error && (
458+ { isLoadingPathContent && ! error ? (
459+ < div className = "flex justify-center items-center py-10" >
460+ < Loader2 className = "h-8 w-8 animate-spin text-primary" />
461+ </ div >
462+ ) : error ? (
464463 < Alert variant = "destructive" >
465464 < AlertTriangle className = "h-4 w-4" />
466465 < AlertTitle > Error Loading Content</ AlertTitle >
467466 < AlertDescription > { error } </ AlertDescription >
468467 </ Alert >
469- ) }
470- { ! error && ! project ?. githubRepoName && (
468+ ) : ! project ?. githubRepoName ? (
471469 < Alert >
472470 < AlertTriangle className = "h-4 w-4" />
473471 < AlertTitle > Repository Not Linked</ AlertTitle >
@@ -478,8 +476,7 @@ function FileExplorerContent() {
478476 </ Button >
479477 </ AlertDescription >
480478 </ Alert >
481- ) }
482- { ! error && project ?. githubRepoName && isViewingFile && fileData && (
479+ ) : isViewingFile && fileData ? (
483480 < div >
484481 < div className = "flex justify-between items-center mb-4" >
485482 < h3 className = "text-xl font-semibold" > { fileData . name } </ h3 >
@@ -557,8 +554,7 @@ function FileExplorerContent() {
557554 </ Alert >
558555 ) }
559556 </ div >
560- ) }
561- { ! error && project ?. githubRepoName && ! isViewingFile && contents . length > 0 && (
557+ ) : ! isViewingFile && contents . length > 0 ? (
562558 < Table >
563559 < TableHeader >
564560 < TableRow >
@@ -609,7 +605,7 @@ function FileExplorerContent() {
609605 < Trash2 className = "h-4 w-4" />
610606 </ Button >
611607 </ AlertDialogTrigger >
612- { contentToDelete ?. sha === item . sha && (
608+ { contentToDelete ?. path === item . path && ( // Check path instead of sha for key consistency
613609 < AlertDialogContent >
614610 < AlertDialogHeader >
615611 < AlertDialogTitle > Delete File: "{ contentToDelete . name } "?</ AlertDialogTitle >
@@ -636,21 +632,19 @@ function FileExplorerContent() {
636632 ) ) }
637633 </ TableBody >
638634 </ Table >
639- ) }
640- { ! error && project ?. githubRepoName && ! isViewingFile && contents . length === 0 && ! currentPath && (
635+ ) : ! isViewingFile && contents . length === 0 && ! currentPath ? (
641636 < Alert >
642637 < FileCode className = "h-4 w-4" />
643638 < AlertTitle > Empty Repository</ AlertTitle >
644- < AlertDescription > This repository ('{ project . githubRepoName } ') appears to be empty. You can create files or folders using the buttons above.</ AlertDescription >
639+ < AlertDescription > This repository ('{ project ? .githubRepoName || 'Unknown' } ') appears to be empty. You can create files or folders using the buttons above.</ AlertDescription >
645640 </ Alert >
646- ) }
647- { ! error && project ?. githubRepoName && ! isViewingFile && contents . length === 0 && currentPath && (
641+ ) : ! isViewingFile && contents . length === 0 && currentPath ? (
648642 < Alert >
649643 < Folder className = "h-4 w-4" />
650644 < AlertTitle > Empty Directory</ AlertTitle >
651645 < AlertDescription > This directory ('/{ currentPath } ') is empty. You can create files or folders here using the buttons above.</ AlertDescription >
652646 </ Alert >
653- ) }
647+ ) : null }
654648 </ CardContent >
655649 </ Card >
656650
@@ -692,3 +686,4 @@ export default function GitHubFilesPage() {
692686 )
693687}
694688
689+
0 commit comments