mirror of
https://github.com/khairul169/garage-webui.git
synced 2026-07-28 11:00:19 +07:00
fix: mount file input in DOM for reliable bucket uploads
Extract openFilePicker() into lib/utils using the same off-screen DOM pattern as copyToClipboard(). The previous handler called input.remove() right after click(), so change never fired in some browsers and uploads silently failed with no toast or network request. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ee420fbf29
commit
2e1a9d078f
@ -29,6 +29,53 @@ export const handleError = (err: unknown) => {
|
||||
toast.error((err as Error)?.message || "Unknown error");
|
||||
};
|
||||
|
||||
/** Delay before removing an unused file input after the picker closes without a selection. */
|
||||
const FILE_PICKER_CANCEL_CLEANUP_MS = 500;
|
||||
|
||||
export type OpenFilePickerOptions = {
|
||||
multiple?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the native file picker and passes the chosen files to `onSelect`.
|
||||
* The input is mounted in the document until selection completes; removing it
|
||||
* immediately after `click()` prevents `change` from firing in some browsers.
|
||||
*/
|
||||
export const openFilePicker = (
|
||||
onSelect: (files: FileList) => void,
|
||||
options: OpenFilePickerOptions = {}
|
||||
) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = options.multiple ?? false;
|
||||
|
||||
// Keep off-screen but attached, same approach as copyToClipboard().
|
||||
input.style.position = "absolute";
|
||||
input.style.left = "-999999px";
|
||||
|
||||
const cleanup = () => {
|
||||
input.remove();
|
||||
window.removeEventListener("focus", onCancelFocus);
|
||||
};
|
||||
|
||||
const onCancelFocus = () => {
|
||||
// Cancel closes the picker without firing change; defer so change can run first.
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(input)) cleanup();
|
||||
}, FILE_PICKER_CANCEL_CLEANUP_MS);
|
||||
};
|
||||
|
||||
input.onchange = (e) => {
|
||||
cleanup();
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
if (files?.length) onSelect(files);
|
||||
};
|
||||
|
||||
document.body.prepend(input);
|
||||
window.addEventListener("focus", onCancelFocus, { once: true });
|
||||
input.click();
|
||||
};
|
||||
|
||||
export const copyToClipboard = async (text: string) => {
|
||||
let textArea: HTMLTextAreaElement | undefined;
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { FolderPlus, UploadIcon } from "lucide-react";
|
||||
import Button from "@/components/ui/button";
|
||||
import { usePutObject } from "./hooks";
|
||||
import { toast } from "sonner";
|
||||
import { handleError } from "@/lib/utils";
|
||||
import { handleError, openFilePicker } from "@/lib/utils";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useBucketContext } from "../context";
|
||||
import { useDisclosure } from "@/hooks/useDisclosure";
|
||||
@ -30,16 +30,7 @@ const Actions = ({ prefix }: Props) => {
|
||||
});
|
||||
|
||||
const onUploadFile = () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true;
|
||||
|
||||
input.onchange = (e) => {
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
if (!files?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
openFilePicker((files) => {
|
||||
if (files.length > 20) {
|
||||
toast.error("You can only upload up to 20 files at a time");
|
||||
return;
|
||||
@ -49,10 +40,7 @@ const Actions = ({ prefix }: Props) => {
|
||||
const key = prefix + file.name;
|
||||
putObject.mutate({ key, file });
|
||||
}
|
||||
};
|
||||
|
||||
input.click();
|
||||
input.remove();
|
||||
}, { multiple: true });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user