diff --git a/editor/components/extension/file-upload.tsx b/editor/components/extension/file-upload.tsx index 3edf998fe..d8674b7c9 100644 --- a/editor/components/extension/file-upload.tsx +++ b/editor/components/extension/file-upload.tsx @@ -404,6 +404,7 @@ export const UploadedFileValue = forwardRef< name={name} required={required} value={value} + readOnly className={cn("sr-only", className)} /> diff --git a/editor/components/formfield/file-upload-field/__tests__/file-upload-validation.test.ts b/editor/components/formfield/file-upload-field/__tests__/file-upload-validation.test.ts new file mode 100644 index 000000000..358d20dca --- /dev/null +++ b/editor/components/formfield/file-upload-field/__tests__/file-upload-validation.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "vitest"; +import { shouldBlockSubmitForFileUpload } from "../file-upload-validation"; + +describe("shouldBlockSubmitForFileUpload", () => { + test("does not block an untouched optional upload field", () => { + expect( + shouldBlockSubmitForFileUpload({ + selectedFilesCount: 0, + uploadedFilesCount: 0, + isUploading: false, + }) + ).toBe(false); + }); + + test("blocks when a selected file has not produced an uploaded path yet", () => { + expect( + shouldBlockSubmitForFileUpload({ + selectedFilesCount: 1, + uploadedFilesCount: 0, + isUploading: true, + }) + ).toBe(true); + }); + + test("allows submit after each selected file has an uploaded path", () => { + expect( + shouldBlockSubmitForFileUpload({ + selectedFilesCount: 2, + uploadedFilesCount: 2, + isUploading: false, + }) + ).toBe(false); + }); + + test("does not block multipart file uploads", () => { + expect( + shouldBlockSubmitForFileUpload({ + isMultipartFile: true, + selectedFilesCount: 1, + uploadedFilesCount: 0, + isUploading: true, + }) + ).toBe(false); + }); +}); diff --git a/editor/components/formfield/file-upload-field/file-upload-field.tsx b/editor/components/formfield/file-upload-field/file-upload-field.tsx index 88669505e..ff9edce73 100644 --- a/editor/components/formfield/file-upload-field/file-upload-field.tsx +++ b/editor/components/formfield/file-upload-field/file-upload-field.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useMemo, useState, useEffect } from "react"; +import React, { useMemo, useState, useEffect, useRef } from "react"; import { FileUploader, FileUploaderTrigger, @@ -16,6 +16,7 @@ import { DropzoneOptions } from "react-dropzone"; import { UploadStatus, useFileUploader } from "./use-file-uploader"; import type { FileUploaderFn } from "./uploader"; import Image from "next/image"; +import { shouldBlockSubmitForFileUpload } from "./file-upload-validation"; type Accept = { [key: string]: string[]; @@ -59,6 +60,8 @@ export const FileUploadField = ({ onFilesChange, isMultipartFile, }: FileUploadDropzoneProps) => { + const uploadedFileValueRef = useRef(null); + useEffect(() => { if (isMultipartFile) return; if (!uploader) { @@ -69,7 +72,7 @@ export const FileUploadField = ({ }, [isMultipartFile, uploader]); const [files, setFiles] = useState([]); - const { getStatus, data } = useFileUploader({ + const { getStatus, data, isUploading } = useFileUploader({ files, uploader, autoUpload: true, @@ -94,6 +97,45 @@ export const FileUploadField = ({ () => data.map((info) => info.path).filter(Boolean) as string[], [data] ); + const uploadedFilesValue = uploadedFilesPaths.join(","); + + const shouldBlockSubmit = shouldBlockSubmitForFileUpload({ + isMultipartFile, + selectedFilesCount: files.length, + uploadedFilesCount: uploadedFilesPaths.length, + isUploading, + }); + + const validationMessage = shouldBlockSubmit + ? "Please wait for the selected file upload to finish." + : ""; + + useEffect(() => { + uploadedFileValueRef.current?.setCustomValidity(validationMessage); + }, [validationMessage]); + + useEffect(() => { + const input = uploadedFileValueRef.current; + const form = input?.form; + + if (!input || !form || isMultipartFile) return; + + const handleSubmit = (event: SubmitEvent) => { + if (!shouldBlockSubmit) return; + + input.setCustomValidity(validationMessage); + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + input.reportValidity(); + }; + + form.addEventListener("submit", handleSubmit, true); + + return () => { + form.removeEventListener("submit", handleSubmit, true); + }; + }, [isMultipartFile, shouldBlockSubmit, validationMessage]); return ( {!isMultipartFile && ( )} diff --git a/editor/components/formfield/file-upload-field/file-upload-validation.ts b/editor/components/formfield/file-upload-field/file-upload-validation.ts new file mode 100644 index 000000000..4735756e1 --- /dev/null +++ b/editor/components/formfield/file-upload-field/file-upload-validation.ts @@ -0,0 +1,11 @@ +export function shouldBlockSubmitForFileUpload(args: { + isMultipartFile?: boolean; + selectedFilesCount: number; + uploadedFilesCount: number; + isUploading: boolean; +}): boolean { + if (args.isMultipartFile) return false; + if (args.selectedFilesCount === 0) return false; + + return args.isUploading || args.uploadedFilesCount < args.selectedFilesCount; +}