Skip to content
Open
17 changes: 17 additions & 0 deletions packages/pluggableWidgets/file-uploader-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Fixed

- We fixed an issue where the dropzone turned grey without explanation when the file limit was reached. A message now appears below the dropzone stating "Maximum file count of X reached."
- We fixed an issue where dropping more files than allowed rejected the entire batch. Only the excess are shown as errors.
- We fixed an issue where files rejected due to the upload or batch limit could not recover. They now automatically retry when capacity becomes available.

### Added

- We added a new "File limit reached" text property to customize the message shown when the upload limit is reached.
- We added a new "Maximum files per upload batch" property to limit how many files are committed to the server per drop event. Files exceeding the batch limit appear in the list with an error message and retry automatically when capacity is freed.
- We added a new "Batch limit exceeded" text property to customize the message shown on files that exceeded the batch limit.

### Changed

- The "Maximum number of files" property is now optional. Leaving it empty or setting it to 0 means unlimited files are allowed. The default behavior is now unlimited (no cap).
- Files in the list are now ordered with successful uploads above rejected files.

## [2.4.2] - 2026-04-23

### Fixed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { hideNestedPropertiesIn, hidePropertiesIn, Problem, Properties } from "@mendix/pluggable-widgets-tools";
import { FileUploaderPreviewProps } from "../typings/FileUploaderProps";
import { parseAllowedFormats } from "./utils/parseAllowedFormats";
import { hideNestedPropertiesIn, hidePropertiesIn, Problem, Properties } from "@mendix/pluggable-widgets-tools";
import { predefinedFormats } from "./utils/predefinedFormats";

export function getProperties(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import classNames from "classnames";
import { ReactElement } from "react";
import { FileUploaderPreviewProps } from "../typings/FileUploaderProps";
import classNames from "classnames";

export function preview(props: FileUploaderPreviewProps): ReactElement {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,14 @@
</propertyGroup>
</properties>
</property>
<property key="maxFilesPerUpload" type="expression" defaultValue="10">
<property key="maxFilesPerUpload" type="expression" defaultValue="10" required="false">
<caption>Maximum number of files</caption>
<description>Limit the number of files per upload.</description>
<description>Maximum total number of files that can be associated at once. Leave empty or set to 0 for unlimited. Use this to cap the total number of attachments.</description>
<returnType type="Integer" />
</property>
<property key="maxFilesPerBatch" type="expression" required="false">
<caption>Maximum files per upload batch</caption>
<description>Limits how many files are committed to the server in a single drop or selection. Leave empty or set to 0 for unlimited. Smaller batch sizes reduce peak server load.</description>
<returnType type="Integer" />
</property>
<property key="maxFileSize" type="integer" defaultValue="25">
Expand Down Expand Up @@ -163,6 +168,22 @@
<translation lang="nl_NL">Te veel bestanden toegevoegd. Slechts ### bestanden per upload zijn toegestaan.</translation>
</translations>
</property>
<property key="uploadLimitReachedMessage" type="textTemplate">
<caption>File limit reached</caption>
<description>Shown below the dropzone when the maximum number of files is already reached.</description>
<translations>
<translation lang="en_US">Maximum file count of ### reached.</translation>
<translation lang="nl_NL">Maximum aantal bestanden van ### bereikt.</translation>
</translations>
</property>
<property key="uploadBatchLimitExceededMessage" type="textTemplate">
<caption>Batch limit exceeded</caption>
<description>Shown on files that were dropped but not uploaded because the batch limit was already reached.</description>
<translations>
<translation lang="en_US">File not uploaded. Batch limit of ### files per drop was reached.</translation>
<translation lang="nl_NL">Bestand niet geüpload. Batchlimiet van ### bestanden per upload is bereikt.</translation>
</translations>
</property>
<property key="unavailableCreateActionMessage" type="textTemplate">
<caption>Action to create new files is not available or failed</caption>
<description />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MouseEvent, ReactElement, useCallback } from "react";
import classNames from "classnames";
import { ListActionValue } from "mendix";
import { MouseEvent, ReactElement, useCallback } from "react";
import { FileStore } from "../stores/FileStore";

interface ActionButtonProps {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ReactElement, useCallback } from "react";
import { FileUploaderContainerProps } from "../../typings/FileUploaderProps";
import { ActionButton, FileActionButton } from "./ActionButton";
import { IconInternal } from "@mendix/widget-plugin-component-kit/IconInternal";
import { ActionButton, FileActionButton } from "./ActionButton";
import { FileUploaderContainerProps } from "../../typings/FileUploaderProps";
import { FileStore } from "../stores/FileStore";
import { useTranslationsStore } from "../utils/useTranslationsStore";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,24 @@
import { observer } from "mobx-react-lite";
import classNames from "classnames";
import { observer } from "mobx-react-lite";
import { Fragment, ReactElement } from "react";
import { FileRejection, useDropzone } from "react-dropzone";
import { MimeCheckFormat } from "../utils/parseAllowedFormats";
import { TranslationsStore } from "../stores/TranslationsStore";
import { MimeCheckFormat } from "../utils/parseAllowedFormats";
import { useTranslationsStore } from "../utils/useTranslationsStore";

interface DropzoneProps {
warningMessage?: string;
onDrop: (files: File[], fileRejections: FileRejection[]) => void;
maxSize: number;
maxFilesPerUpload: number;
acceptFileTypes: MimeCheckFormat;
disabled: boolean;
}

export const Dropzone = observer(
({
warningMessage,
onDrop,
maxSize,
maxFilesPerUpload,
acceptFileTypes,
disabled
}: DropzoneProps): ReactElement => {
({ warningMessage, onDrop, maxSize, acceptFileTypes, disabled }: DropzoneProps): ReactElement => {
const { getRootProps, getInputProps, isDragAccept, isDragReject } = useDropzone({
onDrop,
maxSize: maxSize || undefined,
maxFiles: maxFilesPerUpload,
accept: acceptFileTypes,
disabled
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import classNames from "classnames";
import { observer } from "mobx-react-lite";
import { KeyboardEvent, MouseEvent, ReactElement, ReactNode, useCallback } from "react";
import { ActionsBar } from "./ActionsBar";
import { FileIcon } from "./FileIcon";
import { ProgressBar } from "./ProgressBar";
import { UploadInfo } from "./UploadInfo";
import { KeyboardEvent, MouseEvent, ReactElement, ReactNode, useCallback } from "react";
import { FileUploaderContainerProps } from "../../typings/FileUploaderProps";
import { FileStatus, FileStore } from "../stores/FileStore";
import { observer } from "mobx-react-lite";
import { FileIcon } from "./FileIcon";
import { fileSize } from "../utils/fileSize";
import { FileUploaderContainerProps } from "../../typings/FileUploaderProps";
import { ActionsBar } from "./ActionsBar";

interface FileEntryContainerProps {
store: FileStore;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { observer } from "mobx-react-lite";
import { ReactElement, useCallback } from "react";
import { FileRejection } from "react-dropzone";

import { Dropzone } from "./Dropzone";
import { FileEntryContainer } from "./FileEntry";
import { FileUploaderContainerProps } from "../../typings/FileUploaderProps";
import { prepareAcceptForDropzone } from "../utils/prepareAcceptForDropzone";
import { useRootStore } from "../utils/useRootStore";
import { FileEntryContainer } from "./FileEntry";
import { Dropzone } from "./Dropzone";

import "../ui/FileUploader.scss";

Expand All @@ -26,16 +26,15 @@ export const FileUploaderRoot = observer((props: FileUploaderContainerProps): Re
{!rootStore.isReadOnly && (
<Dropzone
onDrop={onDrop}
warningMessage={rootStore.errorMessage}
warningMessage={rootStore.warningMessage}
maxSize={rootStore._maxFileSize}
acceptFileTypes={prepareAcceptForDropzone(rootStore.acceptedFileTypes)}
maxFilesPerUpload={rootStore.maxFilesPerUpload ?? 0}
disabled={rootStore.isFileUploadLimitReached}
/>
)}

<div className={"files-list"}>
{(rootStore.files ?? []).map(fileStore => {
{rootStore.sortedFiles.map(fileStore => {
return (
<FileEntryContainer
store={fileStore}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FileStatus } from "../stores/FileStore";
import { ReactElement } from "react";
import { FileStatus } from "../stores/FileStore";
import { useTranslationsStore } from "../utils/useTranslationsStore";

type UploadInfoProps = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Big } from "big.js";
import { ListActionValue, ObjectItem } from "mendix";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import mimeTypes from "mime-types";
import { action, computed, makeObservable, observable, runInAction } from "mobx";

import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action";
import { FileUploaderStore } from "./FileUploaderStore";
import {
fetchDocumentUrl,
Expand All @@ -12,7 +13,6 @@ import {
removeObject,
saveFile
} from "../utils/mx-data";
import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action";

export type FileStatus =
| "existingFile"
Expand Down Expand Up @@ -43,6 +43,7 @@ export class FileStore {
key: number;

errorDescription?: string = undefined;
errorType?: "limitExceeded" | "batchExceeded" | "validation" = undefined;

constructor(type: FileStatus, rootStore: FileUploaderStore, file?: File, objectItem?: ObjectItem) {
this.key = getFileKey();
Expand All @@ -55,12 +56,15 @@ export class FileStore {
fileStatus: observable,
_mxObject: observable,
errorDescription: observable,
errorType: observable,
_thumbnailUrl: observable,
canRemove: computed,
imagePreviewUrl: computed,
upload: action,
fetchMxObject: action,
markMissing: action
markMissing: action,
markError: action,
reset: action
});
}

Expand All @@ -71,9 +75,16 @@ export class FileStore {
this._objectItem = undefined;
}

markError(errorMessage: string): void {
markError(errorMessage: string, errorType: "limitExceeded" | "batchExceeded" | "validation" = "validation"): void {
this.fileStatus = "validationError";
this.errorDescription = errorMessage;
this.errorType = errorType;
}

reset(): void {
this.errorType = undefined;
this.errorDescription = undefined;
this.fileStatus = "new";
}

canExecute(listAction: ListActionValue): boolean {
Expand Down Expand Up @@ -237,10 +248,16 @@ export class FileStore {
return new FileStore("new", rootStore, file, undefined);
}

static newFileWithError(file: File, errorMessage: string, rootStore: FileUploaderStore): FileStore {
static newFileWithError(
file: File,
errorMessage: string,
rootStore: FileUploaderStore,
errorType: "limitExceeded" | "batchExceeded" | "validation" = "validation"
): FileStore {
const store = new FileStore("validationError", rootStore, file, undefined);
runInAction(() => {
store.errorDescription = errorMessage;
store.errorType = errorType;
});

return store;
Expand Down
Loading
Loading