From e16a37d7be74f32128e55273c687ad459598027e Mon Sep 17 00:00:00 2001 From: yongrean <78528865+k08200@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:26:52 +0900 Subject: [PATCH 1/3] fix: block submit while file upload is pending --- .../__tests__/file-upload-validation.test.ts | 45 +++++++++++++++++++ .../file-upload-field/file-upload-field.tsx | 25 +++++++++-- .../file-upload-validation.ts | 11 +++++ 3 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 editor/components/formfield/file-upload-field/__tests__/file-upload-validation.test.ts create mode 100644 editor/components/formfield/file-upload-field/file-upload-validation.ts 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..4486fb2b7 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, @@ -95,6 +98,21 @@ export const FileUploadField = ({ [data] ); + 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]); + 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; +} From e1d062f046a331fe086fd49cbd6cee877497802e Mon Sep 17 00:00:00 2001 From: yongrean <78528865+k08200@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:05:45 +0900 Subject: [PATCH 2/3] fix: mark uploaded file value as readonly --- editor/components/extension/file-upload.tsx | 1 + 1 file changed, 1 insertion(+) 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)} /> From 283fc0d9b20f81d57fb8ec28ef299e13453cb944 Mon Sep 17 00:00:00 2001 From: yongrean <78528865+k08200@users.noreply.github.com> Date: Fri, 1 May 2026 00:59:11 +0900 Subject: [PATCH 3/3] fix: block pending file upload submits --- .../file-upload-field/file-upload-field.tsx | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) 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 4486fb2b7..ff9edce73 100644 --- a/editor/components/formfield/file-upload-field/file-upload-field.tsx +++ b/editor/components/formfield/file-upload-field/file-upload-field.tsx @@ -97,6 +97,7 @@ export const FileUploadField = ({ () => data.map((info) => info.path).filter(Boolean) as string[], [data] ); + const uploadedFilesValue = uploadedFilesPaths.join(","); const shouldBlockSubmit = shouldBlockSubmitForFileUpload({ isMultipartFile, @@ -113,6 +114,29 @@ export const FileUploadField = ({ 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 ( )}