Skip to content

Commit 6236ad2

Browse files
fix: run onUploadBegin (#952)
Co-authored-by: Mark R. Florkowski <[email protected]>
1 parent d0b3685 commit 6236ad2

File tree

6 files changed

+133
-34
lines changed

6 files changed

+133
-34
lines changed

.changeset/spotty-spoons-chew.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"uploadthing": patch
3+
---
4+
5+
fix: run `onUploadBegin`

docs/src/app/(docs)/api-reference/client/page.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ is an options object:
5555
<Property name="signal" type="AbortSignal" since="6.7">
5656
An abort signal to abort the upload.
5757
</Property>
58-
<Property name="onUploadBegin" type="({ file }) => void" since="5.4">
58+
<Property name="onUploadBegin" type="({ file: string }) => void" since="5.4">
5959
Callback function called after the presigned URLs have been retrieved, just before
60-
the files are uploaded to the storage provider.
60+
the file is uploaded. Called once per file.
6161
</Property>
6262
<Property
6363
name="onUploadProgress"

docs/src/app/(docs)/api-reference/react/page.mdx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -264,9 +264,9 @@ export const OurUploadButton = () => (
264264
returned are the files that will be uploaded, meaning you can use this to
265265
e.g. rename or resize the files.
266266
</Property>
267-
<Property name="onUploadBegin" type="function" since="5.4">
268-
Callback function called after the presigned URLs have been retrieved, just
269-
before the files are uploaded to the storage provider.
267+
<Property name="onUploadBegin" type="({ file: string }) => void" since="5.4">
268+
Callback function called after the presigned URLs have been retrieved, just before
269+
the file is uploaded. Called once per file.
270270
</Property>
271271
<Property name="disabled" type="boolean" since="6.7" defaultValue="false">
272272
Disables the button.
@@ -405,9 +405,9 @@ export const OurUploadDropzone = () => (
405405
returned are the files that will be uploaded, meaning you can use this to
406406
e.g. rename or resize the files.
407407
</Property>
408-
<Property name="onUploadBegin" type="function" since="5.4">
409-
Callback function called after the presigned URLs have been retrieved, just
410-
before the files are uploaded to the storage provider.
408+
<Property name="onUploadBegin" type="({ file: string }) => void" since="5.4">
409+
Callback function called after the presigned URLs have been retrieved, just before
410+
the file is uploaded. Called once per file.
411411
</Property>
412412
<Property name="disabled" type="boolean" since="6.7" defaultValue="false">
413413
Disables the button.
@@ -512,7 +512,7 @@ is an options object:
512512
Callback function that gets continuously called as the file is uploaded to
513513
the storage provider.
514514
</Property>
515-
<Property name="onUploadBegin" type="function" since="5.4">
515+
<Property name="onUploadBegin" type="({ file: string }) => void" since="5.4">
516516
Callback function called after the presigned URLs have been retrieved, just
517517
before the files are uploaded to the storage provider.
518518
</Property>
@@ -565,8 +565,8 @@ export function MultiUploader() {
565565
onUploadError: () => {
566566
alert("error occurred while uploading");
567567
},
568-
onUploadBegin: () => {
569-
alert("upload has begun");
568+
onUploadBegin: ({ file }) => {
569+
console.log("upload has begun for", file);
570570
},
571571
});
572572

packages/react/test/upload-button.test.tsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ const testRouter = {
3131
pdf: f({ "application/pdf": {} }).onUploadComplete(noop),
3232
multi: f({ image: { maxFileCount: 4 } }).onUploadComplete(noop),
3333
};
34-
const routeHandler = createRouteHandler({ router: testRouter });
34+
const routeHandler = createRouteHandler({
35+
router: testRouter,
36+
config: {
37+
token:
38+
"eyJhcHBJZCI6ImFwcC0xIiwiYXBpS2V5Ijoic2tfZm9vIiwicmVnaW9ucyI6WyJmcmExIl19",
39+
},
40+
});
3541
const UploadButton = generateUploadButton<typeof testRouter>();
3642

3743
const utGet = vi.fn<(req: Request) => void>();
@@ -44,13 +50,16 @@ const server = setupServer(
4450
return routeHandler(request);
4551
}),
4652
http.post("/api/uploadthing", async ({ request }) => {
47-
const body = await request.json();
53+
const body = await request.clone().json();
4854
utPost({ request, body });
49-
return HttpResponse.json([
50-
// empty array, we're not testing the upload endpoint here
51-
// we have other tests for that...
52-
]);
55+
return routeHandler(request);
5356
}),
57+
http.all<{ key: string }>(
58+
"https://fra1.ingest.uploadthing.com/:key",
59+
({ request, params }) => {
60+
return HttpResponse.json({ url: "https://utfs.io/f/" + params.key });
61+
},
62+
),
5463
);
5564

5665
beforeAll(() => server.listen());
@@ -203,6 +212,25 @@ describe("UploadButton - lifecycle hooks", () => {
203212
);
204213
});
205214
});
215+
216+
it("onUploadBegin runs before uploading", async () => {
217+
const onUploadBegin = vi.fn();
218+
const utils = render(
219+
<UploadButton endpoint="multi" onUploadBegin={onUploadBegin} />,
220+
);
221+
await waitFor(() => {
222+
expect(utils.getByText("Choose File(s)")).toBeInTheDocument();
223+
});
224+
225+
fireEvent.change(utils.getByLabelText("Choose File(s)"), {
226+
target: { files: [new File([""], "foo.png"), new File([""], "bar.png")] },
227+
});
228+
await waitFor(() => {
229+
expect(onUploadBegin).toHaveBeenCalledTimes(2);
230+
});
231+
expect(onUploadBegin).toHaveBeenCalledWith("foo.png");
232+
expect(onUploadBegin).toHaveBeenCalledWith("bar.png");
233+
});
206234
});
207235

208236
describe("UploadButton - Theming", () => {

packages/uploadthing/src/internal/upload.browser.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -146,22 +146,32 @@ export const uploadFilesInternal = <
146146
Micro.forEach(
147147
presigneds,
148148
(presigned, i) =>
149-
uploadFile<TRouter, TEndpoint, TServerOutput>(
150-
opts.files[i],
151-
presigned,
152-
{
153-
onUploadProgress: (ev) => {
154-
totalLoaded += ev.delta;
155-
opts.onUploadProgress?.({
156-
file: opts.files[i],
157-
progress: Math.round((ev.loaded / opts.files[i].size) * 100),
158-
loaded: ev.loaded,
159-
delta: ev.delta,
160-
totalLoaded,
161-
totalProgress: Math.round((totalLoaded / totalSize) * 100),
162-
});
163-
},
164-
},
149+
Micro.flatMap(
150+
Micro.sync(() =>
151+
opts.onUploadBegin?.({ file: opts.files[i].name }),
152+
),
153+
() =>
154+
uploadFile<TRouter, TEndpoint, TServerOutput>(
155+
opts.files[i],
156+
presigned,
157+
{
158+
onUploadProgress: (ev) => {
159+
totalLoaded += ev.delta;
160+
opts.onUploadProgress?.({
161+
file: opts.files[i],
162+
progress: Math.round(
163+
(ev.loaded / opts.files[i].size) * 100,
164+
),
165+
loaded: ev.loaded,
166+
delta: ev.delta,
167+
totalLoaded,
168+
totalProgress: Math.round(
169+
(totalLoaded / totalSize) * 100,
170+
),
171+
});
172+
},
173+
},
174+
),
165175
),
166176
{ concurrency: 6 },
167177
),

packages/uploadthing/test/client.test.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import type { AddressInfo } from "node:net";
44
import express from "express";
5-
import { describe, expect, expectTypeOf, it as rawIt } from "vitest";
5+
import { describe, expect, expectTypeOf, it as rawIt, vi } from "vitest";
66

77
import { genUploader } from "../src/client";
88
import { createRouteHandler, createUploadthing } from "../src/express";
@@ -34,6 +34,13 @@ export const setupUTServer = async () => {
3434
})
3535
.onUploadError(onErrorMock)
3636
.onUploadComplete(uploadCompleteMock),
37+
multi: f({ text: { maxFileSize: "16MB", maxFileCount: 2 } })
38+
.middleware((opts) => {
39+
middlewareMock(opts);
40+
return {};
41+
})
42+
.onUploadError(onErrorMock)
43+
.onUploadComplete(uploadCompleteMock),
3744
withServerData: f(
3845
{ text: { maxFileSize: "4MB" } },
3946
{ awaitServerData: true },
@@ -264,6 +271,21 @@ describe("uploadFiles", () => {
264271
await close();
265272
});
266273

274+
it("handles too many files errors", async ({ db }) => {
275+
const { uploadFiles, close } = await setupUTServer();
276+
277+
const file1 = new File(["foo"], "foo.txt", { type: "text/plain" });
278+
const file2 = new File(["bar"], "bar.txt", { type: "text/plain" });
279+
280+
await expect(
281+
uploadFiles("foo", { files: [file1, file2] }),
282+
).rejects.toThrowErrorMatchingInlineSnapshot(
283+
`[UploadThingError: Invalid config: FileCountMismatch]`,
284+
);
285+
286+
await close();
287+
});
288+
267289
it("handles invalid file type errors", async ({ db }) => {
268290
const { uploadFiles, close } = await setupUTServer();
269291

@@ -277,4 +299,38 @@ describe("uploadFiles", () => {
277299

278300
await close();
279301
});
302+
303+
it("runs onUploadBegin before uploading (single file)", async () => {
304+
const { uploadFiles, close } = await setupUTServer();
305+
306+
const file = new File(["foo"], "foo.txt", { type: "text/plain" });
307+
const onUploadBegin = vi.fn();
308+
309+
await uploadFiles("foo", {
310+
files: [file],
311+
onUploadBegin,
312+
});
313+
314+
expect(onUploadBegin).toHaveBeenCalledWith({ file: "foo.txt" });
315+
316+
await close();
317+
});
318+
319+
it("runs onUploadBegin before uploading (multi file)", async () => {
320+
const { uploadFiles, close } = await setupUTServer();
321+
322+
const file1 = new File(["foo"], "foo.txt", { type: "text/plain" });
323+
const file2 = new File(["bar"], "bar.txt", { type: "text/plain" });
324+
const onUploadBegin = vi.fn();
325+
326+
await uploadFiles("multi", {
327+
files: [file1, file2],
328+
onUploadBegin,
329+
});
330+
331+
expect(onUploadBegin).toHaveBeenCalledWith({ file: "foo.txt" });
332+
expect(onUploadBegin).toHaveBeenCalledWith({ file: "bar.txt" });
333+
334+
await close();
335+
});
280336
});

0 commit comments

Comments
 (0)