feat(files): the Downloads page goes — the tray, a Storage tab and a Downloads settings tab take its place
- The Downloads module, its rail icon and badge are gone. A bookmark, the remembered page and an old
history entry that name it land on Files (urlState.resolvePage); `g d` goes to Files too.
- Users page: a Storage tab — the disk in numbers, every user's use against their own limit with
their file count, every user's downloads; aligned tables, one menu per row (edit / reset quota,
open source, copy, the owner's quota). The admin storage rows carry quota + file count, the admin
job list its owner.
- Settings: a Downloads tab — the download formats in one list (duplicate a built-in, edit or delete
your own) and the site sign-ins, moved from Account.
- "View" on an added download, a clip or a converting upload opens the tray on that row (unfolded,
back if closed; a finished one opens as a file; under a player it opens without taking focus).
- The download dialog opens on the format used last.
- DataTable: `pager={false}` for a short list, `cardRow` to give a lone "…" its own card line.
- Welcome and README speak of Files; a new Files screenshot.
This commit is contained in:
@@ -12,6 +12,7 @@ import { api, type QueueJob } from "../lib/api";
|
||||
|
||||
const h = vi.hoisted(() => ({
|
||||
jobs: [] as unknown[],
|
||||
updatedAt: 0,
|
||||
openFile: vi.fn(),
|
||||
share: vi.fn(async () => "https://x/watch/t"),
|
||||
notify: vi.fn(),
|
||||
@@ -27,7 +28,13 @@ vi.mock("react-i18next", async (importOriginal) => ({
|
||||
}),
|
||||
}));
|
||||
vi.mock("../lib/useLiveQuery", () => ({
|
||||
useLiveQuery: () => ({ data: h.jobs, isError: false, isLoading: false, refetch: vi.fn() }),
|
||||
useLiveQuery: () => ({
|
||||
data: h.jobs,
|
||||
dataUpdatedAt: h.updatedAt,
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
refetch: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
// The prefs as state over a map that outlives a mount — a reload, as far as the tray can tell. How
|
||||
// usePref stores them is its own business (usePref.dom.test).
|
||||
@@ -57,11 +64,13 @@ vi.mock("../lib/notifications", async (importOriginal) => ({
|
||||
}));
|
||||
|
||||
import DownloadTray from "./DownloadTray";
|
||||
import { noteQueue, resetQueueWatch } from "../lib/downloadQueue";
|
||||
import { noteQueue, resetQueueWatch, showInTray } from "../lib/downloadQueue";
|
||||
import { pushLayer, removeLayer } from "../lib/layerStack";
|
||||
|
||||
beforeEach(() => {
|
||||
resetQueueWatch();
|
||||
h.prefs.clear();
|
||||
h.updatedAt = 0;
|
||||
globalThis.ResizeObserver ??= class {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
@@ -265,4 +274,62 @@ describe("DownloadTray", () => {
|
||||
fireEvent.click(screen.getByRole("button", { name: "tray.close" }));
|
||||
expect(document.activeElement?.id).toBe("content");
|
||||
});
|
||||
|
||||
describe("View on a job just started", () => {
|
||||
it("brings a closed tray back, on the job's row", async () => {
|
||||
poll([job(3, "error")]);
|
||||
const view = mount();
|
||||
fireEvent.click(screen.getByRole("button", { name: "tray.close" }));
|
||||
expect(tray()).toBeNull();
|
||||
act(() => showInTray(3));
|
||||
view.rerender();
|
||||
expect(tray()).not.toBeNull();
|
||||
await waitFor(() => expect(document.activeElement).toBe(rowOf("Clip 3")));
|
||||
});
|
||||
|
||||
it("unfolds a folded tray onto the row", async () => {
|
||||
poll([job(1, "running"), job(2, "queued")]);
|
||||
mount();
|
||||
fireEvent.click(screen.getByRole("button", { name: "tray.fold" }));
|
||||
act(() => showInTray(2));
|
||||
await waitFor(() => expect(document.activeElement).toBe(rowOf("Clip 2")));
|
||||
});
|
||||
|
||||
it("asked from inside a player, it opens under it but leaves the keyboard there", async () => {
|
||||
poll([job(1, "running"), job(2, "queued")]);
|
||||
mount();
|
||||
fireEvent.click(screen.getByRole("button", { name: "tray.fold" }));
|
||||
const player = document.createElement("button");
|
||||
document.body.append(player);
|
||||
player.focus();
|
||||
const layer = pushLayer();
|
||||
act(() => showInTray(2));
|
||||
await waitFor(() => expect(screen.getByText("Clip 2")).toBeTruthy());
|
||||
expect(document.activeElement).toBe(player);
|
||||
removeLayer(layer);
|
||||
player.remove();
|
||||
});
|
||||
|
||||
it("a job done before the tray saw it run opens as a file", () => {
|
||||
// The queue's first look only learns what is there: this one never ran in this tab.
|
||||
poll([job(5, "done", { folder_id: 7 })]);
|
||||
mount();
|
||||
act(() => showInTray(5));
|
||||
expect(h.openFile).toHaveBeenCalledWith(5, 7);
|
||||
expect(tray()).toBeNull();
|
||||
});
|
||||
|
||||
it("a job a later poll does not have is let go — nothing opens, nothing waits for it", () => {
|
||||
poll([job(1, "running")]);
|
||||
const view = mount();
|
||||
fireEvent.click(screen.getByRole("button", { name: "tray.fold" }));
|
||||
act(() => showInTray(9));
|
||||
h.updatedAt = Date.now() + 1000;
|
||||
poll([job(1, "running")], view);
|
||||
// Had the ask stayed, the job turning up later would pull the tray open on it.
|
||||
poll([job(1, "running"), job(9, "running")], view);
|
||||
expect(screen.queryByText("Clip 9")).toBeNull();
|
||||
expect(h.openFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user