Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 43 additions & 23 deletions frontend/src/components/editor/file-tree/upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type DropzoneOptions, useDropzone } from "react-dropzone";
import { toast } from "@/components/ui/use-toast";
import { useRequestClient } from "@/core/network/requests";
import { serializeBlob } from "@/utils/blob";
import { withLoadingToast } from "@/utils/download";
import { Logger } from "@/utils/Logger";
import { type FilePath, PathBuilder } from "@/utils/paths";
import { refreshRoot } from "./state";
Expand Down Expand Up @@ -40,30 +41,49 @@ export function useFileExplorerUpload(options: DropzoneOptions = {}) {
});
},
onDrop: async (acceptedFiles) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to check, this covers clicking on the upload button on the panel?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it covers both click to upload and drag-drop.

for (const file of acceptedFiles) {
// We strip the leading slash since File.path can return
// `/path/to/file`.
const filePath = stripLeadingSlash(getPath(file));
let directoryPath = "" as FilePath;
if (filePath) {
directoryPath =
PathBuilder.guessDeliminator(filePath).dirname(filePath);
}

// File contents are sent base64-encoded to support arbitrary
// bytes data
//
// get the raw base64-encoded data from a string starting with
// data:*/*;base64,
const base64 = (await serializeBlob(file)).split(",")[1];
await sendCreateFileOrFolder({
path: directoryPath,
type: "file",
name: file.name,
contents: base64,
});
if (acceptedFiles.length === 0) {
return;
}
await refreshRoot();
const isSingle = acceptedFiles.length === 1;
const loadingTitle = isSingle
? "Uploading file..."
: "Uploading files...";
const finishTitle = isSingle
? "File uploaded"
: `${acceptedFiles.length} files uploaded`;

await withLoadingToast(
loadingTitle,
async (progress) => {
progress.addTotal(acceptedFiles.length);
for (const file of acceptedFiles) {
// We strip the leading slash since File.path can return
// `/path/to/file`.
const filePath = stripLeadingSlash(getPath(file));
let directoryPath = "" as FilePath;
if (filePath) {
directoryPath =
PathBuilder.guessDeliminator(filePath).dirname(filePath);
}

// File contents are sent base64-encoded to support arbitrary
// bytes data
//
// get the raw base64-encoded data from a string starting with
// data:*/*;base64,
const base64 = (await serializeBlob(file)).split(",")[1];
await sendCreateFileOrFolder({
path: directoryPath,
type: "file",
name: file.name,
contents: base64,
});
progress.increment(1);
}
await refreshRoot();
},
finishTitle,
);
},
...options,
});
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/utils/__tests__/download.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,36 @@ describe("withLoadingToast", () => {
);
});

it("should show a finish toast when finishTitle is provided", async () => {
await withLoadingToast(
"Uploading files...",
async () => "done",
"Upload complete",
);

expect(toast).toHaveBeenCalledTimes(2);
expect(toast).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
title: "Upload complete",
}),
);
});

it("should not show a finish toast when the operation fails", async () => {
await expect(
withLoadingToast(
"Uploading files...",
async () => {
throw new Error("Upload failed");
},
"Upload complete",
),
).rejects.toThrow("Upload failed");

expect(toast).toHaveBeenCalledTimes(1);
});

it("should wait for the async function to complete", async () => {
const events: string[] = [];

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/utils/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ToastProgress } from "./toast-progress";
export async function withLoadingToast<T>(
title: string,
fn: (progress: ProgressState) => Promise<T>,
finishTitle?: string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if you can do onFinish?: Toast, so it's more flexible how you can call this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, updated to onFinish?: ToastConfig (replacing finishTitle?: string) so the finish toast is configurable beyond just the title.

): Promise<T> {
const progress = ProgressState.indeterminate();
const loadingToast = toast({
Expand All @@ -30,6 +31,9 @@ export async function withLoadingToast<T>(
try {
const result = await fn(progress);
loadingToast.dismiss();
if (finishTitle) {
toast({ title: finishTitle });
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of creating another toast, you can update the existing toast, toast.update. And maybe set a short timeout so the toast can be seen before dismissed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated this part: I now use loadingToast.update() instead of creating a secod toast, and set a short finish duration (1200ms)

return result;
} catch (error) {
loadingToast.dismiss();
Expand Down