feat(files): the Files page — the library in one aligned list, its folders in a rail
Files is the one library: every done file, the ones shared with me, or one folder's, picked in a side rail (sources, own folders, the storage meter) and mirrored to ?folder=. One table with every kind of value in its own column; Copy link, Share and Save in sight, every action on a right click: play, cut, rename in the row, move to an own folder or back home, keep, details, re-download, delete. The Downloads page keeps only the queue until the tray replaces it. The old /api/downloads library routes are gone: list shared, upload, the details PATCH, delete, the file and the usage now live under /api/files, and a listed file says which source it came from. The job's "expired" flag is gone with the state it described.
This commit is contained in:
@@ -24,6 +24,7 @@ vi.mock("../activeAccount", () => ({
|
||||
|
||||
import { setDebugSession } from "../log";
|
||||
import { downloadsApi } from "./downloads";
|
||||
import { filesApi } from "./files";
|
||||
import {
|
||||
beacon,
|
||||
req,
|
||||
@@ -48,10 +49,13 @@ class FakeXHR {
|
||||
onabort: (() => void) | null = null;
|
||||
sent = false;
|
||||
body: unknown = null;
|
||||
url = "";
|
||||
constructor() {
|
||||
FakeXHR.last = this;
|
||||
}
|
||||
open() {}
|
||||
open(_method: string, url: string) {
|
||||
this.url = url;
|
||||
}
|
||||
setRequestHeader(k: string, v: string) {
|
||||
// A real XHR COMBINES values for a name it already has, case-insensitively, rather than
|
||||
// overwriting — so a plain assignment here would hide exactly the duplicate-header bug the
|
||||
@@ -470,10 +474,10 @@ describe("the debug session a tab records into", () => {
|
||||
// body, which the server reads and spools BEFORE its auth dependency runs; the raw body is what lets
|
||||
// `current_user` resolve first. A revert to FormData here would silently re-open that, and the
|
||||
// backend's own test only sees the shape the client actually sends.
|
||||
describe("uploadDownload's wire shape", () => {
|
||||
describe("uploadFile's wire shape", () => {
|
||||
it("sends the File as the raw body, with the names as percent-encoded headers", () => {
|
||||
const file = new File(["abc"], "nyaralás videó.mp4", { type: "video/mp4" });
|
||||
void downloadsApi.uploadDownload(file, { displayName: "Nyaralás 2026" });
|
||||
void filesApi.uploadFile(file, { displayName: "Nyaralás 2026" });
|
||||
const x = FakeXHR.last;
|
||||
expect(x.body).toBe(file);
|
||||
expect(x.body).not.toBeInstanceOf(FormData);
|
||||
@@ -484,8 +488,15 @@ describe("uploadDownload's wire shape", () => {
|
||||
});
|
||||
|
||||
it("omits the display-name header when there is no display name", () => {
|
||||
void downloadsApi.uploadDownload(new File(["abc"], "clip.mp4"));
|
||||
void filesApi.uploadFile(new File(["abc"], "clip.mp4"));
|
||||
expect(FakeXHR.last.headers["X-Upload-Display-Name"]).toBeUndefined();
|
||||
expect(FakeXHR.last.headers["X-Upload-Folder"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("names the folder it goes into, when it has one", () => {
|
||||
void filesApi.uploadFile(new File(["abc"], "clip.mp4"), { folderId: 7 });
|
||||
expect(FakeXHR.last.headers["X-Upload-Folder"]).toBe("7");
|
||||
expect(FakeXHR.last.url).toBe("/api/files/upload");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user