fix: restrict SVG-to-raster dropzone to only accept SVG files

Adds fileFilter and acceptDescription props to Dropzone so tools can
restrict accepted file types across all entry paths (file picker,
drag-and-drop, paste, single URL import, bulk URL import).
This commit is contained in:
SnapOtter
2026-05-13 10:32:18 +08:00
parent bf01dcd47e
commit ac7d9cd591
+24 -11
View File
@@ -88,6 +88,10 @@ interface DropzoneProps {
currentFiles?: File[]; currentFiles?: File[];
/** Use a smaller layout that fits constrained containers like the pipeline preview. */ /** Use a smaller layout that fits constrained containers like the pipeline preview. */
compact?: boolean; compact?: boolean;
/** Override the default isImageFile check for drag/drop and paste. */
fileFilter?: (file: File) => boolean;
/** Override the format hint text (e.g. "SVG files only"). */
acceptDescription?: string;
} }
// Browsers may not map certain formats (HEIC, JXL, RAW, etc.) to image/* in file pickers. // Browsers may not map certain formats (HEIC, JXL, RAW, etc.) to image/* in file pickers.
@@ -104,7 +108,10 @@ export function Dropzone({
multiple = true, multiple = true,
currentFiles = [], currentFiles = [],
compact = false, compact = false,
fileFilter,
acceptDescription,
}: DropzoneProps) { }: DropzoneProps) {
const checkFile = fileFilter ?? isImageFile;
const resolvedAccept = expandAccept(accept); const resolvedAccept = expandAccept(accept);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [urlInput, setUrlInput] = useState(""); const [urlInput, setUrlInput] = useState("");
@@ -121,13 +128,17 @@ export function Dropzone({
setUrlError(null); setUrlError(null);
const file = await importSingleUrl(url); const file = await importSingleUrl(url);
if (file) { if (file) {
setUrlInput(""); if (!checkFile(file)) {
onUrlImport?.(file); setUrlError(acceptDescription ?? "This file type is not supported by this tool");
} else {
setUrlInput("");
onUrlImport?.(file);
}
} else { } else {
setUrlError("Could not fetch image from URL"); setUrlError("Could not fetch image from URL");
} }
setUrlLoading(false); setUrlLoading(false);
}, [urlInput, importSingleUrl, onUrlImport]); }, [urlInput, importSingleUrl, onUrlImport, checkFile, acceptDescription]);
const handleDrag = useCallback((e: DragEvent) => { const handleDrag = useCallback((e: DragEvent) => {
e.preventDefault(); e.preventDefault();
@@ -141,10 +152,10 @@ export function Dropzone({
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setIsDragging(false); setIsDragging(false);
const files = Array.from(e.dataTransfer.files).filter(isImageFile); const files = Array.from(e.dataTransfer.files).filter(checkFile);
if (files.length > 0) onFiles?.(files); if (files.length > 0) onFiles?.(files);
}, },
[onFiles], [onFiles, checkFile],
); );
const handleClick = () => { const handleClick = () => {
@@ -153,7 +164,7 @@ export function Dropzone({
input.multiple = multiple; input.multiple = multiple;
if (resolvedAccept) input.accept = resolvedAccept; if (resolvedAccept) input.accept = resolvedAccept;
input.onchange = (e) => { input.onchange = (e) => {
const files = Array.from((e.target as HTMLInputElement).files || []); const files = Array.from((e.target as HTMLInputElement).files || []).filter(checkFile);
if (files.length > 0) onFiles?.(files); if (files.length > 0) onFiles?.(files);
}; };
input.click(); input.click();
@@ -168,13 +179,13 @@ export function Dropzone({
if (clip.files.length > 0) { if (clip.files.length > 0) {
for (const file of clip.files) { for (const file of clip.files) {
if (isImageFile(file)) files.push(file); if (checkFile(file)) files.push(file);
} }
} else if (clip.items) { } else if (clip.items) {
for (const item of clip.items) { for (const item of clip.items) {
if (item.kind === "file") { if (item.kind === "file") {
const file = item.getAsFile(); const file = item.getAsFile();
if (file && isImageFile(file)) files.push(file); if (file && checkFile(file)) files.push(file);
} }
} }
} }
@@ -186,7 +197,7 @@ export function Dropzone({
}; };
document.addEventListener("paste", handlePaste); document.addEventListener("paste", handlePaste);
return () => document.removeEventListener("paste", handlePaste); return () => document.removeEventListener("paste", handlePaste);
}, [onFiles]); }, [onFiles, checkFile]);
const hasMultipleFiles = currentFiles.length > 1; const hasMultipleFiles = currentFiles.length > 1;
@@ -246,7 +257,7 @@ export function Dropzone({
Upload Upload
</button> </button>
<p className="text-xs text-muted-foreground/50"> <p className="text-xs text-muted-foreground/50">
PNG, JPG, WebP, HEIC, RAW, PSD, and 65+ formats {acceptDescription ?? "PNG, JPG, WebP, HEIC, RAW, PSD, and 65+ formats"}
</p> </p>
{!compact && onUrlImport && ( {!compact && onUrlImport && (
@@ -326,7 +337,9 @@ export function Dropzone({
<UrlImportModal <UrlImportModal
onClose={() => setShowBulkModal(false)} onClose={() => setShowBulkModal(false)}
onImport={(files) => { onImport={(files) => {
for (const file of files) onUrlImport?.(file); for (const file of files) {
if (checkFile(file)) onUrlImport?.(file);
}
setShowBulkModal(false); setShowBulkModal(false);
}} }}
/> />