Open a folder chip on its folder, in the folder that really holds it
A bot's folder chip went to bare /admin/files instead of the folder it named, and even once there the computer volume pointed at the wrong directory, so the folder a bot linked to looked empty. The chip lost its folder because folderRefUrl only accepted a ref's volume name (computer) while the chip hands it the browser's (computers), so storageVolumeFromRef found nothing and the whole query was dropped. Both spellings resolve now, through parseStorageSpec, so a bad owner falls back cleanly instead of emitting owner=0. The deeper bug: a computer's home was assumed to be DATA_DIR/computers/<id>, but on a bind mount it can be anywhere on the docker host, and a computer the app didn't provision (the main one on a server reached at MAIN_COMPUTER_URL, i.e. a dev server outside docker) records nothing at all. resolveComputerHomeDir holds the precedence: the directory provisionComputer actually bound in (containers.metadata.data_dir), else the new MAIN_COMPUTER_DATA_DIR app setting for the main computer, else the conventional folder. A path that isn't on disk also no longer reads as an empty folder: listStorageEntries reports it as missing and the page says so, so a stale folder link can't look like a folder nothing has written yet.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { StorageFolderPayload } from "@/src/types";
|
||||
import { emptyFolderText, folderSummary } from "./folder-text";
|
||||
|
||||
function folder(overrides: Partial<StorageFolderPayload> = {}): StorageFolderPayload {
|
||||
return {
|
||||
spec: { volume: "computers", owner_id: 1 },
|
||||
label: "Main computer",
|
||||
path: "seo/coderank",
|
||||
breadcrumbs: [],
|
||||
entries: [],
|
||||
total: 0,
|
||||
offset: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("folderSummary", () => {
|
||||
test("counts what is in the folder", () => {
|
||||
expect(folderSummary({ folder: folder({ total: 4 }) })).toContain("4 item(s)");
|
||||
});
|
||||
|
||||
test("says a missing folder isn't on disk, not that it is empty", () => {
|
||||
const text = folderSummary({ folder: folder({ missing: true }) });
|
||||
|
||||
expect(text).toContain("isn't on disk");
|
||||
expect(text).not.toContain("0 item(s)");
|
||||
});
|
||||
|
||||
test("asks for a folder when there is none", () => {
|
||||
expect(folderSummary({ folder: null })).toBe("Pick a folder to look inside.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("emptyFolderText", () => {
|
||||
test("an empty folder says so", () => {
|
||||
expect(emptyFolderText({ folder: folder(), search: "" })).toBe("This folder is empty.");
|
||||
});
|
||||
|
||||
test("a path that isn't there says so instead", () => {
|
||||
expect(emptyFolderText({ folder: folder({ missing: true }), search: "" })).toBe(
|
||||
"There's no seo/coderank in here. It may have been moved or deleted.",
|
||||
);
|
||||
});
|
||||
|
||||
test("loading, an error and a search each win over the folder's own state", () => {
|
||||
expect(emptyFolderText({ folder: folder({ missing: true }), search: "", loading: true })).toBe(
|
||||
"Loading...",
|
||||
);
|
||||
expect(
|
||||
emptyFolderText({ folder: folder({ missing: true }), search: "", error: "Not found." }),
|
||||
).toBe("Not found.");
|
||||
expect(emptyFolderText({ folder: folder({ missing: true }), search: "raw" })).toBe(
|
||||
'Nothing matches "raw".',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { StorageFolderPayload } from "@/src/types";
|
||||
|
||||
/**
|
||||
* The line under the open folder's name: how much is in it, or, for a path
|
||||
* that isn't on disk, that it isn't there. A stale folder link must never
|
||||
* read as an empty folder.
|
||||
*/
|
||||
export function folderSummary({ folder }: { folder: StorageFolderPayload | null }) {
|
||||
if (!folder) return "Pick a folder to look inside.";
|
||||
|
||||
if (folder.missing) {
|
||||
return `${folder.label}: this folder isn't on disk. Nothing has written it yet, or its link is out of date.`;
|
||||
}
|
||||
|
||||
return `${folder.label}: ${folder.total} item(s). Everything a bot, a workspace or a computer keeps for you.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* What an empty listing says, and why it is empty when the folder the
|
||||
* user asked for isn't there at all.
|
||||
*/
|
||||
export function emptyFolderText({
|
||||
folder,
|
||||
search,
|
||||
loading,
|
||||
error,
|
||||
}: {
|
||||
folder: StorageFolderPayload | null;
|
||||
search: string;
|
||||
loading?: boolean;
|
||||
error?: string;
|
||||
}) {
|
||||
if (loading) return "Loading...";
|
||||
if (error) return error;
|
||||
if (search) return `Nothing matches "${search}".`;
|
||||
|
||||
if (folder?.missing) {
|
||||
return `There's no ${folder.path || "folder"} in here. It may have been moved or deleted.`;
|
||||
}
|
||||
|
||||
return "This folder is empty.";
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import type { StorageFolderPayload, StorageSpecOption } from "@/src/types";
|
||||
import type { StorageSpec } from "@/src/utils/storage-volumes";
|
||||
import { emptyFolderText, folderSummary } from "../(functions)/folder-text";
|
||||
import FilesBreadcrumbRow from "../(partials)/files-breadcrumb-row";
|
||||
import FilesSearchInput from "../(partials)/files-search-input";
|
||||
import FilesVolumeRow from "../(partials)/files-volume-row";
|
||||
@@ -81,9 +82,7 @@ export default function FilesBrowser({
|
||||
"Files"}
|
||||
</H2>
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
{folder
|
||||
? `${folder.label}: ${folder.total} item(s). Everything a bot, a workspace or a computer keeps for you.`
|
||||
: "Pick a folder to look inside."}
|
||||
{folderSummary({ folder })}
|
||||
</Span>
|
||||
</Stack>
|
||||
<FilesBreadcrumbRow
|
||||
@@ -111,12 +110,7 @@ export default function FilesBrowser({
|
||||
))
|
||||
) : (
|
||||
<Span className="py-8 text-[13px] leading-[18px] text-ink-secondary">
|
||||
{loading
|
||||
? "Loading..."
|
||||
: error ||
|
||||
(search
|
||||
? `Nothing matches "${search}".`
|
||||
: "This folder is empty.")}
|
||||
{emptyFolderText({ folder, search, loading, error })}
|
||||
</Span>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -80,5 +80,6 @@ async function folderPayload({
|
||||
entries: listing.entries,
|
||||
total: listing.total,
|
||||
offset: listing.offset,
|
||||
missing: listing.missing,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user