fix: label file selections and distinguish pending trash

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-09 06:09:06 -05:00
parent a72fd32191
commit 90cad7196b
4 changed files with 20 additions and 1 deletions

View File

@ -85,6 +85,8 @@ describe("FileTree", () => {
const input = row("docs")?.querySelector("input[type='checkbox']") as HTMLInputElement | null;
expect(input?.checked).toBe(false);
expect(input?.indeterminate).toBe(true);
expect(input?.getAttribute("aria-label")).toBe("Select docs");
expect(row("docs/a.md")?.querySelector("input")?.getAttribute("aria-label")).toBe("Select docs/a.md");
expect(row("docs")?.getAttribute("aria-checked")).toBe("mixed");
});

View File

@ -439,6 +439,7 @@ export function FileTree({
<label className="flex items-center pl-2" onClick={(event) => event.stopPropagation()}>
<input
type="checkbox"
aria-label={`Select ${node.path}`}
checked={allChecked}
ref={(element) => {
if (element) element.indeterminate = someChecked;

View File

@ -52,6 +52,20 @@ afterEach(async () => {
await act(async () => root.unmount()); client.clear(); container.remove(); vi.clearAllMocks();
});
describe("cached file selection and retained trash", () => {
it("does not report empty trash or no saved files while loading or after a list failure", async () => {
let rejectList!: (error: Error) => void;
api.list.mockImplementationOnce(() => new Promise((_resolve, reject) => { rejectList = reject; }));
await click(button("Trash")!);
expect(container.textContent).toContain("Loading trash…");
expect(container.textContent).not.toContain("Trash is empty.");
expect(container.textContent).not.toContain("No saved files");
await act(async () => { rejectList(new Error("Storage unavailable")); });
await settle();
expect(container.textContent).toContain("Trash could not be loaded.");
expect(container.textContent).toContain("Storage unavailable");
expect(container.textContent).not.toContain("Trash is empty.");
expect(container.textContent).not.toContain("No saved files");
});
it("does not flash an old preview error while retrying a cached file", async () => {
const row = (path: string) => container.querySelector<HTMLElement>(`[data-file-tree-path="${path}"]`)!;
await click(row("a.txt"));

View File

@ -82,6 +82,8 @@ export function WorkFolderBrowser({ owner, exampleFiles, readOnly = false, fillH
: saving ? "Saving…"
: failures.length > 0 ? "Run save failed"
: !lastSaved && statuses.some((status) => status.active) ? "Waiting for first save"
: !lastSaved && !exampleFiles && filesQuery.isPending ? "Loading files…"
: !lastSaved && filesQuery.isError && !filesQuery.data ? "File list unavailable"
: lastSaved || lastOperation || files.length > 0 ? "Saved"
: "No saved files";
const disabled = mutation.isPending || Boolean(exampleFiles);
@ -117,7 +119,7 @@ export function WorkFolderBrowser({ owner, exampleFiles, readOnly = false, fillH
</ul>
</div>}
<p className="sr-only" aria-live="polite">{announcement}</p>
{trash ? <TabsContent value="trash" className={cn("overflow-auto", fillHeight ? "min-h-0 flex-1" : "max-h-96")}><p className="mb-3 text-sm text-muted-foreground">Deleted cached files are retained here. Restore them to return them to Files.</p>{files.length === 0 ? <p className="text-sm text-muted-foreground">Trash is empty.</p> : files.map((file) => <div key={file.id} className="flex items-center gap-2 border-b py-2">
{trash ? <TabsContent value="trash" className={cn("overflow-auto", fillHeight ? "min-h-0 flex-1" : "max-h-96")}><p className="mb-3 text-sm text-muted-foreground">Deleted cached files are retained here. Restore them to return them to Files.</p>{!exampleFiles && filesQuery.isPending ? <p role="status" className="text-sm text-muted-foreground">Loading trash…</p> : filesQuery.isError && !filesQuery.data ? <p className="text-sm text-muted-foreground">Trash could not be loaded.</p> : files.length === 0 ? <p className="text-sm text-muted-foreground">Trash is empty.</p> : files.map((file) => <div key={file.id} className="flex items-center gap-2 border-b py-2">
<span className="min-w-0 flex-1 truncate text-sm">{file.path}</span>{canManageTrash && <Button size="sm" variant="outline" disabled={disabled} onClick={() => mutation.mutate({ type: "restore", fileId: file.id })}><RotateCcw aria-hidden />Restore</Button>}
{!readOnly && <AlertDialog><AlertDialogTrigger asChild><Button size="sm" variant="ghost" disabled={disabled}>Purge…</Button></AlertDialogTrigger>
<AlertDialogContent><AlertDialogHeader><AlertDialogTitle>Permanently delete {file.path}?</AlertDialogTitle>