fix: error state for file upload

This commit is contained in:
Juan Di Toro 2024-03-26 14:42:18 +01:00
parent 2bca9ce939
commit b2a822bf08
4 changed files with 83 additions and 13 deletions

View File

@ -11,7 +11,7 @@ import {
DialogTrigger, DialogTrigger,
} from "~/components/ui/dialog"; } from "~/components/ui/dialog";
import { useUppy } from "./lib/uppy"; import { useUppy } from "./lib/uppy";
import type { UppyFile } from "@uppy/core"; import type { FailedUppyFile, UppyFile } from "@uppy/core";
import { Progress } from "~/components/ui/progress"; import { Progress } from "~/components/ui/progress";
import { DialogClose } from "@radix-ui/react-dialog"; import { DialogClose } from "@radix-ui/react-dialog";
import { ChevronDownIcon, ExitIcon, TrashIcon } from "@radix-ui/react-icons"; import { ChevronDownIcon, ExitIcon, TrashIcon } from "@radix-ui/react-icons";
@ -24,6 +24,7 @@ import {
BoxCheckedIcon, BoxCheckedIcon,
PageIcon, PageIcon,
ThemeIcon, ThemeIcon,
ExclamationCircleIcon,
} from "./icons"; } from "./icons";
import { import {
DropdownMenu, DropdownMenu,
@ -178,13 +179,15 @@ const UploadFileForm = () => {
state, state,
removeFile, removeFile,
cancelAll, cancelAll,
failedFiles,
} = useUppy(); } = useUppy();
console.log({ state, files: getFiles() });
const isUploading = state === "uploading"; const isUploading = state === "uploading";
const isCompleted = state === "completed"; const isCompleted = state === "completed";
const hasErrored = state === "error";
const hasStarted = state !== "idle" && state !== "initializing"; const hasStarted = state !== "idle" && state !== "initializing";
const getFailedState = (id: string) =>
failedFiles.find((file) => file.id === id);
return ( return (
<> <>
@ -216,11 +219,18 @@ const UploadFileForm = () => {
onRemove={(id) => { onRemove={(id) => {
removeFile(id); removeFile(id);
}} }}
failedState={getFailedState(file.id)}
/> />
))} ))}
</div> </div>
{hasStarted ? ( {hasErrored ? (
<div className="text-red-500">
<p>An error occurred</p>
</div>
) : null}
{hasStarted && !hasErrored ? (
<div className="flex flex-col items-center gap-y-2 w-full text-primary-1"> <div className="flex flex-col items-center gap-y-2 w-full text-primary-1">
<CloudCheckIcon className="w-32 h-32" /> <CloudCheckIcon className="w-32 h-32" />
{isCompleted {isCompleted
@ -260,19 +270,26 @@ function bytestoMegabytes(bytes: number) {
const UploadFileItem = ({ const UploadFileItem = ({
file, file,
failedState,
onRemove, onRemove,
}: { }: {
file: UppyFile; file: UppyFile;
failedState?: FailedUppyFile<Record<string, any>, Record<string, any>>;
onRemove: (id: string) => void; onRemove: (id: string) => void;
}) => { }) => {
const sizeInMb = bytestoMegabytes(file.size).toFixed(2); const sizeInMb = bytestoMegabytes(file.size).toFixed(2);
return ( return (
<div className="flex flex-col w-full py-4 px-2 bg-primary-dark"> <div className="flex flex-col w-full py-4 px-2 bg-primary-dark">
<div className="flex text-primary-1 items-center justify-between"> <div
className={`flex items-center justify-between ${
failedState ? "text-red-500" : "text-primary-1"
}`}>
<div className="flex items-center"> <div className="flex items-center">
<div className="p-2"> <div className="p-2">
{file.progress?.uploadComplete ? ( {file.progress?.uploadComplete ? (
<BoxCheckedIcon className="w-4 h-4" /> <BoxCheckedIcon className="w-4 h-4" />
) : failedState?.error ? (
<ExclamationCircleIcon className="w-4 h-4" />
) : ( ) : (
<PageIcon className="w-4 h-4" /> <PageIcon className="w-4 h-4" />
)} )}
@ -304,6 +321,27 @@ const UploadFileItem = ({
</Button> </Button>
</div> </div>
{failedState ? (
<div className="mt-2 text-red-500 text-sm">
<p>Error uploading: {failedState.error}</p>
<div className="flex gap-2">
<Button
size={"sm"}
onClick={() => {
/* Retry upload function here */
}}>
Retry
</Button>
<Button
size={"sm"}
variant={"outline"}
onClick={() => onRemove(file.id)}>
Remove
</Button>
</div>
</div>
) : null}
{file.progress?.uploadStarted && !file.progress.uploadComplete ? ( {file.progress?.uploadStarted && !file.progress.uploadComplete ? (
<Progress max={100} value={file.progress.percentage} className="mt-2" /> <Progress max={100} value={file.progress.percentage} className="mt-2" />
) : null} ) : null}

View File

@ -566,3 +566,26 @@ export const RecentIcon = ({ className }: { className?: string }) => {
</svg> </svg>
); );
}; };
export const ExclamationCircleIcon = ({
className,
}: {
className?: string;
}) => {
return (
<svg
aria-hidden="true"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
xmlns="http://www.w3.org/2000/svg"
className={className}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"
/>
</svg>
);
};

View File

@ -1,4 +1,4 @@
import Uppy, { debugLogger, type State, type UppyFile } from "@uppy/core"; import Uppy, { debugLogger, FailedUppyFile, type State, type UppyFile } from "@uppy/core";
import Tus from "@uppy/tus"; import Tus from "@uppy/tus";
import toArray from "@uppy/utils/lib/toArray"; import toArray from "@uppy/utils/lib/toArray";
@ -63,6 +63,8 @@ export function useUppy() {
} }
| object | object
>({}); >({});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [failedFiles, setFailedFiles] = useState<FailedUppyFile<Record<string, any>, Record<string, any>>[]>([])
const getRootProps = useMemo( const getRootProps = useMemo(
() => () => { () => () => {
return { return {
@ -197,6 +199,7 @@ export function useUppy() {
} }
console.log("successful files:", result.successful); console.log("successful files:", result.successful);
console.log("failed files:", result.failed); console.log("failed files:", result.failed);
setFailedFiles(result.failed);
}); });
const setStateCb = (event: (typeof LISTENING_EVENTS)[number]) => { const setStateCb = (event: (typeof LISTENING_EVENTS)[number]) => {
@ -232,6 +235,7 @@ export function useUppy() {
return { return {
getFiles: () => uppyInstance.current?.getFiles() ?? [], getFiles: () => uppyInstance.current?.getFiles() ?? [],
error: uppyInstance.current?.getState, error: uppyInstance.current?.getState,
failedFiles,
state, state,
upload: () => upload: () =>
uppyInstance.current?.upload() ?? uppyInstance.current?.upload() ??

View File

@ -12,6 +12,7 @@ import {
import { cn } from "~/utils"; import { cn } from "~/utils";
import type { FileItem } from "~/data/file-provider"; import type { FileItem } from "~/data/file-provider";
import { usePinning } from "~/hooks/usePinning";
// This type is used to define the shape of our data. // This type is used to define the shape of our data.
// You can use a Zod schema here if you want. // You can use a Zod schema here if you want.
@ -73,7 +74,9 @@ export const columns: ColumnDef<FileItem>[] = [
accessorKey: "actions", accessorKey: "actions",
header: () => null, header: () => null,
size: 20, size: 20,
cell: ({ row }) => ( cell: ({ row }) => {
const {unpin} = usePinning();
return (
<div className="flex w-5 items-center justify-between"> <div className="flex w-5 items-center justify-between">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
@ -85,7 +88,9 @@ export const columns: ColumnDef<FileItem>[] = [
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem variant="destructive"> <DropdownMenuItem variant="destructive" onClick={() => {
unpin(row.getValue("cid"))
}}>
<TrashIcon className="mr-2" /> <TrashIcon className="mr-2" />
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>
@ -93,6 +98,6 @@ export const columns: ColumnDef<FileItem>[] = [
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
), )},
}, },
]; ];