mirror of
https://github.com/get-drexa/drive.git
synced 2025-11-30 21:41:39 +00:00
feat: upload file dialog err handling & new flow
- add basic err handling to upload file dialog. - rework the upload flow. now, on all successful uploads, the dialog won't auto disappear. if some fails, the dialog will allow for retry.
This commit is contained in:
71
packages/web/src/files/PickedFileItem.tsx
Normal file
71
packages/web/src/files/PickedFileItem.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { useAtomValue } from "jotai"
|
||||||
|
import { CircleAlertIcon, XIcon } from "lucide-react"
|
||||||
|
import type React from "react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Progress } from "@/components/ui/progress"
|
||||||
|
import { Tooltip } from "@/components/ui/tooltip"
|
||||||
|
import { FileUploadStatusKind, fileUploadStatusAtomFamily } from "./store"
|
||||||
|
import type { PickedFile } from "./upload-file-dialog"
|
||||||
|
|
||||||
|
export function PickedFileItem({
|
||||||
|
file: pickedFile,
|
||||||
|
onRemove,
|
||||||
|
}: {
|
||||||
|
file: PickedFile
|
||||||
|
onRemove: (file: PickedFile) => void
|
||||||
|
}) {
|
||||||
|
const fileUploadAtom = fileUploadStatusAtomFamily(pickedFile.id)
|
||||||
|
const fileUpload = useAtomValue(fileUploadAtom)
|
||||||
|
console.log("fileUpload", fileUpload)
|
||||||
|
const { file, id } = pickedFile
|
||||||
|
|
||||||
|
let statusIndicator: React.ReactNode
|
||||||
|
if (!fileUpload) {
|
||||||
|
statusIndicator = (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onRemove(pickedFile)}
|
||||||
|
>
|
||||||
|
<XIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
switch (fileUpload.kind) {
|
||||||
|
case FileUploadStatusKind.InProgress:
|
||||||
|
statusIndicator = <Progress value={fileUpload.progress * 100} />
|
||||||
|
break
|
||||||
|
case FileUploadStatusKind.Error:
|
||||||
|
statusIndicator = (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<CircleAlertIcon />
|
||||||
|
</TooltipTrigger>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className="pl-3 pr-1 py-0.5 h-8 hover:bg-muted flex justify-between items-center"
|
||||||
|
key={id}
|
||||||
|
>
|
||||||
|
<span>{file.name}</span>
|
||||||
|
{fileUpload ? (
|
||||||
|
<Progress
|
||||||
|
className="max-w-20"
|
||||||
|
value={fileUpload.progress * 100}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onRemove(pickedFile)}
|
||||||
|
>
|
||||||
|
<XIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,19 +1,39 @@
|
|||||||
import { atom } from "jotai"
|
import { atom } from "jotai"
|
||||||
import { atomFamily } from "jotai/utils"
|
import { atomFamily } from "jotai/utils"
|
||||||
|
|
||||||
type FileUpload = {
|
export enum FileUploadStatusKind {
|
||||||
id: string
|
InProgress = "InProgress",
|
||||||
|
Error = "Error",
|
||||||
|
Success = "Success",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FileUploadInProgress = {
|
||||||
|
kind: FileUploadStatusKind.InProgress
|
||||||
progress: number
|
progress: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fileUploadsAtom = atom<Record<string, FileUpload>>({})
|
export type FileUploadError = {
|
||||||
|
kind: FileUploadStatusKind.Error
|
||||||
|
error: unknown
|
||||||
|
}
|
||||||
|
|
||||||
export const fileUploadAtomFamily = atomFamily((id: string) =>
|
export type FileUploadSuccess = {
|
||||||
|
kind: FileUploadStatusKind.Success
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FileUploadStatus =
|
||||||
|
| FileUploadInProgress
|
||||||
|
| FileUploadError
|
||||||
|
| FileUploadSuccess
|
||||||
|
|
||||||
|
export const fileUploadsAtom = atom<Record<string, FileUploadStatus>>({})
|
||||||
|
|
||||||
|
export const fileUploadStatusAtomFamily = atomFamily((id: string) =>
|
||||||
atom(
|
atom(
|
||||||
(get) => get(fileUploadsAtom)[id],
|
(get) => get(fileUploadsAtom)[id],
|
||||||
(get, set, progress: number) => {
|
(get, set, status: FileUploadStatus) => {
|
||||||
const fileUploads = { ...get(fileUploadsAtom) }
|
const fileUploads = { ...get(fileUploadsAtom) }
|
||||||
fileUploads[id] = { id, progress }
|
fileUploads[id] = status
|
||||||
set(fileUploadsAtom, fileUploads)
|
set(fileUploadsAtom, fileUploads)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -22,12 +42,56 @@ export const fileUploadAtomFamily = atomFamily((id: string) =>
|
|||||||
export const clearFileUploadAtom = atom(null, (get, set, id: string) => {
|
export const clearFileUploadAtom = atom(null, (get, set, id: string) => {
|
||||||
const fileUploads = { ...get(fileUploadsAtom) }
|
const fileUploads = { ...get(fileUploadsAtom) }
|
||||||
delete fileUploads[id]
|
delete fileUploads[id]
|
||||||
fileUploadAtomFamily.remove(id)
|
fileUploadStatusAtomFamily.remove(id)
|
||||||
set(fileUploadsAtom, fileUploads)
|
set(fileUploadsAtom, fileUploads)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const clearFileUploadStatusesAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, ids: string[]) => {
|
||||||
|
const fileUploads = { ...get(fileUploadsAtom) }
|
||||||
|
for (const id of ids) {
|
||||||
|
if (fileUploads[id]) {
|
||||||
|
delete fileUploads[id]
|
||||||
|
}
|
||||||
|
fileUploadStatusAtomFamily.remove(id)
|
||||||
|
}
|
||||||
|
set(fileUploadsAtom, fileUploads)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export const fileUploadCountAtom = atom(
|
export const fileUploadCountAtom = atom(
|
||||||
(get) => Object.keys(get(fileUploadsAtom)).length,
|
(get) => Object.keys(get(fileUploadsAtom)).length,
|
||||||
)
|
)
|
||||||
|
|
||||||
export const hasFileUploadsAtom = atom((get) => get(fileUploadCountAtom) > 0)
|
export const inProgressFileUploadCountAtom = atom((get) => {
|
||||||
|
const statuses = get(fileUploadsAtom)
|
||||||
|
let count = 0
|
||||||
|
for (const status in statuses) {
|
||||||
|
if (statuses[status]?.kind === FileUploadStatusKind.InProgress) {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
})
|
||||||
|
|
||||||
|
export const successfulFileUploadCountAtom = atom((get) => {
|
||||||
|
const statuses = get(fileUploadsAtom)
|
||||||
|
let count = 0
|
||||||
|
for (const status in statuses) {
|
||||||
|
if (statuses[status]?.kind === FileUploadStatusKind.Success) {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
})
|
||||||
|
|
||||||
|
export const hasFileUploadsErrorAtom = atom((get) => {
|
||||||
|
const statuses = get(fileUploadsAtom)
|
||||||
|
for (const status in statuses) {
|
||||||
|
if (statuses[status]?.kind === FileUploadStatusKind.Error) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import type { Doc } from "@fileone/convex/_generated/dataModel"
|
|||||||
import { useMutation } from "@tanstack/react-query"
|
import { useMutation } from "@tanstack/react-query"
|
||||||
import { atom, useAtom, useAtomValue, useSetAtom, useStore } from "jotai"
|
import { atom, useAtom, useAtomValue, useSetAtom, useStore } from "jotai"
|
||||||
import { atomEffect } from "jotai-effect"
|
import { atomEffect } from "jotai-effect"
|
||||||
import { FilePlus2Icon, UploadCloudIcon, XIcon } from "lucide-react"
|
import {
|
||||||
|
CircleAlertIcon,
|
||||||
|
CircleCheckIcon,
|
||||||
|
FilePlus2Icon,
|
||||||
|
UploadCloudIcon,
|
||||||
|
XIcon,
|
||||||
|
} from "lucide-react"
|
||||||
import { nanoid } from "nanoid"
|
import { nanoid } from "nanoid"
|
||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { useId, useMemo, useRef, useState } from "react"
|
import { useId, useMemo, useRef, useState } from "react"
|
||||||
@@ -17,8 +23,21 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Progress } from "@/components/ui/progress"
|
import { Progress } from "@/components/ui/progress"
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip"
|
||||||
import { WithAtom } from "@/components/with-atom"
|
import { WithAtom } from "@/components/with-atom"
|
||||||
import { clearFileUploadAtom, fileUploadAtomFamily } from "./store"
|
import { formatError } from "@/lib/error"
|
||||||
|
import {
|
||||||
|
clearFileUploadStatusesAtom,
|
||||||
|
FileUploadStatusKind,
|
||||||
|
fileUploadCountAtom,
|
||||||
|
fileUploadStatusAtomFamily,
|
||||||
|
fileUploadsAtom,
|
||||||
|
hasFileUploadsErrorAtom,
|
||||||
|
} from "./store"
|
||||||
import useUploadFile from "./use-upload-file"
|
import useUploadFile from "./use-upload-file"
|
||||||
|
|
||||||
type UploadFileDialogProps = {
|
type UploadFileDialogProps = {
|
||||||
@@ -41,7 +60,7 @@ export function UploadFileDialog({
|
|||||||
const formId = useId()
|
const formId = useId()
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
const setPickedFiles = useSetAtom(pickedFilesAtom)
|
const setPickedFiles = useSetAtom(pickedFilesAtom)
|
||||||
const clearFileUpload = useSetAtom(clearFileUploadAtom)
|
const clearFileUploadStatuses = useSetAtom(clearFileUploadStatusesAtom)
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
|
||||||
const updateFileInputEffect = useMemo(
|
const updateFileInputEffect = useMemo(
|
||||||
@@ -69,21 +88,58 @@ export function UploadFileDialog({
|
|||||||
uploadFile({
|
uploadFile({
|
||||||
file: pickedFile.file,
|
file: pickedFile.file,
|
||||||
onStart: () => {
|
onStart: () => {
|
||||||
store.set(fileUploadAtomFamily(pickedFile.id), 0)
|
store.set(fileUploadStatusAtomFamily(pickedFile.id), {
|
||||||
|
kind: FileUploadStatusKind.InProgress,
|
||||||
|
progress: 0,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
onProgress: (progress) => {
|
onProgress: (progress) => {
|
||||||
store.set(fileUploadAtomFamily(pickedFile.id), progress)
|
store.set(fileUploadStatusAtomFamily(pickedFile.id), {
|
||||||
|
kind: FileUploadStatusKind.InProgress,
|
||||||
|
progress,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
}).then(() => {
|
})
|
||||||
clearFileUpload(pickedFile.id)
|
.then(() => {
|
||||||
}),
|
// clearFileUpload(pickedFile.id)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log("error", error)
|
||||||
|
store.set(fileUploadStatusAtomFamily(pickedFile.id), {
|
||||||
|
kind: FileUploadStatusKind.Error,
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
throw error
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
await Promise.all(promises)
|
return await Promise.allSettled(promises)
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: (results, files) => {
|
||||||
toast.success("All files uploaded successfully")
|
const remainingPickedFiles: PickedFile[] = []
|
||||||
setPickedFiles([])
|
results.forEach((result, i) => {
|
||||||
onClose()
|
// biome-ignore lint/style/noNonNullAssertion: results lenght must match input files array length
|
||||||
|
const pickedFile = files[i]!
|
||||||
|
const statusAtom = fileUploadStatusAtomFamily(pickedFile.id)
|
||||||
|
switch (result.status) {
|
||||||
|
case "fulfilled":
|
||||||
|
store.set(statusAtom, {
|
||||||
|
kind: FileUploadStatusKind.Success,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "rejected":
|
||||||
|
store.set(statusAtom, {
|
||||||
|
kind: FileUploadStatusKind.Error,
|
||||||
|
error: result.reason,
|
||||||
|
})
|
||||||
|
remainingPickedFiles.push(pickedFile)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// setPickedFiles(remainingPickedFiles)
|
||||||
|
|
||||||
|
if (remainingPickedFiles.length === 0) {
|
||||||
|
toast.success("All files uploaded successfully")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -105,9 +161,50 @@ export function UploadFileDialog({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startFileUpload() {
|
function onUploadButtonClick() {
|
||||||
|
const uploadStatuses = store.get(fileUploadsAtom)
|
||||||
|
const fileUploadCount = store.get(fileUploadCountAtom)
|
||||||
const pickedFiles = store.get(pickedFilesAtom)
|
const pickedFiles = store.get(pickedFilesAtom)
|
||||||
uploadFiles(pickedFiles)
|
|
||||||
|
if (pickedFiles.length === 0) {
|
||||||
|
// no files are picked, nothing to upload
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileUploadCount === 0) {
|
||||||
|
// no files are being uploaded, upload all picked files
|
||||||
|
uploadFiles(pickedFiles)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const successfulUploads: PickedFile["id"][] = []
|
||||||
|
const nextPickedFiles: PickedFile[] = []
|
||||||
|
for (const file of pickedFiles) {
|
||||||
|
const uploadStatus = uploadStatuses[file.id]
|
||||||
|
if (uploadStatus) {
|
||||||
|
switch (uploadStatus.kind) {
|
||||||
|
case FileUploadStatusKind.Success:
|
||||||
|
successfulUploads.push(file.id)
|
||||||
|
continue
|
||||||
|
case FileUploadStatusKind.InProgress:
|
||||||
|
continue
|
||||||
|
case FileUploadStatusKind.Error:
|
||||||
|
nextPickedFiles.push(file)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearFileUploadStatuses(successfulUploads)
|
||||||
|
|
||||||
|
if (successfulUploads.length === pickedFiles.length) {
|
||||||
|
// all files were successfully uploaded, close the dialog
|
||||||
|
onClose()
|
||||||
|
} else {
|
||||||
|
// some files were not successfully uploaded, set the next picked files
|
||||||
|
setPickedFiles(nextPickedFiles)
|
||||||
|
uploadFiles(nextPickedFiles)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let dialogTitle: string
|
let dialogTitle: string
|
||||||
@@ -165,15 +262,11 @@ export function UploadFileDialog({
|
|||||||
Select more files
|
Select more files
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
<Button
|
<UploadButton
|
||||||
onClick={startFileUpload}
|
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
loading={isUploading}
|
loading={isUploading}
|
||||||
>
|
onClick={onUploadButtonClick}
|
||||||
{pickedFiles.length === 0
|
/>
|
||||||
? "Upload"
|
|
||||||
: `Upload ${pickedFiles.length} files`}
|
|
||||||
</Button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</WithAtom>
|
</WithAtom>
|
||||||
@@ -183,6 +276,39 @@ export function UploadFileDialog({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function UploadButton({
|
||||||
|
disabled,
|
||||||
|
loading,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
disabled: boolean
|
||||||
|
loading: boolean
|
||||||
|
onClick: () => void
|
||||||
|
}) {
|
||||||
|
const pickedFiles = useAtomValue(pickedFilesAtom)
|
||||||
|
const hasUploadErrors = useAtomValue(hasFileUploadsErrorAtom)
|
||||||
|
const fileUploadCount = useAtomValue(fileUploadCountAtom)
|
||||||
|
|
||||||
|
let label: string
|
||||||
|
if (hasUploadErrors) {
|
||||||
|
label = "Retry failed uploads"
|
||||||
|
} else if (pickedFiles.length > 0) {
|
||||||
|
if (fileUploadCount > 0) {
|
||||||
|
label = "Done"
|
||||||
|
} else {
|
||||||
|
label = `Upload ${pickedFiles.length} files`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
label = "Upload"
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={onClick} disabled={disabled} loading={loading}>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function UploadFileDropContainer({ children }: React.PropsWithChildren) {
|
function UploadFileDropContainer({ children }: React.PropsWithChildren) {
|
||||||
const [draggedFiles, setDraggedFiles] = useState<DataTransferItem[]>([])
|
const [draggedFiles, setDraggedFiles] = useState<DataTransferItem[]>([])
|
||||||
const setPickedFiles = useSetAtom(pickedFilesAtom)
|
const setPickedFiles = useSetAtom(pickedFilesAtom)
|
||||||
@@ -239,6 +365,7 @@ function UploadFileDropContainer({ children }: React.PropsWithChildren) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tag: uploadfilearea area fileuploadarea
|
||||||
function UploadFileArea({ onClick }: { onClick: () => void }) {
|
function UploadFileArea({ onClick }: { onClick: () => void }) {
|
||||||
const [pickedFiles, setPickedFiles] = useAtom(pickedFilesAtom)
|
const [pickedFiles, setPickedFiles] = useAtom(pickedFilesAtom)
|
||||||
|
|
||||||
@@ -294,30 +421,62 @@ function PickedFileItem({
|
|||||||
file: PickedFile
|
file: PickedFile
|
||||||
onRemove: (file: PickedFile) => void
|
onRemove: (file: PickedFile) => void
|
||||||
}) {
|
}) {
|
||||||
const fileUploadAtom = fileUploadAtomFamily(pickedFile.id)
|
const fileUploadAtom = fileUploadStatusAtomFamily(pickedFile.id)
|
||||||
const fileUpload = useAtomValue(fileUploadAtom)
|
const fileUpload = useAtomValue(fileUploadAtom)
|
||||||
console.log("fileUpload", fileUpload)
|
console.log("fileUpload", fileUpload)
|
||||||
const { file, id } = pickedFile
|
const { file, id } = pickedFile
|
||||||
|
|
||||||
|
let statusIndicator: React.ReactNode
|
||||||
|
if (!fileUpload) {
|
||||||
|
statusIndicator = (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onRemove(pickedFile)}
|
||||||
|
>
|
||||||
|
<XIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
switch (fileUpload.kind) {
|
||||||
|
case FileUploadStatusKind.InProgress:
|
||||||
|
statusIndicator = (
|
||||||
|
<Progress
|
||||||
|
className="max-w-20"
|
||||||
|
value={fileUpload.progress * 100}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case FileUploadStatusKind.Error:
|
||||||
|
statusIndicator = (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<CircleAlertIcon className="pr-2 text-destructive" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Failed to upload file:{" "}
|
||||||
|
{formatError(fileUpload.error)}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case FileUploadStatusKind.Success:
|
||||||
|
statusIndicator = (
|
||||||
|
<CircleCheckIcon className="pr-2 text-green-500" />
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className="pl-3 pr-1 py-0.5 h-8 hover:bg-muted flex justify-between items-center"
|
className="pl-3 pr-1 py-0.5 h-8 hover:bg-muted flex justify-between items-center"
|
||||||
key={id}
|
key={id}
|
||||||
>
|
>
|
||||||
<span>{file.name}</span>
|
<span>{file.name}</span>
|
||||||
{fileUpload ? (
|
{statusIndicator}
|
||||||
<Progress
|
|
||||||
className="max-w-20"
|
|
||||||
value={fileUpload.progress * 100}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => onRemove(pickedFile)}
|
|
||||||
>
|
|
||||||
<XIcon className="size-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ function useUploadFile({
|
|||||||
})
|
})
|
||||||
xhr.upload.addEventListener("error", reject)
|
xhr.upload.addEventListener("error", reject)
|
||||||
xhr.addEventListener("load", () => {
|
xhr.addEventListener("load", () => {
|
||||||
console.log("load", xhr.response)
|
|
||||||
resolve(
|
resolve(
|
||||||
xhr.response as {
|
xhr.response as {
|
||||||
storageId: Id<"_storage">
|
storageId: Id<"_storage">
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import { FilePathBreadcrumb } from "@/directories/directory-page/file-path-bread
|
|||||||
import { NewDirectoryDialog } from "@/directories/directory-page/new-directory-dialog"
|
import { NewDirectoryDialog } from "@/directories/directory-page/new-directory-dialog"
|
||||||
import { RenameFileDialog } from "@/directories/directory-page/rename-file-dialog"
|
import { RenameFileDialog } from "@/directories/directory-page/rename-file-dialog"
|
||||||
import { FilePreviewDialog } from "@/files/file-preview-dialog"
|
import { FilePreviewDialog } from "@/files/file-preview-dialog"
|
||||||
import { fileUploadCountAtom } from "@/files/store"
|
import { inProgressFileUploadCountAtom } from "@/files/store"
|
||||||
import { UploadFileDialog } from "@/files/upload-file-dialog"
|
import { UploadFileDialog } from "@/files/upload-file-dialog"
|
||||||
import type { FileDragInfo } from "@/files/use-file-drop"
|
import type { FileDragInfo } from "@/files/use-file-drop"
|
||||||
|
|
||||||
@@ -333,7 +333,9 @@ function RenameMenuItem() {
|
|||||||
function UploadFileButton() {
|
function UploadFileButton() {
|
||||||
const { directory } = useContext(DirectoryPageContext)
|
const { directory } = useContext(DirectoryPageContext)
|
||||||
const setActiveDialogData = useSetAtom(activeDialogDataAtom)
|
const setActiveDialogData = useSetAtom(activeDialogDataAtom)
|
||||||
const fileUploadCount = useAtomValue(fileUploadCountAtom)
|
const inProgressFileUploadCount = useAtomValue(
|
||||||
|
inProgressFileUploadCountAtom,
|
||||||
|
)
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
setActiveDialogData({
|
setActiveDialogData({
|
||||||
@@ -342,10 +344,10 @@ function UploadFileButton() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileUploadCount > 0) {
|
if (inProgressFileUploadCount > 0) {
|
||||||
return (
|
return (
|
||||||
<Button size="sm" type="button" loading onClick={handleClick}>
|
<Button size="sm" type="button" loading onClick={handleClick}>
|
||||||
Uploading {fileUploadCount} files
|
Uploading {inProgressFileUploadCount} files
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user