Skip to content

Commit 79baf6f

Browse files
committed
feat(file-uploader): limit total number of files and make it dynamic
1 parent 66b60d0 commit 79baf6f

File tree

11 files changed

+84
-22
lines changed

11 files changed

+84
-22
lines changed

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+
### Changed
14+
15+
- We change the max file 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: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,6 @@ export function check(values: FileUploaderPreviewProps): Problem[] {
8989
}
9090
}
9191

92-
if (!values.maxFilesPerUpload || values.maxFilesPerUpload < 1) {
93-
errors.push({
94-
property: "maxFilesPerUpload",
95-
message: "There must be at least one file per upload allowed."
96-
});
97-
}
98-
9992
if (values.enableCustomButtons) {
10093
// check that at max one actions is default
10194
const defaultIdx = new Set<number>();

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,10 @@
8080
</propertyGroup>
8181
</properties>
8282
</property>
83-
<property key="maxFilesPerUpload" type="integer" defaultValue="10">
83+
<property key="maxFilesPerUpload" type="expression" defaultValue="10">
8484
<caption>Maximum number of files</caption>
85-
<description>Limit the number of files per one upload.</description>
85+
<description>Limit the number of files per upload.</description>
86+
<returnType type="Integer" />
8687
</property>
8788
<property key="maxFileSize" type="integer" defaultValue="25">
8889
<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/FileStore.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ export class FileStore {
6969
this._objectItem = undefined;
7070
}
7171

72+
markError(errorMessage: string): void {
73+
this.fileStatus = "validationError";
74+
this.errorDescription = errorMessage;
75+
}
76+
7277
canExecute(listAction: ListActionValue): boolean {
7378
if (!this._objectItem) {
7479
return false;

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

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { ListValue, ObjectItem } from "mendix";
1+
import { DynamicValue, ListValue, ObjectItem } from "mendix";
22
import { FileUploaderContainerProps, 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";
@@ -26,7 +27,7 @@ export class FileUploaderStore {
2627
_maxFileSizeMiB = 0;
2728
_maxFileSize = 0;
2829
_ds?: ListValue;
29-
_maxFilesPerUpload: number;
30+
_maxFilesPerUpload: DynamicValue<Big>;
3031

3132
errorMessage?: string = undefined;
3233

@@ -79,7 +80,10 @@ export class FileUploaderStore {
7980
files: observable,
8081
existingItemsLoaded: observable,
8182
errorMessage: observable,
82-
allowedFormatsDescription: computed
83+
allowedFormatsDescription: computed,
84+
maxFilesPerUpload: computed,
85+
_maxFilesPerUpload: observable,
86+
isFileUploadLimitReached: computed
8387
});
8488

8589
this.updateProps(props);
@@ -94,6 +98,9 @@ export class FileUploaderStore {
9498
this._ds = props.associatedImages;
9599
}
96100

101+
// Update max files properties
102+
this._maxFilesPerUpload = props.maxFilesPerUpload;
103+
97104
this.translations.updateProps(props);
98105
this.updateProcessor.processUpdate(this._ds);
99106
}
@@ -113,6 +120,29 @@ export class FileUploaderStore {
113120
.join(", ");
114121
}
115122

123+
get maxFilesPerUpload(): number {
124+
const expressionValue = this._maxFilesPerUpload.value;
125+
if (expressionValue) {
126+
return expressionValue.toNumber();
127+
}
128+
// Fallback to unlimited
129+
return 0;
130+
}
131+
132+
get isFileUploadLimitReached(): boolean {
133+
const activeFiles = this.files.filter(
134+
file =>
135+
file.fileStatus !== "missing" &&
136+
file.fileStatus !== "removedFile" &&
137+
file.fileStatus !== "validationError"
138+
);
139+
if (this.maxFilesPerUpload === 0) {
140+
return false;
141+
}
142+
143+
return activeFiles.length >= this.maxFilesPerUpload;
144+
}
145+
116146
setMessage(msg?: string): void {
117147
this.errorMessage = msg;
118148
}
@@ -128,7 +158,7 @@ export class FileUploaderStore {
128158

129159
if (fileRejections.length && fileRejections[0].errors[0].code === "too-many-files") {
130160
this.setMessage(
131-
this.translations.get("uploadFailureTooManyFilesMessage", this._maxFilesPerUpload.toString())
161+
this.translations.get("uploadFailureTooManyFilesMessage", this.maxFilesPerUpload.toString())
132162
);
133163
return;
134164
}
@@ -164,6 +194,12 @@ export class FileUploaderStore {
164194
for (const file of acceptedFiles) {
165195
const newFileStore = FileStore.newFile(file, this);
166196

197+
if (this.isFileUploadLimitReached) {
198+
newFileStore.markError(
199+
this.translations.get("uploadFailureTooManyFilesMessage", this.maxFilesPerUpload.toString())
200+
);
201+
}
202+
167203
this.files.unshift(newFileStore);
168204

169205
if (newFileStore.validate()) {

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;

0 commit comments

Comments
 (0)