fix: error state for file upload
This commit is contained in:
parent
2bca9ce939
commit
b2a822bf08
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -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() ??
|
||||||
|
|
|
@ -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>
|
||||||
),
|
)},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in New Issue