@@ -4,6 +4,7 @@ import { getFileBlame, getFileSource } from "@/app/api/(client)/client";
44import { PathHeader } from "@/app/(app)/components/pathHeader" ;
55import { Button } from "@/components/ui/button" ;
66import { Separator } from "@/components/ui/separator" ;
7+ import { Skeleton } from "@/components/ui/skeleton" ;
78import { Tooltip , TooltipContent , TooltipTrigger } from "@/components/ui/tooltip" ;
89import { cn , getCodeHostInfoForRepo , isServiceError , truncateSha } from "@/lib/utils" ;
910import { useQuery } from "@tanstack/react-query" ;
@@ -57,41 +58,119 @@ export const CodePreviewPanelClient = ({ path, repoName, revisionName, previewRe
5758 enabled : ! ! blame ,
5859 } ) ;
5960
60- if ( isFileSourcePending || ( blame && isBlamePending ) ) {
61- return (
62- < div className = "flex flex-col w-full min-h-full items-center justify-center" >
63- < Loader2 className = "w-4 h-4 animate-spin" />
64- Loading...
65- </ div >
66- ) ;
67- }
68-
69- if ( ! fileSourceResponse || isServiceError ( fileSourceResponse ) ) {
70- return < div > Error loading file source: { isServiceError ( fileSourceResponse ) ? fileSourceResponse . message : 'No response received' } </ div >
71- }
72-
73- if ( blameResponse !== undefined && isServiceError ( blameResponse ) ) {
74- return < div > Error loading blame: { blameResponse . message } </ div >
75- }
76-
77- const source = fileSourceResponse . source ;
78- const lineCount = source . length === 0
79- ? 0
80- : source . split ( '\n' ) . length - ( source . endsWith ( '\n' ) ? 1 : 0 ) ;
81- const byteSize = new TextEncoder ( ) . encode ( source ) . length ;
82- const fileSize = formatFileSize ( byteSize ) ;
83-
8461 const codeHostInfo = getCodeHostInfoForRepo ( {
8562 codeHostType : repo . codeHostType ,
8663 name : repo . name ,
8764 displayName : repo . displayName ,
8865 externalWebUrl : repo . externalWebUrl ,
8966 } ) ;
9067
91- // @todo : this is a hack to support linking to files for ADO. ADO doesn't support web urls with HEAD so we replace it with main. THis
68+ const loadedFile = fileSourceResponse && ! isServiceError ( fileSourceResponse ) ? fileSourceResponse : undefined ;
69+
70+ // @todo : this is a hack to support linking to files for ADO. ADO doesn't support web urls with HEAD so we replace it with main. This
9271 // will break if the default branch is not main.
93- const fileWebUrl = repo . codeHostType === "azuredevops" && fileSourceResponse . externalWebUrl ?
94- fileSourceResponse . externalWebUrl . replace ( "version=GBHEAD" , "version=GBmain" ) : fileSourceResponse . externalWebUrl ;
72+ const fileWebUrl = loadedFile
73+ ? ( repo . codeHostType === "azuredevops" && loadedFile . externalWebUrl
74+ ? loadedFile . externalWebUrl . replace ( "version=GBHEAD" , "version=GBmain" )
75+ : loadedFile . externalWebUrl )
76+ : undefined ;
77+
78+ // Line/size stats are derived from the fetched file, so they only appear
79+ // once it has loaded. The surrounding toolbar (blame toggle) does not wait.
80+ const lineCount = loadedFile === undefined
81+ ? undefined
82+ : loadedFile . source . length === 0
83+ ? 0
84+ : loadedFile . source . split ( '\n' ) . length - ( loadedFile . source . endsWith ( '\n' ) ? 1 : 0 ) ;
85+ const fileSize = loadedFile === undefined
86+ ? undefined
87+ : formatFileSize ( new TextEncoder ( ) . encode ( loadedFile . source ) . length ) ;
88+
89+ // The path header and separator are derived entirely from props, so they
90+ // render immediately. Only the body below depends on the fetched source,
91+ // so the loading / error states live here rather than replacing the whole
92+ // panel.
93+ const renderBody = ( ) => {
94+ if ( isFileSourcePending || ( blame && isBlamePending ) ) {
95+ return (
96+ < div className = "flex flex-1 flex-col w-full items-center justify-center" >
97+ < Loader2 className = "w-4 h-4 animate-spin" />
98+ Loading...
99+ </ div >
100+ ) ;
101+ }
102+
103+ if ( ! fileSourceResponse || isServiceError ( fileSourceResponse ) ) {
104+ return (
105+ < div className = "p-4 text-sm text-destructive" >
106+ Error loading file source: { isServiceError ( fileSourceResponse ) ? fileSourceResponse . message : 'No response received' }
107+ </ div >
108+ ) ;
109+ }
110+
111+ if ( blameResponse !== undefined && isServiceError ( blameResponse ) ) {
112+ return (
113+ < div className = "p-4 text-sm text-destructive" >
114+ Error loading blame: { blameResponse . message }
115+ </ div >
116+ ) ;
117+ }
118+
119+ return (
120+ < >
121+ { previewRef && (
122+ < div className = "flex flex-row items-center justify-between gap-2 px-4 py-2 border-b shrink-0" >
123+ < span className = "text-sm" >
124+ Previewing file at revision{ " " }
125+ < Link
126+ href = { getBrowsePath ( {
127+ repoName,
128+ revisionName,
129+ path : '' ,
130+ pathType : 'commit' ,
131+ commitSha : previewRef ,
132+ } ) }
133+ className = "font-mono text-link hover:underline"
134+ >
135+ { truncateSha ( previewRef ) }
136+ </ Link >
137+ </ span >
138+ < Tooltip key = { previewRef } >
139+ < TooltipTrigger >
140+ < Button
141+ asChild
142+ variant = "ghost"
143+ size = "icon"
144+ className = "h-6 w-6 text-muted-foreground"
145+ >
146+ < Link
147+ href = { getBrowsePath ( {
148+ repoName,
149+ revisionName,
150+ path,
151+ pathType : 'blob' ,
152+ } ) }
153+ aria-label = "Close preview"
154+ >
155+ < X className = "h-4 w-4" />
156+ </ Link >
157+ </ Button >
158+ </ TooltipTrigger >
159+ < TooltipContent > Close preview</ TooltipContent >
160+ </ Tooltip >
161+ </ div >
162+ ) }
163+ < PureCodePreviewPanel
164+ source = { fileSourceResponse . source }
165+ language = { fileSourceResponse . language }
166+ repoName = { repoName }
167+ path = { path }
168+ revisionName = { contentRef ?? 'HEAD' }
169+ blame = { blame ? blameResponse : undefined }
170+ />
171+ </ >
172+ ) ;
173+ } ;
95174
96175 return (
97176 < >
@@ -128,9 +207,13 @@ export const CodePreviewPanelClient = ({ path, repoName, revisionName, previewRe
128207 path = { path }
129208 blame = { blame ?? false }
130209 />
131- < span className = "text-sm text-muted-foreground" >
132- { lineCount . toLocaleString ( ) } lines · { fileSize }
133- </ span >
210+ { isFileSourcePending ? (
211+ < Skeleton className = "h-4 w-32" />
212+ ) : lineCount !== undefined ? (
213+ < span className = "text-sm text-muted-foreground" >
214+ { lineCount . toLocaleString ( ) } lines · { fileSize }
215+ </ span >
216+ ) : null }
134217 { blame && (
135218 < >
136219 < Separator orientation = "vertical" className = "h-4" />
@@ -139,56 +222,7 @@ export const CodePreviewPanelClient = ({ path, repoName, revisionName, previewRe
139222 ) }
140223 </ div >
141224 ) }
142- { previewRef && (
143- < div className = "flex flex-row items-center justify-between gap-2 px-4 py-2 border-b shrink-0" >
144- < span className = "text-sm" >
145- Previewing file at revision{ " " }
146- < Link
147- href = { getBrowsePath ( {
148- repoName,
149- revisionName,
150- path : '' ,
151- pathType : 'commit' ,
152- commitSha : previewRef ,
153- } ) }
154- className = "font-mono text-link hover:underline"
155- >
156- { truncateSha ( previewRef ) }
157- </ Link >
158- </ span >
159- < Tooltip key = { previewRef } >
160- < TooltipTrigger >
161- < Button
162- asChild
163- variant = "ghost"
164- size = "icon"
165- className = "h-6 w-6 text-muted-foreground"
166- >
167- < Link
168- href = { getBrowsePath ( {
169- repoName,
170- revisionName,
171- path,
172- pathType : 'blob' ,
173- } ) }
174- aria-label = "Close preview"
175- >
176- < X className = "h-4 w-4" />
177- </ Link >
178- </ Button >
179- </ TooltipTrigger >
180- < TooltipContent > Close preview</ TooltipContent >
181- </ Tooltip >
182- </ div >
183- ) }
184- < PureCodePreviewPanel
185- source = { fileSourceResponse . source }
186- language = { fileSourceResponse . language }
187- repoName = { repoName }
188- path = { path }
189- revisionName = { contentRef ?? 'HEAD' }
190- blame = { blame ? blameResponse : undefined }
191- />
225+ { renderBody ( ) }
192226 </ >
193227 )
194228}
0 commit comments