From 3f56f193ba8556f6bf82304d078d5390e1fd28c4 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Tue, 16 Jun 2026 16:04:12 +0200 Subject: [PATCH 1/4] fix(file-uploader-web): sort rejected files below successful, show warning on invalid-format drop --- .../src/components/FileUploaderRoot.tsx | 2 ++ .../file-uploader-web/src/stores/FileUploaderStore.ts | 9 +++++++-- .../src/stores/__tests__/FileUploaderStore.spec.ts | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx b/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx index 519ca7d642..f782fbcb77 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx +++ b/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx @@ -28,6 +28,8 @@ export const FileUploaderRoot = observer((props: FileUploaderContainerProps): Re warningMessage = translations.get("uploadLimitReachedMessage", rootStore.maxTotalFiles.toString()); } else if (rootStore.createActionFailed) { warningMessage = translations.get("unavailableCreateActionMessage"); + } else if (rootStore.hasValidationErrors) { + warningMessage = translations.get("dropzoneRejectedMessage"); } return ( diff --git a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts index 9c78942c45..9088737403 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts @@ -91,6 +91,7 @@ export class FileUploaderStore { _maxTotalFiles: observable, _maxConcurrentUploads: observable, isFileUploadLimitReached: computed, + hasValidationErrors: computed, sortedFiles: computed, activeCount: computed, uploadingCount: computed, @@ -188,10 +189,14 @@ export class FileUploaderStore { return this.activeCount >= this.maxTotalFiles; } + get hasValidationErrors(): boolean { + return this.files.some(f => f.fileStatus === "validationError"); + } + get sortedFiles(): FileStore[] { return [...this.files].sort((a, b) => { - const isErrorA = a.fileStatus === "validationError" ? 1 : 0; - const isErrorB = b.fileStatus === "validationError" ? 1 : 0; + const isErrorA = a.fileStatus === "validationError" || a.fileStatus === "rejected" ? 1 : 0; + const isErrorB = b.fileStatus === "validationError" || b.fileStatus === "rejected" ? 1 : 0; return isErrorA - isErrorB; }); } diff --git a/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts index b544f88ad5..f0f9f53779 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts @@ -344,7 +344,7 @@ describe("FileStore.canRetry — reacts to freed slots", () => { // ─── FileStore.retry ───────────────────────────────────────────────────────── describe("FileStore.retry", () => { - test("transitions rejected file to queued", () => { + test("transitions rejected file to uploading via queue reaction", () => { const store = buildStore({ maxFilesPerUpload: dynamic(new Big(3)), maxFilesPerBatch: unavailableDynamic() From f25c0f2c0bb07455c91c85ea02e53ae547ee4134 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Thu, 18 Jun 2026 15:58:39 +0200 Subject: [PATCH 2/4] fix(file-uploader-web): add dismiss button to over-limit rejected files --- .../src/components/ActionsBar.tsx | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/pluggableWidgets/file-uploader-web/src/components/ActionsBar.tsx b/packages/pluggableWidgets/file-uploader-web/src/components/ActionsBar.tsx index 36f0fd3902..bbe428c523 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/components/ActionsBar.tsx +++ b/packages/pluggableWidgets/file-uploader-web/src/components/ActionsBar.tsx @@ -59,11 +59,7 @@ const DefaultActionsBar = observer(function DefaultActionsBar(props: ButtonsBarP }, [props.store]); if (props.store.fileStatus === "rejected") { - return ( -
- -
- ); + return ; } return ( @@ -84,6 +80,26 @@ const DefaultActionsBar = observer(function DefaultActionsBar(props: ButtonsBarP ); }); +function RejectedActionsBar({ store }: ButtonsBarProps): ReactElement { + const translations = useTranslationsStore(); + + const onDismiss = useCallback(() => { + store.dismiss(); + }, [store]); + + return ( +
+ + } + title={translations.get("removeButtonTextMessage")} + action={onDismiss} + isDisabled={false} + /> +
+ ); +} + function DismissActionsBar({ store }: ButtonsBarProps): ReactElement { const translations = useTranslationsStore(); From 67761667da557652164982919732d69fa8259d31 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Thu, 18 Jun 2026 17:02:50 +0200 Subject: [PATCH 3/4] fix(file-uploader-web): inline validation error check to fix warning not clearing on dismiss --- .../src/components/FileUploaderRoot.tsx | 2 +- .../__tests__/FileUploaderStore.spec.ts | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx b/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx index f782fbcb77..a7e0a1f9e4 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx +++ b/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx @@ -28,7 +28,7 @@ export const FileUploaderRoot = observer((props: FileUploaderContainerProps): Re warningMessage = translations.get("uploadLimitReachedMessage", rootStore.maxTotalFiles.toString()); } else if (rootStore.createActionFailed) { warningMessage = translations.get("unavailableCreateActionMessage"); - } else if (rootStore.hasValidationErrors) { + } else if (rootStore.files.some(f => f.fileStatus === "validationError")) { warningMessage = translations.get("dropzoneRejectedMessage"); } diff --git a/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts index f0f9f53779..04a4305261 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts @@ -1018,3 +1018,33 @@ describe("upload queue — end-to-end", () => { expect(store.files.filter(f => f.fileStatus === "uploading")).toHaveLength(0); }); }); + +describe("FileUploaderStore.hasValidationErrors", () => { + test("returns true when validationError files exist", () => { + const store = buildStore(); + store.processDrop([], [{ file: makeFile("bad.exe"), errors: [{ code: "file-invalid-type", message: "bad" }] }]); + expect(store.hasValidationErrors).toBe(true); + }); + + test("returns false after all validationError files are dismissed", () => { + const store = buildStore(); + store.processDrop([], [{ file: makeFile("bad.exe"), errors: [{ code: "file-invalid-type", message: "bad" }] }]); + const errorFile = store.files.find(f => f.fileStatus === "validationError")!; + store.dismissFile(errorFile); + expect(store.hasValidationErrors).toBe(false); + }); + + test("remains true when one validationError file dismissed but others remain", () => { + const store = buildStore(); + store.processDrop( + [], + [ + { file: makeFile("a.exe"), errors: [{ code: "file-invalid-type", message: "bad" }] }, + { file: makeFile("b.exe"), errors: [{ code: "file-invalid-type", message: "bad" }] } + ] + ); + const firstError = store.files.find(f => f.fileStatus === "validationError")!; + store.dismissFile(firstError); + expect(store.hasValidationErrors).toBe(true); + }); +}); From 9978956884f4a4aaa99a81d33a292ac0b9c4e804 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Fri, 19 Jun 2026 14:35:09 +0200 Subject: [PATCH 4/4] fix(file-uploader-web): update validation error handling and tests --- .../src/components/Dropzone.tsx | 17 +++++++++++++---- .../src/stores/FileUploaderStore.ts | 5 ----- .../stores/__tests__/FileUploaderStore.spec.ts | 14 +++++++------- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/pluggableWidgets/file-uploader-web/src/components/Dropzone.tsx b/packages/pluggableWidgets/file-uploader-web/src/components/Dropzone.tsx index d8b4f51b3b..764b7c7cf0 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/components/Dropzone.tsx +++ b/packages/pluggableWidgets/file-uploader-web/src/components/Dropzone.tsx @@ -16,7 +16,7 @@ interface DropzoneProps { export const Dropzone = observer( ({ warningMessage, onDrop, maxSize, acceptFileTypes, disabled }: DropzoneProps): ReactElement => { - const { getRootProps, getInputProps, isDragAccept, isDragReject } = useDropzone({ + const { getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject } = useDropzone({ onDrop, maxSize: maxSize || undefined, accept: acceptFileTypes, @@ -24,7 +24,12 @@ export const Dropzone = observer( }); const translations = useTranslationsStore(); - const [type, msg] = getMessage(translations, isDragAccept, isDragReject); + const [type, msg] = getMessage( + translations, + isDragActive && isDragAccept, + isDragActive && isDragReject, + warningMessage + ); return ( @@ -32,7 +37,7 @@ export const Dropzone = observer( className={classNames("dropzone", { active: type === "active", disabled, - warning: !!warningMessage || type === "warning" + warning: type === "warning" })} {...getRootProps()} > @@ -52,7 +57,8 @@ type MessageType = "active" | "warning" | "idle"; function getMessage( translations: TranslationsStore, isDragAccept: boolean, - isDragReject: boolean + isDragReject: boolean, + warningMessage?: string ): [MessageType, string] { if (isDragAccept) { return ["active", translations.get("dropzoneAcceptedMessage")]; @@ -60,6 +66,9 @@ function getMessage( if (isDragReject) { return ["warning", translations.get("dropzoneRejectedMessage")]; } + if (warningMessage) { + return ["warning", warningMessage]; + } return ["idle", translations.get("dropzoneIdleMessage")]; } diff --git a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts index 9088737403..9fb7b02f1c 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts @@ -91,7 +91,6 @@ export class FileUploaderStore { _maxTotalFiles: observable, _maxConcurrentUploads: observable, isFileUploadLimitReached: computed, - hasValidationErrors: computed, sortedFiles: computed, activeCount: computed, uploadingCount: computed, @@ -189,10 +188,6 @@ export class FileUploaderStore { return this.activeCount >= this.maxTotalFiles; } - get hasValidationErrors(): boolean { - return this.files.some(f => f.fileStatus === "validationError"); - } - get sortedFiles(): FileStore[] { return [...this.files].sort((a, b) => { const isErrorA = a.fileStatus === "validationError" || a.fileStatus === "rejected" ? 1 : 0; diff --git a/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts index 04a4305261..722ea57bdf 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts @@ -1019,22 +1019,22 @@ describe("upload queue — end-to-end", () => { }); }); -describe("FileUploaderStore.hasValidationErrors", () => { - test("returns true when validationError files exist", () => { +describe("FileUploaderStore validationError files tracking", () => { + test("has validationError files after rejected drop", () => { const store = buildStore(); store.processDrop([], [{ file: makeFile("bad.exe"), errors: [{ code: "file-invalid-type", message: "bad" }] }]); - expect(store.hasValidationErrors).toBe(true); + expect(store.files.some(f => f.fileStatus === "validationError")).toBe(true); }); - test("returns false after all validationError files are dismissed", () => { + test("no validationError files after all are dismissed", () => { const store = buildStore(); store.processDrop([], [{ file: makeFile("bad.exe"), errors: [{ code: "file-invalid-type", message: "bad" }] }]); const errorFile = store.files.find(f => f.fileStatus === "validationError")!; store.dismissFile(errorFile); - expect(store.hasValidationErrors).toBe(false); + expect(store.files.some(f => f.fileStatus === "validationError")).toBe(false); }); - test("remains true when one validationError file dismissed but others remain", () => { + test("validationError files remain when one dismissed but others exist", () => { const store = buildStore(); store.processDrop( [], @@ -1045,6 +1045,6 @@ describe("FileUploaderStore.hasValidationErrors", () => { ); const firstError = store.files.find(f => f.fileStatus === "validationError")!; store.dismissFile(firstError); - expect(store.hasValidationErrors).toBe(true); + expect(store.files.some(f => f.fileStatus === "validationError")).toBe(true); }); });