Skip to content

Commit

Permalink
feat: add ProgressGranularity (#1151)
Browse files Browse the repository at this point in the history
Co-authored-by: Mark R. Florkowski <[email protected]>
  • Loading branch information
juliusmarminge and markflorkowski authored Feb 17, 2025
1 parent 5e3f815 commit 67c3b1c
Show file tree
Hide file tree
Showing 25 changed files with 216 additions and 149 deletions.
18 changes: 18 additions & 0 deletions .changeset/fair-tomatoes-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@uploadthing/svelte": minor
"@uploadthing/react": minor
"@uploadthing/solid": minor
"@uploadthing/vue": minor
"uploadthing": patch
"@uploadthing/shared": patch
---

feat: add `uploadProgressGranularity` option to control how granular progress events
are fired at

You can now set `uploadProgressGranularity` to `all`, `fine`, or `coarse` to control
how granular progress events are fired at.

- `all` will forward every event from the XHR upload
- `fine` will forward events for every 1% of progress
- `coarse` (default) will forward events for every 10% of progress
9 changes: 9 additions & 0 deletions docs/src/app/(docs)/api-reference/react/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ export const OurUploadButton = () => (
<Property name="onUploadAborted" type="function" since="6.7">
Callback function when that runs when an upload is aborted.
</Property>
<Property name="uploadProgressGranularity" type="'all' | 'fine' | 'coarse'" since="7.3" defaultValue="coarse">
The granularity of which progress events are fired. 'all' forwards every progress event, 'fine' forwards events for every 1% of progress, 'coarse' forwards events for every 10% of progress.
</Property>
<Property name="onUploadProgress" type="function" since="5.1">
Callback function that gets continuously called as the file is uploaded to
the storage provider.
Expand Down Expand Up @@ -422,6 +425,9 @@ export const OurUploadDropzone = () => (
<Property name="onUploadAborted" type="function" since="6.7">
Callback function when that runs when an upload is aborted.
</Property>
<Property name="uploadProgressGranularity" type="'all' | 'fine' | 'coarse'" since="7.3" defaultValue="coarse">
The granularity of which progress events are fired. 'all' forwards every progress event, 'fine' forwards events for every 1% of progress, 'coarse' forwards events for every 10% of progress.
</Property>
<Property name="onUploadProgress" type="function" since="5.1">
Callback function that gets continuously called as the file is uploaded to
the storage provider.
Expand Down Expand Up @@ -552,6 +558,9 @@ using a string literal parameter.
<Property name="onUploadAborted" type="function" since="6.7">
Callback function when that runs when an upload is aborted.
</Property>
<Property name="uploadProgressGranularity" type="'all' | 'fine' | 'coarse'" since="7.3" defaultValue="coarse">
The granularity of which progress events are fired. 'all' forwards every progress event, 'fine' forwards events for every 1% of progress, 'coarse' forwards events for every 10% of progress.
</Property>
<Property name="onUploadProgress" type="function" since="5.1">
Callback function that gets continuously called as the file is uploaded to
the storage provider.
Expand Down
24 changes: 16 additions & 8 deletions packages/react/src/components/button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import type { CSSProperties } from "react";
import { useCallback, useMemo, useRef, useState } from "react";

import {
Expand All @@ -24,7 +25,7 @@ import type { FileRouter } from "uploadthing/types";
import type { UploadthingComponentProps } from "../types";
import { __useUploadThingInternal } from "../use-uploadthing";
import { usePaste } from "../utils/usePaste";
import { Cancel, progressWidths, Spinner } from "./shared";
import { Cancel, Spinner } from "./shared";

type ButtonStyleFieldCallbackArgs = {
__runtime: "react";
Expand Down Expand Up @@ -123,6 +124,7 @@ export function UploadButton<
void $props.onClientUploadComplete?.(res);
setUploadProgress(0);
},
uploadProgressGranularity: $props.uploadProgressGranularity,
onUploadProgress: (p) => {
setUploadProgress(p);
$props.onUploadProgress?.(p);
Expand Down Expand Up @@ -247,7 +249,9 @@ export function UploadButton<
if (uploadProgress >= 100) return <Spinner />;
return (
<span className="z-50">
<span className="block group-hover:hidden">{uploadProgress}%</span>
<span className="block group-hover:hidden">
{Math.round(uploadProgress)}%
</span>
<Cancel cn={cn} className="hidden size-4 group-hover:block" />
</span>
);
Expand Down Expand Up @@ -312,17 +316,21 @@ export function UploadButton<
$props.className,
styleFieldToClassName($props.appearance?.container, styleFieldArg),
)}
style={styleFieldToCssObject($props.appearance?.container, styleFieldArg)}
style={
{
"--progress-width": `${uploadProgress}%`,
...styleFieldToCssObject($props.appearance?.container, styleFieldArg),
} as CSSProperties
}
data-state={state}
>
<label
className={cn(
"group relative flex h-10 w-36 cursor-pointer items-center justify-center overflow-hidden rounded-md text-white after:transition-[width] after:duration-500 focus-within:ring-2 focus-within:ring-blue-600 focus-within:ring-offset-2",
state === "disabled" && "cursor-not-allowed bg-blue-400",
state === "readying" && "cursor-not-allowed bg-blue-400",
state === "uploading" &&
`bg-blue-400 after:absolute after:left-0 after:h-full after:bg-blue-600 after:content-[''] ${progressWidths[uploadProgress]}`,
state === "ready" && "bg-blue-600",
"disabled:pointer-events-none",
"data-[state=disabled]:cursor-not-allowed data-[state=readying]:cursor-not-allowed",
"data-[state=disabled]:bg-blue-400 data-[state=ready]:bg-blue-600 data-[state=readying]:bg-blue-400 data-[state=uploading]:bg-blue-400",
"after:absolute after:left-0 after:h-full after:w-[var(--progress-width)] after:content-[''] data-[state=uploading]:after:bg-blue-600",
styleFieldToClassName($props.appearance?.button, styleFieldArg),
)}
style={styleFieldToCssObject($props.appearance?.button, styleFieldArg)}
Expand Down
26 changes: 17 additions & 9 deletions packages/react/src/components/dropzone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "react";
import type {
ChangeEvent,
CSSProperties,
DragEvent,
HTMLProps,
KeyboardEvent,
Expand Down Expand Up @@ -51,7 +52,7 @@ import type { FileRouter } from "uploadthing/types";
import type { UploadthingComponentProps } from "../types";
import { __useUploadThingInternal } from "../use-uploadthing";
import { usePaste } from "../utils/usePaste";
import { Cancel, progressWidths, Spinner } from "./shared";
import { Cancel, Spinner } from "./shared";

type DropzoneStyleFieldCallbackArgs = {
__runtime: "react";
Expand Down Expand Up @@ -155,6 +156,7 @@ export function UploadDropzone<
void $props.onClientUploadComplete?.(res);
setUploadProgress(0);
},
uploadProgressGranularity: $props.uploadProgressGranularity,
onUploadProgress: (p) => {
setUploadProgress(p);
$props.onUploadProgress?.(p);
Expand Down Expand Up @@ -279,7 +281,9 @@ export function UploadDropzone<
if (uploadProgress >= 100) return <Spinner />;
return (
<span className="z-50">
<span className="block group-hover:hidden">{uploadProgress}%</span>
<span className="block group-hover:hidden">
{Math.round(uploadProgress)}%
</span>
<Cancel cn={cn} className="hidden size-4 group-hover:block" />
</span>
);
Expand Down Expand Up @@ -367,16 +371,20 @@ export function UploadDropzone<

<button
className={cn(
"group relative mt-4 flex h-10 w-36 cursor-pointer items-center justify-center overflow-hidden rounded-md border-none text-base text-white after:transition-[width] after:duration-500 focus-within:ring-2 focus-within:ring-blue-600 focus-within:ring-offset-2",
state === "disabled" && "cursor-not-allowed bg-blue-400",
state === "readying" && "cursor-not-allowed bg-blue-400",
state === "uploading" &&
`bg-blue-400 after:absolute after:left-0 after:h-full after:bg-blue-600 after:content-[''] ${progressWidths[uploadProgress]}`,
state === "ready" && "bg-blue-600",
"group relative mt-4 flex h-10 w-36 items-center justify-center overflow-hidden rounded-md border-none text-base text-white",
"after:absolute after:left-0 after:h-full after:w-[var(--progress-width)] after:bg-blue-600 after:transition-[width] after:duration-500 after:content-['']",
"focus-within:ring-2 focus-within:ring-blue-600 focus-within:ring-offset-2",
"disabled:pointer-events-none",
"data-[state=disabled]:cursor-not-allowed data-[state=readying]:cursor-not-allowed",
"data-[state=disabled]:bg-blue-400 data-[state=ready]:bg-blue-600 data-[state=readying]:bg-blue-400 data-[state=uploading]:bg-blue-400",
styleFieldToClassName($props.appearance?.button, styleFieldArg),
)}
style={styleFieldToCssObject($props.appearance?.button, styleFieldArg)}
style={
{
"--progress-width": `${uploadProgress}%`,
...styleFieldToCssObject($props.appearance?.button, styleFieldArg),
} as CSSProperties
}
onClick={onUploadClick}
data-ut-element="button"
data-state={state}
Expand Down
14 changes: 0 additions & 14 deletions packages/react/src/components/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,3 @@ export function Cancel({
</svg>
);
}

export const progressWidths: Record<number, string> = {
0: "after:w-0",
10: "after:w-[10%]",
20: "after:w-[20%]",
30: "after:w-[30%]",
40: "after:w-[40%]",
50: "after:w-[50%]",
60: "after:w-[60%]",
70: "after:w-[70%]",
80: "after:w-[80%]",
90: "after:w-[90%]",
100: "after:w-[100%]",
};
9 changes: 9 additions & 0 deletions packages/react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
ExtendObjectIf,
FetchEsque,
MaybePromise,
ProgressGranularity,
UploadThingError,
} from "@uploadthing/shared";
import type {
Expand Down Expand Up @@ -63,6 +64,14 @@ export type UseUploadthingProps<
* Called when presigned URLs have been retrieved and the file upload is about to begin
*/
onUploadBegin?: ((fileName: string) => void) | undefined;
/**
* Control how granular the upload progress is reported
* - "all" - No filtering is applied, all progress events are reported
* - "fine" - Progress is reported in increments of 1%
* - "coarse" - Progress is reported in increments of 10%
* @default "coarse"
*/
uploadProgressGranularity?: ProgressGranularity | undefined;
/**
* Called continuously as the file is uploaded to the storage provider
*/
Expand Down
8 changes: 6 additions & 2 deletions packages/react/src/use-uploadthing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
import {
INTERNAL_DO_NOT_USE__fatalClientError,
resolveMaybeUrlArg,
roundProgress,
unwrap,
UploadAbortedError,
UploadThingError,
Expand Down Expand Up @@ -62,6 +63,7 @@ function useUploadThingInternal<
fetch: FetchEsque,
opts?: UseUploadthingProps<TRouter[TEndpoint]>,
) {
const progressGranularity = opts?.uploadProgressGranularity ?? "coarse";
const { uploadFiles, routeRegistry } = genUploader<TRouter>({
fetch,
url,
Expand Down Expand Up @@ -96,8 +98,10 @@ function useUploadThingInternal<
fileProgress.current.forEach((p) => {
sum += p;
});
const averageProgress =
Math.floor(sum / fileProgress.current.size / 10) * 10;
const averageProgress = roundProgress(
Math.min(100, sum / fileProgress.current.size),
progressGranularity,
);
if (averageProgress !== uploadProgress.current) {
opts.onUploadProgress(averageProgress);
uploadProgress.current = averageProgress;
Expand Down
10 changes: 10 additions & 0 deletions packages/shared/src/component-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ import { video } from "@uploadthing/mime-types/video";
import type { ExpandedRouteConfig } from "./types";
import { objectKeys } from "./utils";

export type ProgressGranularity = "all" | "fine" | "coarse";
export const roundProgress = (
progress: number,
granularity: ProgressGranularity,
) => {
if (granularity === "all") return progress;
if (granularity === "fine") return Math.round(progress);
return Math.floor(progress / 10) * 10;
};

export const generateMimeTypes = (
typesOrRouteConfig: string[] | ExpandedRouteConfig,
) => {
Expand Down
22 changes: 13 additions & 9 deletions packages/solid/src/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type { FileRouter } from "uploadthing/types";

import { __createUploadThingInternal } from "../create-uploadthing";
import type { UploadthingComponentProps } from "../types";
import { Cancel, progressWidths, Spinner } from "./shared";
import { Cancel, Spinner } from "./shared";

type ButtonStyleFieldCallbackArgs = {
__runtime: "solid";
Expand Down Expand Up @@ -104,6 +104,7 @@ export function UploadButton<
void $props.onClientUploadComplete?.(res);
setUploadProgress(0);
},
uploadProgressGranularity: $props.uploadProgressGranularity,
onUploadProgress: (p) => {
setUploadProgress(p);
$props.onUploadProgress?.(p);
Expand Down Expand Up @@ -205,7 +206,9 @@ export function UploadButton<

return (
<span class="z-50">
<span class="block group-hover:hidden">{uploadProgress()}%</span>
<span class="block group-hover:hidden">
{Math.round(uploadProgress())}%
</span>
<Cancel cn={cn} class="hidden size-4 group-hover:block" />
</span>
);
Expand All @@ -218,18 +221,19 @@ export function UploadButton<
$props.class,
styleFieldToClassName($props.appearance?.container, styleFieldArg),
)}
style={styleFieldToCssObject($props.appearance?.container, styleFieldArg)}
style={{
"--progress-width": `${uploadProgress()}%`,
...styleFieldToCssObject($props.appearance?.container, styleFieldArg),
}}
data-state={state()}
>
<label
class={cn(
"group relative flex h-10 w-36 cursor-pointer items-center justify-center overflow-hidden rounded-md text-white after:transition-[width] after:duration-500 focus-within:ring-2 focus-within:ring-blue-600 focus-within:ring-offset-2",
state() === "readying" && "cursor-not-allowed bg-blue-400",
state() === "uploading" &&
`bg-blue-400 after:absolute after:left-0 after:h-full after:bg-blue-600 ${
progressWidths[uploadProgress()]
}`,
state() === "ready" && "bg-blue-600",
"disabled:pointer-events-none",
"data-[state=disabled]:cursor-not-allowed data-[state=readying]:cursor-not-allowed",
"data-[state=disabled]:bg-blue-400 data-[state=ready]:bg-blue-600 data-[state=readying]:bg-blue-400 data-[state=uploading]:bg-blue-400",
"after:absolute after:left-0 after:h-full after:w-[var(--progress-width)] after:content-[''] data-[state=uploading]:after:bg-blue-600",
styleFieldToClassName($props.appearance?.button, styleFieldArg),
)}
style={styleFieldToCssObject($props.appearance?.button, styleFieldArg)}
Expand Down
23 changes: 12 additions & 11 deletions packages/solid/src/components/dropzone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import type { FileRouter } from "uploadthing/types";

import { __createUploadThingInternal } from "../create-uploadthing";
import type { UploadthingComponentProps } from "../types";
import { Cancel, progressWidths, Spinner } from "./shared";
import { Cancel, Spinner } from "./shared";

type DropzoneStyleFieldCallbackArgs = {
__runtime: "solid";
Expand Down Expand Up @@ -129,6 +129,7 @@ export const UploadDropzone = <
void $props.onClientUploadComplete?.(res);
setUploadProgress(0);
},
uploadProgressGranularity: $props.uploadProgressGranularity,
onUploadProgress: (p) => {
setUploadProgress(p);
$props.onUploadProgress?.(p);
Expand Down Expand Up @@ -304,18 +305,18 @@ export const UploadDropzone = <

<button
class={cn(
"group relative mt-4 flex h-10 w-36 cursor-pointer items-center justify-center overflow-hidden rounded-md border-none text-base text-white after:transition-[width] after:duration-500 focus-within:ring-2 focus-within:ring-blue-600 focus-within:ring-offset-2",
state() === "disabled" && "cursor-not-allowed bg-blue-400",
state() === "readying" && "cursor-not-allowed bg-blue-400",
state() === "uploading" &&
`bg-blue-400 after:absolute after:left-0 after:h-full after:bg-blue-600 ${
progressWidths[uploadProgress()]
}`,
state() === "ready" && "bg-blue-600",
"group relative mt-4 flex h-10 w-36 items-center justify-center overflow-hidden rounded-md border-none text-base text-white",
"after:absolute after:left-0 after:h-full after:w-[var(--progress-width)] after:bg-blue-600 after:transition-[width] after:duration-500 after:content-['']",
"focus-within:ring-2 focus-within:ring-blue-600 focus-within:ring-offset-2",
"disabled:pointer-events-none",
"data-[state=disabled]:cursor-not-allowed data-[state=readying]:cursor-not-allowed",
"data-[state=disabled]:bg-blue-400 data-[state=ready]:bg-blue-600 data-[state=readying]:bg-blue-400 data-[state=uploading]:bg-blue-400",
styleFieldToClassName($props.appearance?.button, styleFieldArg),
)}
style={styleFieldToCssObject($props.appearance?.button, styleFieldArg)}
style={{
"--progress-width": `${uploadProgress()}%`,
...styleFieldToCssObject($props.appearance?.button, styleFieldArg),
}}
onClick={onUploadClick}
data-ut-element="button"
data-state={state()}
Expand All @@ -338,7 +339,7 @@ export const UploadDropzone = <
<Show when={uploadProgress() < 100} fallback={<Spinner />}>
<span class="z-50">
<span class="block group-hover:hidden">
{uploadProgress()}%
{Math.round(uploadProgress())}%
</span>
<Cancel cn={cn} class="hidden size-4 group-hover:block" />
</span>
Expand Down
14 changes: 0 additions & 14 deletions packages/solid/src/components/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,3 @@ export function Cancel(props: { class?: string; cn: ClassListMerger }) {
</svg>
);
}

export const progressWidths: Record<number, string> = {
0: "after:w-0",
10: "after:w-[10%]",
20: "after:w-[20%]",
30: "after:w-[30%]",
40: "after:w-[40%]",
50: "after:w-[50%]",
60: "after:w-[60%]",
70: "after:w-[70%]",
80: "after:w-[80%]",
90: "after:w-[90%]",
100: "after:w-[100%]",
};
Loading

0 comments on commit 67c3b1c

Please sign in to comment.