file browser small UI improvements
This commit is contained in:
parent
448bb8e034
commit
2dfeb4e2bb
1 changed files with 14 additions and 21 deletions
|
|
@ -11,18 +11,17 @@ type FileItem = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type ListResponse =
|
type ListResponse =
|
||||||
| { path: string; items: FileItem[] } // current API shape
|
| { path: string; items: FileItem[] }
|
||||||
| { path: string; entries: FileItem[] } // legacy fallback
|
| { path: string; entries: FileItem[] }
|
||||||
| { items?: FileItem[]; entries?: FileItem[]; path?: string };
|
| { items?: FileItem[]; entries?: FileItem[]; path?: string };
|
||||||
|
|
||||||
const BASE =
|
const BASE =
|
||||||
(process.env.NEXT_PUBLIC_FILE_API_BASE_URL || "").replace(/\/$/, "") || ""; // same-origin fallback
|
(process.env.NEXT_PUBLIC_FILE_API_BASE_URL || "").replace(/\/$/, "") || "";
|
||||||
|
|
||||||
function joinPath(a: string, b: string) {
|
function joinPath(a: string, b: string) {
|
||||||
if (!a || a === "/") return b.startsWith("/") ? b : `/${b}`;
|
if (!a || a === "/") return b.startsWith("/") ? b : `/${b}`;
|
||||||
return `${a.replace(/\/$/, "")}/${b.replace(/^\//, "")}`;
|
return `${a.replace(/\/$/, "")}/${b.replace(/^\//, "")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parentPath(path: string) {
|
function parentPath(path: string) {
|
||||||
if (!path || path === "/") return "/";
|
if (!path || path === "/") return "/";
|
||||||
const parts = path.replace(/\/+$/, "").split("/");
|
const parts = path.replace(/\/+$/, "").split("/");
|
||||||
|
|
@ -30,7 +29,6 @@ function parentPath(path: string) {
|
||||||
const p = parts.join("/");
|
const p = parts.join("/");
|
||||||
return p === "" ? "/" : p;
|
return p === "" ? "/" : p;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSize(bytes?: number) {
|
function formatSize(bytes?: number) {
|
||||||
if (bytes == null) return "—";
|
if (bytes == null) return "—";
|
||||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||||
|
|
@ -54,12 +52,10 @@ export default function FileBrowserPanel() {
|
||||||
const p = encodeURIComponent(path || "/");
|
const p = encodeURIComponent(path || "/");
|
||||||
return `${BASE}/api/files/list?path=${p}`;
|
return `${BASE}/api/files/list?path=${p}`;
|
||||||
}, [path]);
|
}, [path]);
|
||||||
|
|
||||||
const urlDownload = useCallback((p: string) => {
|
const urlDownload = useCallback((p: string) => {
|
||||||
const qp = encodeURIComponent(p || "/");
|
const qp = encodeURIComponent(p || "/");
|
||||||
return `${BASE}/api/files/download?path=${qp}`;
|
return `${BASE}/api/files/download?path=${qp}`;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const urlRaw = useCallback((p: string) => {
|
const urlRaw = useCallback((p: string) => {
|
||||||
const qp = encodeURIComponent(p || "/");
|
const qp = encodeURIComponent(p || "/");
|
||||||
return `${BASE}/api/files/raw?path=${qp}`;
|
return `${BASE}/api/files/raw?path=${qp}`;
|
||||||
|
|
@ -72,7 +68,7 @@ export default function FileBrowserPanel() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(urlList, {
|
const res = await fetch(urlList, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
credentials: "include", // ensure cookies flow past middleware
|
credentials: "include",
|
||||||
headers: { Accept: "application/json" },
|
headers: { Accept: "application/json" },
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
|
|
@ -83,7 +79,6 @@ export default function FileBrowserPanel() {
|
||||||
const json: ListResponse = await res.json();
|
const json: ListResponse = await res.json();
|
||||||
const arr = (json as any).items || (json as any).entries || [];
|
const arr = (json as any).items || (json as any).entries || [];
|
||||||
if (!Array.isArray(arr)) throw new Error("Malformed list response");
|
if (!Array.isArray(arr)) throw new Error("Malformed list response");
|
||||||
// Sort: dirs first, then files, alpha
|
|
||||||
arr.sort((a: FileItem, b: FileItem) => {
|
arr.sort((a: FileItem, b: FileItem) => {
|
||||||
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
|
|
@ -105,17 +100,13 @@ export default function FileBrowserPanel() {
|
||||||
if (it.type === "dir") {
|
if (it.type === "dir") {
|
||||||
setPath((p) => joinPath(p, it.name));
|
setPath((p) => joinPath(p, it.name));
|
||||||
} else {
|
} else {
|
||||||
// try to preview; if the file is not previewable, user can still download
|
|
||||||
setPreviewHref(urlRaw(joinPath(path, it.name)));
|
setPreviewHref(urlRaw(joinPath(path, it.name)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onUp = () => setPath((p) => parentPath(p));
|
const onUp = () => setPath((p) => parentPath(p));
|
||||||
const onHome = () => setPath("/");
|
const onHome = () => setPath("/");
|
||||||
|
|
||||||
const onDownload = (it: FileItem) => {
|
const onDownload = (it: FileItem) => {
|
||||||
const href = urlDownload(joinPath(path, it.name));
|
const href = urlDownload(joinPath(path, it.name));
|
||||||
// create an anchor to download
|
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = href;
|
a.href = href;
|
||||||
a.download = it.name;
|
a.download = it.name;
|
||||||
|
|
@ -126,7 +117,6 @@ export default function FileBrowserPanel() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* toolbar */}
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={onHome}
|
onClick={onHome}
|
||||||
|
|
@ -153,13 +143,15 @@ export default function FileBrowserPanel() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* path crumb */}
|
|
||||||
<div className="text-xs text-muted-foreground select-all">{path}</div>
|
<div className="text-xs text-muted-foreground select-all">{path}</div>
|
||||||
|
|
||||||
{/* grid: table + preview */}
|
{/* Wider table, same preview: left has min 760px; table can scroll horizontally if cramped */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-[1fr,420px] gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-[minmax(760px,1fr),420px] gap-4">
|
||||||
|
{/* TABLE */}
|
||||||
<div className="rounded-md border overflow-hidden">
|
<div className="rounded-md border overflow-hidden">
|
||||||
<div className="grid grid-cols-[minmax(220px,1fr),90px,100px,130px,90px] items-center bg-muted px-3 py-2 text-sm font-medium">
|
<div className="overflow-x-auto">
|
||||||
|
<div className="min-w-[740px]">
|
||||||
|
<div className="grid grid-cols-[minmax(340px,1fr),110px,120px,170px,120px] items-center bg-muted px-3 py-2 text-sm font-medium whitespace-nowrap">
|
||||||
<div>Name</div>
|
<div>Name</div>
|
||||||
<div>Type</div>
|
<div>Type</div>
|
||||||
<div>Size</div>
|
<div>Size</div>
|
||||||
|
|
@ -180,7 +172,7 @@ export default function FileBrowserPanel() {
|
||||||
items.map((it) => (
|
items.map((it) => (
|
||||||
<div
|
<div
|
||||||
key={it.name + it.type}
|
key={it.name + it.type}
|
||||||
className="grid grid-cols-[minmax(220px,1fr),90px,100px,130px,90px] items-center px-3 py-2 text-sm hover:bg-muted/50"
|
className="grid grid-cols-[minmax(340px,1fr),110px,120px,170px,120px] items-center px-3 py-2 text-sm hover:bg-muted/50"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="text-left inline-flex items-center gap-2 hover:underline"
|
className="text-left inline-flex items-center gap-2 hover:underline"
|
||||||
|
|
@ -218,13 +210,14 @@ export default function FileBrowserPanel() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* preview */}
|
{/* PREVIEW (unchanged size) */}
|
||||||
<div className="rounded-md border p-3">
|
<div className="rounded-md border p-3">
|
||||||
<div className="mb-2 text-sm font-medium">Preview</div>
|
<div className="mb-2 text-sm font-medium">Preview</div>
|
||||||
{previewHref ? (
|
{previewHref ? (
|
||||||
<div className="border rounded overflow-hidden">
|
<div className="border rounded overflow-hidden">
|
||||||
{/* let the browser try its best; PDFs/images will render inline */}
|
|
||||||
<iframe
|
<iframe
|
||||||
key={previewHref}
|
key={previewHref}
|
||||||
src={previewHref}
|
src={previewHref}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue