feat(files): folders managed in place, a grid per folder, drop to upload, open on one file
- Own folders are made, renamed and deleted from the rail (right click, long press, Shift+F10) or the "…" beside an open folder's name; any folder's retention is a submenu (7/30/90 days, until deleted). Deleting confirms where its files go. - List or grid per folder, sharing one sort; the grid is the one media card. - A file dropped anywhere on the page opens Upload with it, bound for the folder on screen. - A link (?item=) or the upload notice opens Files on the file: its page turned to, the keyboard on it, a ring round it. - An in-place name field left by Enter or Escape hands the focus back to what replaced it (the row, the entry, the "…") instead of dropping it on <body>. - The table's saved page size is per account.
This commit is contained in:
@@ -16,6 +16,7 @@ const h = vi.hoisted(() => ({
|
||||
filesUsage: vi.fn(async () => ({ upload_max_bytes: 1_000_000 })),
|
||||
notify: vi.fn(),
|
||||
navigateTo: vi.fn(),
|
||||
openFile: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", async (importOriginal) => {
|
||||
@@ -37,12 +38,25 @@ vi.mock("../lib/notifications", async (importOriginal) => ({
|
||||
vi.mock("../lib/nav", () => ({ navigateTo: h.navigateTo }));
|
||||
|
||||
import UploadDialog from "./UploadDialog";
|
||||
import { FilesActionsContext } from "./filesContext";
|
||||
|
||||
const wrap = (onClose = vi.fn(), folderId: number | null = null) => {
|
||||
const wrap = (
|
||||
onClose = vi.fn(),
|
||||
folderId: number | null = null,
|
||||
initialFile: File | null = null,
|
||||
) => {
|
||||
const qc = new QueryClient();
|
||||
const files = {
|
||||
setSearch: vi.fn(),
|
||||
setView: vi.fn(),
|
||||
openFile: h.openFile,
|
||||
consumeFocus: vi.fn(),
|
||||
};
|
||||
return render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<UploadDialog folderId={folderId} onClose={onClose} />
|
||||
<FilesActionsContext.Provider value={files}>
|
||||
<UploadDialog folderId={folderId} initialFile={initialFile} onClose={onClose} />
|
||||
</FilesActionsContext.Provider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
};
|
||||
@@ -52,6 +66,8 @@ const fileInput = () => document.querySelector('input[type="file"]') as HTMLInpu
|
||||
|
||||
beforeEach(() => {
|
||||
h.uploadFile.mockClear();
|
||||
h.openFile.mockClear();
|
||||
h.navigateTo.mockClear();
|
||||
h.notify.mockClear();
|
||||
h.filesUsage.mockClear();
|
||||
h.filesUsage.mockResolvedValue({ upload_max_bytes: 1_000_000 });
|
||||
@@ -99,14 +115,17 @@ describe("UploadDialog", () => {
|
||||
});
|
||||
|
||||
it("puts the file in the folder it was opened from, and its notice opens where it now is", async () => {
|
||||
// Done: the file is in Files. Still converting: it is in the download queue until it is done.
|
||||
// Done: Files opens on the file itself, in its folder. Still converting: it is in the download
|
||||
// queue until it is done.
|
||||
h.uploadFile.mockResolvedValueOnce({ id: 42, status: "done", folder_id: 5 } as never);
|
||||
wrap(vi.fn(), 5);
|
||||
fireEvent.change(fileInput(), { target: { files: [new File(["x"], "a.mp4")] } });
|
||||
fireEvent.click(screen.getByText("downloads.upload.submit"));
|
||||
await waitFor(() => expect(h.notify).toHaveBeenCalledTimes(1));
|
||||
expect((h.uploadFile.mock.calls[0]![1] as { folderId?: number }).folderId).toBe(5);
|
||||
(h.notify.mock.calls[0]![0] as { action: { onClick: () => void } }).action.onClick();
|
||||
expect(h.navigateTo).toHaveBeenLastCalledWith("files");
|
||||
expect(h.openFile).toHaveBeenLastCalledWith(42, 5);
|
||||
expect(h.navigateTo).not.toHaveBeenCalled();
|
||||
|
||||
cleanup();
|
||||
h.notify.mockClear();
|
||||
@@ -119,6 +138,17 @@ describe("UploadDialog", () => {
|
||||
expect(h.navigateTo).toHaveBeenLastCalledWith("downloads");
|
||||
});
|
||||
|
||||
it("opens ready to send a file dropped on the page", async () => {
|
||||
const dropped = new File(["dropped"], "recept.pdf");
|
||||
wrap(vi.fn(), null, dropped);
|
||||
expect(screen.getByText("recept.pdf")).toBeTruthy();
|
||||
const btn = screen.getByText("downloads.upload.submit").closest("button")!;
|
||||
expect(btn.disabled).toBe(false);
|
||||
fireEvent.click(btn);
|
||||
await waitFor(() => expect(h.uploadFile).toHaveBeenCalledTimes(1));
|
||||
expect(h.uploadFile.mock.calls[0]![0]).toBe(dropped);
|
||||
});
|
||||
|
||||
it("keeps Upload disabled until a file is chosen", () => {
|
||||
wrap();
|
||||
const btn = screen.getByText("downloads.upload.submit").closest("button")!;
|
||||
|
||||
Reference in New Issue
Block a user