Skip to content

Commit df1a4b7

Browse files
committed
feat(file-upload): improve max file upload
1 parent 6a4c621 commit df1a4b7

10 files changed

Lines changed: 98 additions & 12 deletions

File tree

packages/pluggableWidgets/file-uploader-web/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- We fixed an issue where file uploader can still add more files when refreshed eventhough the number of maximum uploaded files has been reached.
12+
13+
### Added
14+
15+
- We added a configuration to set maximum number of uploaded files through expression.
16+
917
## [2.2.2] - 2025-07-01
1018

1119
### Fixed

packages/pluggableWidgets/file-uploader-web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@mendix/file-uploader-web",
33
"widgetName": "FileUploader",
4-
"version": "2.2.2",
4+
"version": "2.3.0",
55
"description": "",
66
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
77
"license": "Apache-2.0",

packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export function getProperties(
4040
hidePropertiesIn(properties, values, ["customButtons"]);
4141
}
4242

43+
if (values.maxFilePerUploadType === "expression") {
44+
hidePropertiesIn(properties, values, ["maxFilesPerUpload"]);
45+
} else {
46+
hidePropertiesIn(properties, values, ["maxFilesPerUploadExpression"]);
47+
}
48+
4349
return properties;
4450
}
4551

packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,22 @@
8080
</propertyGroup>
8181
</properties>
8282
</property>
83+
<property key="maxFilePerUploadType" type="enumeration" defaultValue="number">
84+
<caption>Maximum number of files per upload type</caption>
85+
<description />
86+
<enumerationValues>
87+
<enumerationValue key="number">Number</enumerationValue>
88+
<enumerationValue key="expression">Expression</enumerationValue>
89+
</enumerationValues>
90+
</property>
91+
<property key="maxFilesPerUploadExpression" type="expression" defaultValue="10">
92+
<caption>Maximum number of files</caption>
93+
<description>Limit the number of files per upload.</description>
94+
<returnType type="Integer" />
95+
</property>
8396
<property key="maxFilesPerUpload" type="integer" defaultValue="10">
8497
<caption>Maximum number of files</caption>
85-
<description>Limit the number of files per one upload.</description>
98+
<description>Limit the number of files per upload.</description>
8699
</property>
87100
<property key="maxFileSize" type="integer" defaultValue="25">
88101
<caption>Maximum file size (MB)</caption>

packages/pluggableWidgets/file-uploader-web/src/components/Dropzone.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,24 @@ interface DropzoneProps {
1212
maxSize: number;
1313
maxFilesPerUpload: number;
1414
acceptFileTypes: MimeCheckFormat;
15+
disabled: boolean;
1516
}
1617

1718
export const Dropzone = observer(
18-
({ warningMessage, onDrop, maxSize, maxFilesPerUpload, acceptFileTypes }: DropzoneProps): ReactElement => {
19+
({
20+
warningMessage,
21+
onDrop,
22+
maxSize,
23+
maxFilesPerUpload,
24+
acceptFileTypes,
25+
disabled
26+
}: DropzoneProps): ReactElement => {
1927
const { getRootProps, getInputProps, isDragAccept, isDragReject } = useDropzone({
2028
onDrop,
2129
maxSize: maxSize || undefined,
2230
maxFiles: maxFilesPerUpload,
23-
accept: acceptFileTypes
31+
accept: acceptFileTypes,
32+
disabled
2433
});
2534

2635
const translations = useTranslationsStore();
@@ -31,14 +40,15 @@ export const Dropzone = observer(
3140
<div
3241
className={classNames("dropzone", {
3342
active: type === "active",
43+
disabled,
3444
warning: !!warningMessage || type === "warning"
3545
})}
3646
{...getRootProps()}
3747
>
3848
<div className={"file-icon"} />
39-
<p className={"upload-text"}>{msg}</p>
49+
{!disabled && <p className={"upload-text"}>{msg}</p>}
4050

41-
<input {...getInputProps()} />
51+
{!disabled && <input {...getInputProps()} />}
4252
</div>
4353
{warningMessage && <div className={classNames("dropzone-message")}>{warningMessage}</div>}
4454
</Fragment>

packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export const FileUploaderRoot = observer((props: FileUploaderContainerProps): Re
2929
warningMessage={rootStore.errorMessage}
3030
maxSize={rootStore._maxFileSize}
3131
acceptFileTypes={prepareAcceptForDropzone(rootStore.acceptedFileTypes)}
32-
maxFilesPerUpload={rootStore._maxFilesPerUpload}
32+
maxFilesPerUpload={rootStore.maxFilesPerUpload ?? 0}
33+
disabled={rootStore.isFileUploadLimitReached}
3334
/>
3435
)}
3536

packages/pluggableWidgets/file-uploader-web/src/package.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8" ?>
22
<package xmlns="http://www.mendix.com/package/1.0/">
3-
<clientModule name="FileUploader" version="2.2.2" xmlns="http://www.mendix.com/clientModule/1.0/">
3+
<clientModule name="FileUploader" version="2.3.0" xmlns="http://www.mendix.com/clientModule/1.0/">
44
<widgetFiles>
55
<widgetFile path="FileUploader.xml" />
66
</widgetFiles>

packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { ListValue, ObjectItem } from "mendix";
2-
import { FileUploaderContainerProps, UploadModeEnum } from "../../typings/FileUploaderProps";
1+
import { DynamicValue, ListValue, ObjectItem } from "mendix";
2+
import { FileUploaderContainerProps, MaxFilePerUploadTypeEnum, UploadModeEnum } from "../../typings/FileUploaderProps";
33
import { action, computed, makeObservable, observable } from "mobx";
4+
import { Big } from "big.js";
45
import { getImageUploaderFormats, parseAllowedFormats } from "../utils/parseAllowedFormats";
56
import { FileStore } from "./FileStore";
67
import { FileRejection } from "react-dropzone";
@@ -27,6 +28,8 @@ export class FileUploaderStore {
2728
_maxFileSize = 0;
2829
_ds?: ListValue;
2930
_maxFilesPerUpload: number;
31+
_maxFilePerUploadType: MaxFilePerUploadTypeEnum;
32+
_maxFilesPerUploadExpression: DynamicValue<Big>;
3033

3134
errorMessage?: string = undefined;
3235

@@ -37,6 +40,8 @@ export class FileUploaderStore {
3740
this._maxFileSizeMiB = props.maxFileSize;
3841
this._maxFileSize = this._maxFileSizeMiB * 1024 * 1024;
3942
this._maxFilesPerUpload = props.maxFilesPerUpload;
43+
this._maxFilePerUploadType = props.maxFilePerUploadType;
44+
this._maxFilesPerUploadExpression = props.maxFilesPerUploadExpression;
4045
this._uploadMode = props.uploadMode;
4146

4247
this.objectCreationHelper = new ObjectCreationHelper(this._widgetName, props.objectCreationTimeout);
@@ -79,7 +84,9 @@ export class FileUploaderStore {
7984
files: observable,
8085
existingItemsLoaded: observable,
8186
errorMessage: observable,
82-
allowedFormatsDescription: computed
87+
allowedFormatsDescription: computed,
88+
maxFilesPerUpload: computed,
89+
isFileUploadLimitReached: computed
8390
});
8491

8592
this.updateProps(props);
@@ -94,6 +101,11 @@ export class FileUploaderStore {
94101
this._ds = props.associatedImages;
95102
}
96103

104+
// Update max files properties
105+
this._maxFilesPerUpload = props.maxFilesPerUpload;
106+
this._maxFilePerUploadType = props.maxFilePerUploadType;
107+
this._maxFilesPerUploadExpression = props.maxFilesPerUploadExpression;
108+
97109
this.translations.updateProps(props);
98110
this.updateProcessor.processUpdate(this._ds);
99111
}
@@ -113,6 +125,28 @@ export class FileUploaderStore {
113125
.join(", ");
114126
}
115127

128+
get maxFilesPerUpload(): number | undefined {
129+
if (this._maxFilePerUploadType === "expression") {
130+
const expressionValue = this._maxFilesPerUploadExpression.value;
131+
if (expressionValue && !isNaN(Number(expressionValue))) {
132+
return Number(expressionValue);
133+
}
134+
// Fallback to default if expression is invalid
135+
return undefined;
136+
}
137+
return this._maxFilesPerUpload;
138+
}
139+
140+
get isFileUploadLimitReached(): boolean {
141+
const activeFiles = this.files.filter(
142+
file => file.fileStatus !== "missing" && file.fileStatus !== "removedFile"
143+
);
144+
if (!this.maxFilesPerUpload || this.maxFilesPerUpload === 0) {
145+
return false;
146+
}
147+
return activeFiles.length >= this.maxFilesPerUpload;
148+
}
149+
116150
setMessage(msg?: string): void {
117151
this.errorMessage = msg;
118152
}
@@ -128,7 +162,7 @@ export class FileUploaderStore {
128162

129163
if (fileRejections.length && fileRejections[0].errors[0].code === "too-many-files") {
130164
this.setMessage(
131-
this.translations.get("uploadFailureTooManyFilesMessage", this._maxFilesPerUpload.toString())
165+
this.translations.get("uploadFailureTooManyFilesMessage", this.maxFilesPerUpload?.toString() ?? "")
132166
);
133167
return;
134168
}

packages/pluggableWidgets/file-uploader-web/src/ui/FileUploader.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ Place your custom CSS here
7474
border: 1.5px solid var(--brand-primary, $file-brand-primary);
7575
background-color: var(--color-primary-lighter, $file-color-primary-lighter);
7676
}
77+
&.disabled {
78+
border: 1.5px dashed var(--border-color-default, $file-border-color-default);
79+
background-color: var(--bg-color, $file-bg-color);
80+
.file-icon {
81+
opacity: 0.5;
82+
}
83+
}
7784

7885
.file-icon {
7986
flex: 0 0 34px;

packages/pluggableWidgets/file-uploader-web/typings/FileUploaderProps.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
import { CSSProperties } from "react";
77
import { ActionValue, DynamicValue, ListValue, ListActionValue, WebIcon } from "mendix";
8+
import { Big } from "big.js";
89

910
export type UploadModeEnum = "files" | "images";
1011

@@ -20,6 +21,8 @@ export interface AllowedFileFormatsType {
2021
typeFormatDescription: DynamicValue<string>;
2122
}
2223

24+
export type MaxFilePerUploadTypeEnum = "number" | "expression";
25+
2326
export interface CustomButtonsType {
2427
buttonCaption: DynamicValue<string>;
2528
buttonIcon: DynamicValue<WebIcon>;
@@ -58,6 +61,8 @@ export interface FileUploaderContainerProps {
5861
createFileAction?: ActionValue;
5962
createImageAction?: ActionValue;
6063
allowedFileFormats: AllowedFileFormatsType[];
64+
maxFilePerUploadType: MaxFilePerUploadTypeEnum;
65+
maxFilesPerUploadExpression: DynamicValue<Big>;
6166
maxFilesPerUpload: number;
6267
maxFileSize: number;
6368
dropzoneIdleMessage: DynamicValue<string>;
@@ -97,6 +102,8 @@ export interface FileUploaderPreviewProps {
97102
createFileAction: {} | null;
98103
createImageAction: {} | null;
99104
allowedFileFormats: AllowedFileFormatsPreviewType[];
105+
maxFilePerUploadType: MaxFilePerUploadTypeEnum;
106+
maxFilesPerUploadExpression: string;
100107
maxFilesPerUpload: number | null;
101108
maxFileSize: number | null;
102109
dropzoneIdleMessage: string;

0 commit comments

Comments
 (0)