Files
AgentHub/client/src/pages/FilesPage.tsx
T
nntrivi2001 220309db12 fix(files): a workspace whose folder was deleted leaves the Files places
The lane payload carries folder_missing (ENOENT/ENOTDIR only). Files hides
such a workspace and never reopens it as the default place; the Workspace
sidebar strikes its name through with a hint to remove it from its menu.
A folder that cannot be read no longer keeps the previous folder's entries
under its error, and a change in Files re-reads the workspace list.
2026-11-02 10:11:47 +07:00

89 lines
3.5 KiB
TypeScript

/**
* @file FilesPage.tsx
* @description `/files` — the file manager (`FileBrowser`) as a page of its
* own, reached from the sidebar's **Files** entry. The location is kept in the
* URL (`?home=<me|user>&path=` or `?lane=<id>&path=`), so Back and a shared
* link land in the same folder. With neither it opens the workspace last
* shown (`agenthub.workspace.selected`) — reached from a workspace, Files is
* about THAT workspace — else the user's own HOME (their Linux home under S3),
* else the first workspace.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { api, isHomePlace, type FsPlace } from "../lib/api";
import { FileBrowser } from "../components/lanes/FileExplorer";
function lastWorkspace(): number | null {
try {
const v = Number(localStorage.getItem("agenthub.workspace.selected"));
return Number.isInteger(v) && v > 0 ? v : null;
} catch {
return null;
}
}
type Start = { lane: number } | { home: string };
export function FilesPage() {
const { t } = useTranslation(["lanes"]);
const [params, setParams] = useSearchParams();
const askedLane = Number(params.get("lane"));
const askedHome = params.get("home");
// The URL wins on every render: a place picked in the browser (another
// user's home) lands in the URL, and a frozen start would send the next
// folder opened there back to the first place.
const asked: Start | null = askedHome
? { home: askedHome }
: Number.isInteger(askedLane) && askedLane > 0
? { lane: askedLane }
: null;
const [fallback, setStart] = useState<Start | null>(null);
const start = asked ?? fallback;
const [none, setNone] = useState(false);
useEffect(() => {
if (start) return;
void (async () => {
// The workspace last shown first: Files reached from a workspace opens
// THAT workspace (from PhuNT → AC_SHOP it opened your own home, empty
// but for hidden files); the home is the fallback.
// A workspace whose folder was deleted has nothing to open.
const lanes = (await api.lanes.list().catch(() => ({ lanes: [] }))).lanes.filter(
(l) => !l.folder_missing
);
const last = lastWorkspace();
const shown = lanes.find((l) => l.id === last);
if (shown) return setStart({ lane: shown.id });
const homes = await api.files.homes().catch(() => ({ homes: [] }));
if (homes.homes.some((h) => h.id === "me")) return setStart({ home: "me" });
if (lanes[0]) setStart({ lane: lanes[0].id });
else setNone(true);
})();
}, [start]);
if (none) return <p className="p-8 text-sm text-fg-muted">{t("fx.noWorkspace")}</p>;
if (!start) return null;
return (
// Cancels Layout's page padding, like the Workspace session view.
<div className="-m-5 flex h-[calc(100vh-1.5rem)] max-md:h-[calc(100vh-4.25rem)] lg:-m-6">
<FileBrowser
laneId={"lane" in start ? start.lane : null}
initialHome={"home" in start ? start.home : null}
initialPath={params.get("path") ?? ""}
onNavigate={(place: FsPlace, path) => {
const next = new URLSearchParams(
isHomePlace(place)
? { home: place.home }
: { lane: String(typeof place === "number" ? place : place.lane) }
);
if (path) next.set("path", path);
setParams(next, { replace: false });
}}
/>
</div>
);
}