diff --git a/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md b/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md
index bb04444d0f..88b9ffff0a 100644
--- a/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md
+++ b/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [Unreleased]
+### Fixed
+
+- We fixed an issue where validation errors could not be dismissed and persisted after uploading a valid file.
+
## [2.4.2] - 2026-04-23
### Fixed
diff --git a/packages/pluggableWidgets/file-uploader-web/src/components/ActionsBar.tsx b/packages/pluggableWidgets/file-uploader-web/src/components/ActionsBar.tsx
index 0d74d7ad0d..6557d83587 100644
--- a/packages/pluggableWidgets/file-uploader-web/src/components/ActionsBar.tsx
+++ b/packages/pluggableWidgets/file-uploader-web/src/components/ActionsBar.tsx
@@ -11,6 +11,10 @@ interface ButtonsBarProps {
}
export const ActionsBar = ({ actions, store }: ButtonsBarProps): ReactElement | null => {
+ if (store.fileStatus === "validationError") {
+ return ;
+ }
+
if (!actions) {
return ;
}
@@ -70,6 +74,25 @@ function DefaultActionsBar(props: ButtonsBarProps): ReactElement {
);
}
+function DismissActionsBar({ store }: ButtonsBarProps): ReactElement {
+ const translations = useTranslationsStore();
+
+ const onDismiss = useCallback(() => {
+ store.dismiss();
+ }, [store]);
+
+ return (
+
+
}
+ title={translations.get("removeButtonTextMessage")}
+ action={onDismiss}
+ isDisabled={false}
+ />
+
+ );
+}
+
function onDownloadClick(fileUrl: string | undefined): void {
if (!fileUrl) {
return;
diff --git a/packages/pluggableWidgets/file-uploader-web/src/components/__tests__/DismissActionsBar.spec.tsx b/packages/pluggableWidgets/file-uploader-web/src/components/__tests__/DismissActionsBar.spec.tsx
new file mode 100644
index 0000000000..4008eb5197
--- /dev/null
+++ b/packages/pluggableWidgets/file-uploader-web/src/components/__tests__/DismissActionsBar.spec.tsx
@@ -0,0 +1,75 @@
+import "@testing-library/jest-dom";
+import { fireEvent, render, screen } from "@testing-library/react";
+import { FileUploaderContainerProps } from "../../../typings/FileUploaderProps";
+import { FileStore } from "../../stores/FileStore";
+import { TranslationsStoreProvider } from "../../utils/useTranslationsStore";
+import { ActionsBar } from "../ActionsBar";
+
+jest.mock("../../utils/mx-data", () => ({
+ fetchDocumentUrl: jest.fn(),
+ fetchImageThumbnail: jest.fn(),
+ fetchMxObject: jest.fn(),
+ removeObject: jest.fn(),
+ saveFile: jest.fn(),
+ fileHasContents: jest.fn()
+}));
+
+function makeFakeProps(): FileUploaderContainerProps {
+ const dv = (v: string): { value: string; status: string } => ({ value: v, status: "available" });
+ return {
+ name: "fileUploader1",
+ uploadMode: "files",
+ maxFileSize: 10,
+ maxFilesPerUpload: { value: { toNumber: () => 5 } },
+ readOnlyMode: false,
+ objectCreationTimeout: 30,
+ allowedFileFormats: "",
+ removeButtonTextMessage: dv("Remove"),
+ downloadButtonTextMessage: dv("Download"),
+ unavailableCreateActionMessage: dv("Unavailable"),
+ uploadFailureTooManyFilesMessage: dv("Too many"),
+ uploadFailureInvalidFileFormatMessage: dv("Invalid format"),
+ uploadFailureFileIsTooBigMessage: dv("Too big")
+ } as unknown as FileUploaderContainerProps;
+}
+
+function makeValidationErrorStore(): { store: FileStore; dismiss: jest.Mock } {
+ const dismiss = jest.fn();
+ const rootStore = { dismissFile: dismiss, _uploadMode: "files", isReadOnly: false } as any;
+ const store = FileStore.newFileWithError(new File([], "bad.txt"), "bad format", rootStore);
+ return { store, dismiss };
+}
+
+function renderWithTranslations(store: FileStore): void {
+ const props = makeFakeProps();
+ render(
+
+
+
+ );
+}
+
+describe("DismissActionsBar", () => {
+ it("renders dismiss button for validationError file", () => {
+ const { store } = makeValidationErrorStore();
+ renderWithTranslations(store);
+ expect(screen.getByRole("button")).toBeInTheDocument();
+ });
+
+ it("calls store.dismiss() when button clicked", () => {
+ const { store } = makeValidationErrorStore();
+ const dismissSpy = jest.spyOn(store, "dismiss");
+ renderWithTranslations(store);
+ fireEvent.click(screen.getByRole("button"));
+ expect(dismissSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("does not render DismissActionsBar for non-validationError file", () => {
+ const rootStore = { dismissFile: jest.fn(), _uploadMode: "files", isReadOnly: false } as any;
+ const store = FileStore.newFile(new File([], "ok.txt"), rootStore);
+ (store as any).fileStatus = "done";
+ renderWithTranslations(store);
+ // DefaultActionsBar renders 2 buttons (download + remove)
+ expect(screen.queryAllByRole("button")).toHaveLength(2);
+ });
+});
diff --git a/packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts
index dc1b1e3a7f..74fddc56c3 100644
--- a/packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts
+++ b/packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts
@@ -60,7 +60,8 @@ export class FileStore {
imagePreviewUrl: computed,
upload: action,
fetchMxObject: action,
- markMissing: action
+ markMissing: action,
+ dismiss: action
});
}
@@ -76,6 +77,10 @@ export class FileStore {
this.errorDescription = errorMessage;
}
+ dismiss(): void {
+ this._rootStore.dismissFile(this);
+ }
+
canExecute(listAction: ListActionValue): boolean {
if (!this._objectItem) {
return false;
@@ -123,6 +128,7 @@ export class FileStore {
runInAction(() => {
this.fileStatus = "done";
this._rootStore.objectCreationHelper.reportUploadSuccess(this._objectItem!);
+ this._rootStore.dismissValidationErrors();
});
} catch (_e: unknown) {
runInAction(() => {
diff --git a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts
index eb44dfb68f..ca829d93db 100644
--- a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts
+++ b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts
@@ -74,6 +74,8 @@ export class FileUploaderStore {
makeObservable(this, {
updateProps: action,
processDrop: action,
+ dismissValidationErrors: action,
+ dismissFile: action,
setMessage: action,
processExistingFileItem: action,
files: observable,
@@ -142,6 +144,14 @@ export class FileUploaderStore {
this.errorMessage = msg;
}
+ dismissValidationErrors(): void {
+ this.files = this.files.filter(file => file.fileStatus !== "validationError");
+ }
+
+ dismissFile(file: FileStore): void {
+ this.files = this.files.filter(f => f !== file);
+ }
+
processDrop(acceptedFiles: File[], fileRejections: FileRejection[]): void {
if (!this.objectCreationHelper.canCreateFiles) {
console.error(
@@ -158,6 +168,7 @@ export class FileUploaderStore {
return;
}
+ this.dismissValidationErrors();
this.setMessage();
for (const file of fileRejections) {
diff --git a/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileStore.spec.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileStore.spec.ts
new file mode 100644
index 0000000000..69ce298121
--- /dev/null
+++ b/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileStore.spec.ts
@@ -0,0 +1,29 @@
+import { FileStore } from "../FileStore";
+import { FileUploaderStore } from "../FileUploaderStore";
+
+jest.mock("../../utils/mx-data", () => ({
+ fetchDocumentUrl: jest.fn(),
+ fetchImageThumbnail: jest.fn(),
+ fetchMxObject: jest.fn(),
+ removeObject: jest.fn(),
+ saveFile: jest.fn(),
+ fileHasContents: jest.fn()
+}));
+
+function makeRootStore(): { dismissFile: jest.Mock } {
+ return {
+ dismissFile: jest.fn(),
+ _uploadMode: "files",
+ isReadOnly: false
+ } as unknown as FileUploaderStore & { dismissFile: jest.Mock };
+}
+
+describe("FileStore.dismiss()", () => {
+ it("calls dismissFile on root store with itself", () => {
+ const rootStore = makeRootStore();
+ const store = FileStore.newFileWithError(new File([], "test.txt"), "bad format", rootStore as any);
+ store.dismiss();
+ expect(rootStore.dismissFile).toHaveBeenCalledTimes(1);
+ expect(rootStore.dismissFile).toHaveBeenCalledWith(store);
+ });
+});
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
new file mode 100644
index 0000000000..48afcb408d
--- /dev/null
+++ b/packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts
@@ -0,0 +1,109 @@
+import { FileStore } from "../FileStore";
+import { FileUploaderStore } from "../FileUploaderStore";
+import { TranslationsStore } from "../TranslationsStore";
+
+jest.mock("../../utils/mx-data", () => ({
+ fetchDocumentUrl: jest.fn(),
+ fetchImageThumbnail: jest.fn(),
+ fetchMxObject: jest.fn(),
+ removeObject: jest.fn(),
+ saveFile: jest.fn(),
+ fileHasContents: jest.fn()
+}));
+
+function makeStore(): FileUploaderStore {
+ const translations = { get: jest.fn(() => "msg"), updateProps: jest.fn() } as unknown as TranslationsStore;
+ const store = Object.create(FileUploaderStore.prototype) as FileUploaderStore;
+ store.files = [];
+ return Object.assign(store, {
+ translations,
+ objectCreationHelper: { canCreateFiles: true, enable: jest.fn(), updateProps: jest.fn(), request: jest.fn() },
+ updateProcessor: { processUpdate: jest.fn() },
+ isReadOnly: false,
+ _uploadMode: "files" as const,
+ _maxFileSizeMiB: 10,
+ _maxFileSize: 10 * 1024 * 1024,
+ acceptedFileTypes: [],
+ existingItemsLoaded: false
+ });
+}
+
+function makeValidationErrorFile(store: FileUploaderStore): FileStore {
+ return FileStore.newFileWithError(new File([], "bad.txt"), "bad format", store);
+}
+
+function makeDoneFile(store: FileUploaderStore): FileStore {
+ const f = FileStore.newFile(new File([], "good.txt"), store);
+ (f as any).fileStatus = "done";
+ return f;
+}
+
+describe("FileUploaderStore.dismissValidationErrors()", () => {
+ it("removes only validationError files", () => {
+ const store = makeStore();
+ const validationFile = makeValidationErrorFile(store);
+ const doneFile = makeDoneFile(store);
+ store.files = [validationFile, doneFile];
+
+ store.dismissValidationErrors();
+
+ expect(store.files).toHaveLength(1);
+ expect(store.files[0]).toBe(doneFile);
+ });
+
+ it("leaves files untouched when none have validationError status", () => {
+ const store = makeStore();
+ const doneFile = makeDoneFile(store);
+ store.files = [doneFile];
+
+ store.dismissValidationErrors();
+
+ expect(store.files).toHaveLength(1);
+ });
+
+ it("empties files when all have validationError status", () => {
+ const store = makeStore();
+ store.files = [makeValidationErrorFile(store), makeValidationErrorFile(store)];
+
+ store.dismissValidationErrors();
+
+ expect(store.files).toHaveLength(0);
+ });
+});
+
+describe("FileUploaderStore.dismissFile()", () => {
+ it("removes the specific file from the list", () => {
+ const store = makeStore();
+ const fileA = makeValidationErrorFile(store);
+ const fileB = makeValidationErrorFile(store);
+ store.files = [fileA, fileB];
+
+ store.dismissFile(fileA);
+
+ expect(store.files).toHaveLength(1);
+ expect(store.files[0]).toBe(fileB);
+ });
+
+ it("does not remove other files", () => {
+ const store = makeStore();
+ const fileA = makeValidationErrorFile(store);
+ const fileB = makeDoneFile(store);
+ store.files = [fileA, fileB];
+
+ store.dismissFile(fileA);
+
+ expect(store.files[0]).toBe(fileB);
+ });
+
+ it("is a no-op when file is not in the list", () => {
+ const store = makeStore();
+ const fileA = makeValidationErrorFile(store);
+ const fileB = makeValidationErrorFile(store);
+ store.files = [fileA];
+
+ store.dismissFile(fileB);
+
+ expect(store.files).toHaveLength(1);
+ expect(store.files[0]).toBe(fileA);
+ });
+});
diff --git a/packages/pluggableWidgets/file-uploader-web/src/ui/FileUploader.scss b/packages/pluggableWidgets/file-uploader-web/src/ui/FileUploader.scss
index 4f9bf3c3cf..d3a0905fe8 100644
--- a/packages/pluggableWidgets/file-uploader-web/src/ui/FileUploader.scss
+++ b/packages/pluggableWidgets/file-uploader-web/src/ui/FileUploader.scss
@@ -328,13 +328,13 @@ Place your custom CSS here
}
&.invalid {
- opacity: 0.7;
+ .entry-details-preview,
+ .entry-details-main {
+ opacity: 0.5;
+ }
.download-icon {
visibility: hidden;
}
- .entry-details-actions {
- display: none;
- }
}
}
}