Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 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,19 @@ 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."

### 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 or selection. Files exceeding the batch limit appear in the list with an error message explaining why they were not uploaded.

### 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).

## [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,9 +1,9 @@
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 {
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,7 +26,7 @@ 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}
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
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Big } from "big.js";
import { DynamicValue, ObjectItem } from "mendix";
import { FileUploaderContainerProps, UploadModeEnum } from "../../typings/FileUploaderProps";
import { action, computed, makeObservable, observable } from "mobx";
import { Big } from "big.js";
import { getImageUploaderFormats, parseAllowedFormats } from "../utils/parseAllowedFormats";
import { FileStore } from "./FileStore";
import { FileRejection } from "react-dropzone";
import { FileCheckFormat } from "../utils/predefinedFormats";
import { FileStore } from "./FileStore";
import { TranslationsStore } from "./TranslationsStore";
import { ObjectCreationHelper } from "../utils/ObjectCreationHelper";
import { FileUploaderContainerProps, UploadModeEnum } from "../../typings/FileUploaderProps";
import { DatasourceUpdateProcessor } from "../utils/DatasourceUpdateProcessor";
import { ObjectCreationHelper } from "../utils/ObjectCreationHelper";
import { getImageUploaderFormats, parseAllowedFormats } from "../utils/parseAllowedFormats";
import { FileCheckFormat } from "../utils/predefinedFormats";

export class FileUploaderStore {
files: FileStore[] = [];
Expand All @@ -26,7 +26,8 @@ export class FileUploaderStore {
_uploadMode: UploadModeEnum;
_maxFileSizeMiB = 0;
_maxFileSize = 0;
_maxFilesPerUpload: DynamicValue<Big>;
_maxFilesPerUpload: DynamicValue<Big> | undefined;
_maxFilesPerBatch: DynamicValue<Big> | undefined;

errorMessage?: string = undefined;

Expand All @@ -37,6 +38,7 @@ export class FileUploaderStore {
this._maxFileSizeMiB = props.maxFileSize;
this._maxFileSize = this._maxFileSizeMiB * 1024 * 1024;
this._maxFilesPerUpload = props.maxFilesPerUpload;
this._maxFilesPerBatch = props.maxFilesPerBatch;
this._uploadMode = props.uploadMode;

this.objectCreationHelper = new ObjectCreationHelper(this._widgetName, props.objectCreationTimeout);
Expand Down Expand Up @@ -81,8 +83,11 @@ export class FileUploaderStore {
errorMessage: observable,
allowedFormatsDescription: computed,
maxFilesPerUpload: computed,
maxFilesPerBatch: computed,
_maxFilesPerUpload: observable,
isFileUploadLimitReached: computed
_maxFilesPerBatch: observable,
isFileUploadLimitReached: computed,
warningMessage: computed
});

this.updateProps(props);
Expand All @@ -91,8 +96,8 @@ export class FileUploaderStore {
updateProps(props: FileUploaderContainerProps): void {
this.objectCreationHelper.updateProps(props);

// Update max files properties
this._maxFilesPerUpload = props.maxFilesPerUpload;
this._maxFilesPerBatch = props.maxFilesPerBatch;

this.translations.updateProps(props);
this.updateProcessor.processUpdate(
Expand All @@ -116,11 +121,18 @@ export class FileUploaderStore {
}

get maxFilesPerUpload(): number {
const expressionValue = this._maxFilesPerUpload.value;
const expressionValue = this._maxFilesPerUpload?.value;
if (expressionValue) {
return expressionValue.toNumber();
}
return 0;
}

get maxFilesPerBatch(): number {
const expressionValue = this._maxFilesPerBatch?.value;
if (expressionValue) {
return expressionValue.toNumber();
}
// Fallback to unlimited
return 0;
}

Expand All @@ -138,6 +150,13 @@ export class FileUploaderStore {
return activeFiles.length >= this.maxFilesPerUpload;
}

get warningMessage(): string | undefined {
if (this.isFileUploadLimitReached) {
return this.translations.get("uploadLimitReachedMessage", this.maxFilesPerUpload.toString());
}
return this.errorMessage;
}

setMessage(msg?: string): void {
this.errorMessage = msg;
}
Expand All @@ -160,6 +179,11 @@ export class FileUploaderStore {

this.setMessage();

const batchLimit = this.maxFilesPerBatch;
const filesToProcess =
batchLimit > 0 && acceptedFiles.length > batchLimit ? acceptedFiles.slice(0, batchLimit) : acceptedFiles;
const batchExcess = batchLimit > 0 && acceptedFiles.length > batchLimit ? acceptedFiles.slice(batchLimit) : [];

for (const file of fileRejections) {
const newFileStore = FileStore.newFileWithError(
file.file,
Expand All @@ -186,7 +210,16 @@ export class FileUploaderStore {
this.files.unshift(newFileStore);
}

for (const file of acceptedFiles) {
for (const file of batchExcess) {
const newFileStore = FileStore.newFileWithError(
file,
this.translations.get("uploadBatchLimitExceededMessage", batchLimit.toString()),
this
);
this.files.unshift(newFileStore);
}

for (const file of filesToProcess) {
const newFileStore = FileStore.newFile(file, this);

if (this.isFileUploadLimitReached) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FileUploaderContainerProps } from "../../typings/FileUploaderProps";
import { DynamicValue } from "mendix";
import { action, makeObservable, observable } from "mobx";
import { FileUploaderContainerProps } from "../../typings/FileUploaderProps";

export class TranslationsStore {
translationsMap: Map<keyof FileUploaderContainerProps, string> = new Map();
Expand Down
Loading
Loading