diff --git a/packages/react/src/components/button.tsx b/packages/react/src/components/button.tsx index c0ca05ec34..ab76d6e4cc 100644 --- a/packages/react/src/components/button.tsx +++ b/packages/react/src/components/button.tsx @@ -3,6 +3,11 @@ import type { CSSProperties } from "react"; import { useCallback, useMemo, useRef, useState } from "react"; +import type { + ContentField, + ErrorMessage, + StyleField, +} from "@uploadthing/shared"; import { allowedContentTextLabelGenerator, contentFieldToContent, @@ -15,11 +20,6 @@ import { styleFieldToCssObject, UploadAbortedError, } from "@uploadthing/shared"; -import type { - ContentField, - ErrorMessage, - StyleField, -} from "@uploadthing/shared"; import type { FileRouter } from "uploadthing/types"; import type { UploadthingComponentProps } from "../types"; @@ -116,6 +116,7 @@ export function UploadButton< { signal: acRef.current.signal, headers: $props.headers, + onFileUploadComplete: $props.onFileUploadComplete, onClientUploadComplete: (res) => { if (fileInputRef.current) { fileInputRef.current.value = ""; diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index c3d3b295e9..011c911cf8 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -53,6 +53,25 @@ export type UseUploadthingProps< TFileRoute extends AnyFileRoute, TServerOutput = inferEndpointOutput, > = { + /** + * Called when a file upload is completed + */ + onFileUploadComplete?: + | ((_: { + /** + * The response from the upload + */ + fileData: ClientUploadedFileData; + /** + * The file object that was uploaded + */ + file: File; + /** + * All the files that are in this upload + */ + files: File[]; + }) => void) + | undefined; /** * Called when the upload is submitted and the server is about to be queried for presigned URLs * Can be used to modify the files before they are uploaded, e.g. renaming them diff --git a/packages/react/src/use-uploadthing.ts b/packages/react/src/use-uploadthing.ts index e52199d4ec..23e6ed9287 100644 --- a/packages/react/src/use-uploadthing.ts +++ b/packages/react/src/use-uploadthing.ts @@ -91,6 +91,7 @@ function useUploadThingInternal< signal: opts?.signal, headers: opts?.headers, files, + onFileUploadComplete: opts?.onFileUploadComplete, onUploadProgress: (progress) => { if (!opts?.onUploadProgress) return; fileProgress.current.set(progress.file, progress.progress); diff --git a/packages/uploadthing/src/_internal/upload-browser.ts b/packages/uploadthing/src/_internal/upload-browser.ts index c485a28f69..f1e8d4c4e5 100644 --- a/packages/uploadthing/src/_internal/upload-browser.ts +++ b/packages/uploadthing/src/_internal/upload-browser.ts @@ -108,10 +108,8 @@ export const uploadFile = < file: File, presigned: NewPresignedUrl, opts: { - onUploadProgress?: (progressEvent: { - loaded: number; - delta: number; - }) => void; + onFileUploadComplete?: (_: ClientUploadedFileData) => void; + onUploadProgress?: (_: { loaded: number; delta: number }) => void; }, ) => fetchEff(presigned.url, { method: "HEAD" }).pipe( @@ -156,6 +154,7 @@ export const uploadFile = < type: file.type, fileHash: uploadResponse.fileHash, })), + Micro.tap((fileData) => opts.onFileUploadComplete?.(fileData)), ); export const uploadFilesInternal = < @@ -204,6 +203,13 @@ export const uploadFilesInternal = < opts.files[i]!, presigned, { + onFileUploadComplete: (result) => { + opts.onFileUploadComplete?.({ + fileData: result, + file: opts.files[i]!, + files: opts.files, + }); + }, onUploadProgress: (ev) => { totalLoaded += ev.delta; opts.onUploadProgress?.({ diff --git a/packages/uploadthing/src/types.ts b/packages/uploadthing/src/types.ts index 6bdf9f2e14..74852c89b6 100644 --- a/packages/uploadthing/src/types.ts +++ b/packages/uploadthing/src/types.ts @@ -9,6 +9,7 @@ import type { import type { LogFormat } from "./_internal/logger"; import type { AnyFileRoute, FileRoute } from "./_internal/types"; +import type { ClientUploadedFileData } from "./types"; export * from "./sdk/types"; @@ -124,6 +125,25 @@ export type UploadFilesOptions = { totalProgress: number; }) => void) | undefined; + /** + * Called when a file upload is completed + */ + onFileUploadComplete?: + | ((_: { + /** + * The response from the upload + */ + fileData: ClientUploadedFileData>; + /** + * The file object that was uploaded + */ + file: File; + /** + * All the files that are in this upload + */ + files: File[]; + }) => void) + | undefined; /** * This option has been moved to your serverside route config. * Please opt-in by setting `awaitServerData: false` in your route