Skip to content

Commit 9ec577c

Browse files
committed
add validateSearch to r2/bucketname/object route
1 parent b9bfcf7 commit 9ec577c

1 file changed

Lines changed: 30 additions & 19 deletions

File tree

  • packages/local-explorer-ui/src/routes/r2/$bucketName

packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
Link,
66
notFound,
77
useNavigate,
8-
useRouterState,
98
} from "@tanstack/react-router";
109
import { useState } from "react";
1110
import { r2BucketDeleteObjects, r2BucketGetObject } from "../../../api";
@@ -17,9 +16,16 @@ import { ResourceError } from "../../../components/ResourceError";
1716
import { formatDate, formatSize } from "../../../utils/format";
1817
import type { R2HeadObjectResult } from "../../../api";
1918

19+
export interface ObjectDetailSearch {
20+
delimiter?: boolean;
21+
}
22+
2023
export 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

134140
function 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 &ldquo;{search.objectKey}
291+
Are you sure you want to delete &ldquo;{loaderData.objectKey}
281292
&rdquo;? This cannot be undone.
282293
</Dialog.Description>
283294

0 commit comments

Comments
 (0)