55 Link ,
66 notFound ,
77 useNavigate ,
8- useRouterState ,
98} from "@tanstack/react-router" ;
109import { useState } from "react" ;
1110import { r2BucketDeleteObjects , r2BucketGetObject } from "../../../api" ;
@@ -17,9 +16,16 @@ import { ResourceError } from "../../../components/ResourceError";
1716import { formatDate , formatSize } from "../../../utils/format" ;
1817import type { R2HeadObjectResult } from "../../../api" ;
1918
19+ export interface ObjectDetailSearch {
20+ delimiter ?: boolean ;
21+ }
22+
2023export const Route = createFileRoute ( "/r2/$bucketName/object/$" ) ( {
2124 component : ObjectDetailView ,
2225 errorComponent : ResourceError ,
26+ validateSearch : ( search : Record < string , unknown > ) : ObjectDetailSearch => ( {
27+ delimiter : search . delimiter === false ? false : true ,
28+ } ) ,
2329 loader : async ( { params } ) => {
2430 const objectKey = params . _splat ;
2531 if ( ! objectKey ) {
@@ -133,18 +139,19 @@ function CustomMetadataCard({
133139
134140function ObjectDetailView ( ) : JSX . Element {
135141 const params = Route . useParams ( ) ;
136- const search = Route . useLoaderData ( ) ;
142+ const loaderData = Route . useLoaderData ( ) ;
143+ const search = Route . useSearch ( ) ;
137144 const navigate = useNavigate ( ) ;
138145
139146 const [ deleteDialogOpen , setDeleteDialogOpen ] = useState < boolean > ( false ) ;
140147 const [ deleting , setDeleting ] = useState < boolean > ( false ) ;
141148 const [ error , setError ] = useState < string | null > ( null ) ;
142149
143150 function handleDownload ( ) : void {
144- const downloadUrl = `/cdn-cgi/explorer/api/r2/buckets/${ encodeURIComponent ( params . bucketName ) } /objects/${ encodeURIComponent ( search . objectKey ) } ` ;
151+ const downloadUrl = `/cdn-cgi/explorer/api/r2/buckets/${ encodeURIComponent ( params . bucketName ) } /objects/${ encodeURIComponent ( loaderData . objectKey ) } ` ;
145152 const link = document . createElement ( "a" ) ;
146153 link . href = downloadUrl ;
147- link . download = search . objectKey . split ( "/" ) . pop ( ) || "download" ;
154+ link . download = loaderData . objectKey . split ( "/" ) . pop ( ) || "download" ;
148155 document . body . appendChild ( link ) ;
149156 link . click ( ) ;
150157 document . body . removeChild ( link ) ;
@@ -157,11 +164,14 @@ function ObjectDetailView(): JSX.Element {
157164 path : {
158165 bucket_name : params . bucketName ,
159166 } ,
160- body : [ search . objectKey ] ,
167+ body : [ loaderData . objectKey ] ,
161168 } ) ;
162169 // Navigate back to bucket root or parent prefix
163- const parentPrefix = search . objectKey . includes ( "/" )
164- ? search . objectKey . substring ( 0 , search . objectKey . lastIndexOf ( "/" ) + 1 )
170+ const parentPrefix = loaderData . objectKey . includes ( "/" )
171+ ? loaderData . objectKey . substring (
172+ 0 ,
173+ loaderData . objectKey . lastIndexOf ( "/" ) + 1
174+ )
165175 : undefined ;
166176 void navigate ( {
167177 params : {
@@ -182,17 +192,16 @@ function ObjectDetailView(): JSX.Element {
182192 }
183193
184194 // Build breadcrumb items - bucket, parent folders, and object name
185- const routerState = useRouterState ( ) ;
186- const urlParams = new URLSearchParams ( routerState . location . searchStr ) ;
187- const directoryView = urlParams . get ( "delimiter" ) !== "false" ;
195+ const directoryView = search . delimiter !== false ;
188196
189197 const pathSegments = directoryView
190- ? search . objectKey . split ( "/" ) . filter ( Boolean )
198+ ? loaderData . objectKey . split ( "/" ) . filter ( Boolean )
191199 : [ ] ;
192200 const fileName = directoryView
193- ? pathSegments . pop ( ) || search . objectKey
194- : search . objectKey ;
201+ ? pathSegments . pop ( ) || loaderData . objectKey
202+ : loaderData . objectKey ;
195203 const breadcrumbItems = [
204+ // bucket name
196205 < Link
197206 className = "text-kumo-default no-underline hover:text-kumo-link"
198207 key = "bucket"
@@ -202,6 +211,7 @@ function ObjectDetailView(): JSX.Element {
202211 >
203212 { params . bucketName }
204213 </ Link > ,
214+ // optional path segments (only if set to folder mode)
205215 ...pathSegments . map ( ( segment , index ) => {
206216 const segmentPrefix = pathSegments . slice ( 0 , index + 1 ) . join ( "/" ) + "/" ;
207217 return (
@@ -216,6 +226,7 @@ function ObjectDetailView(): JSX.Element {
216226 </ Link >
217227 ) ;
218228 } ) ,
229+ // file name (may be full object key if not in folder mode)
219230 < span key = "file" > { fileName } </ span > ,
220231 ] ;
221232
@@ -234,11 +245,11 @@ function ObjectDetailView(): JSX.Element {
234245 < div className = "flex min-w-0 items-center gap-2" >
235246 < h1
236247 className = "truncate text-base text-kumo-default"
237- title = { search . objectKey }
248+ title = { loaderData . objectKey }
238249 >
239- { search . objectKey }
250+ { loaderData . objectKey }
240251 </ h1 >
241- < CopyButton text = { search . objectKey } />
252+ < CopyButton text = { loaderData . objectKey } />
242253 </ div >
243254
244255 < div className = "flex shrink-0 items-center gap-2" >
@@ -260,8 +271,8 @@ function ObjectDetailView(): JSX.Element {
260271 </ div >
261272
262273 < div className = "space-y-6" >
263- < ObjectDetailsCard object = { search . object } />
264- < CustomMetadataCard metadata = { search . object . custom_metadata } />
274+ < ObjectDetailsCard object = { loaderData . object } />
275+ < CustomMetadataCard metadata = { loaderData . object . custom_metadata } />
265276 </ div >
266277
267278 { /* Delete Confirmation Dialog */ }
@@ -277,7 +288,7 @@ function ObjectDetailView(): JSX.Element {
277288
278289 { /* @ts -expect-error - Type mismatch due to pnpm monorepo @types/react version conflict */ }
279290 < Dialog . Description className = "mb-2 text-kumo-subtle" >
280- Are you sure you want to delete “{ search . objectKey }
291+ Are you sure you want to delete “{ loaderData . objectKey }
281292 ”? This cannot be undone.
282293 </ Dialog . Description >
283294
0 commit comments