Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ProgressGranularity #1151

Merged
merged 11 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
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",
Copy link
Collaborator

Choose a reason for hiding this comment

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

oh this is nice

"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