From b1109f142f06dc29d17874e4e3d3459def6f239f Mon Sep 17 00:00:00 2001 From: gjulivan Date: Fri, 18 Jul 2025 09:23:22 +0200 Subject: [PATCH 1/4] feat(file-upload): improve max file upload --- .../file-uploader-web/CHANGELOG.md | 8 ++++ .../file-uploader-web/package.json | 2 +- .../src/FileUploader.editorConfig.ts | 6 +++ .../file-uploader-web/src/FileUploader.xml | 15 ++++++- .../src/components/Dropzone.tsx | 18 ++++++-- .../src/components/FileUploaderRoot.tsx | 3 +- .../file-uploader-web/src/package.xml | 2 +- .../src/stores/FileUploaderStore.ts | 42 +++++++++++++++++-- .../src/ui/FileUploader.scss | 7 ++++ .../typings/FileUploaderProps.d.ts | 7 ++++ 10 files changed, 98 insertions(+), 12 deletions(-) diff --git a/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md b/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md index facae6f684..3f995904d3 100644 --- a/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md +++ b/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- We fixed an issue where file uploader can still add more files when refreshed eventhough the number of maximum uploaded files has been reached. + +### Added + +- We added a configuration to set maximum number of uploaded files through expression. + ## [2.2.2] - 2025-07-01 ### Fixed diff --git a/packages/pluggableWidgets/file-uploader-web/package.json b/packages/pluggableWidgets/file-uploader-web/package.json index 076e822792..871ba6d3cd 100644 --- a/packages/pluggableWidgets/file-uploader-web/package.json +++ b/packages/pluggableWidgets/file-uploader-web/package.json @@ -1,7 +1,7 @@ { "name": "@mendix/file-uploader-web", "widgetName": "FileUploader", - "version": "2.2.2", + "version": "2.3.0", "description": "", "copyright": "© Mendix Technology BV 2025. All rights reserved.", "license": "Apache-2.0", diff --git a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts index 7275c6973a..341685303a 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts @@ -40,6 +40,12 @@ export function getProperties( hidePropertiesIn(properties, values, ["customButtons"]); } + if (values.maxFilePerUploadType === "expression") { + hidePropertiesIn(properties, values, ["maxFilesPerUpload"]); + } else { + hidePropertiesIn(properties, values, ["maxFilesPerUploadExpression"]); + } + return properties; } diff --git a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml index 93972898df..2386afebd9 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml +++ b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml @@ -80,9 +80,22 @@ + + Maximum number of files per upload type + + + Number + Expression + + + + Maximum number of files + Limit the number of files per upload. + + Maximum number of files - Limit the number of files per one upload. + Limit the number of files per upload. Maximum file size (MB) diff --git a/packages/pluggableWidgets/file-uploader-web/src/components/Dropzone.tsx b/packages/pluggableWidgets/file-uploader-web/src/components/Dropzone.tsx index ee0d24aa60..9e0ace8992 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/components/Dropzone.tsx +++ b/packages/pluggableWidgets/file-uploader-web/src/components/Dropzone.tsx @@ -12,15 +12,24 @@ interface DropzoneProps { maxSize: number; maxFilesPerUpload: number; acceptFileTypes: MimeCheckFormat; + disabled: boolean; } export const Dropzone = observer( - ({ warningMessage, onDrop, maxSize, maxFilesPerUpload, acceptFileTypes }: DropzoneProps): ReactElement => { + ({ + warningMessage, + onDrop, + maxSize, + maxFilesPerUpload, + acceptFileTypes, + disabled + }: DropzoneProps): ReactElement => { const { getRootProps, getInputProps, isDragAccept, isDragReject } = useDropzone({ onDrop, maxSize: maxSize || undefined, maxFiles: maxFilesPerUpload, - accept: acceptFileTypes + accept: acceptFileTypes, + disabled }); const translations = useTranslationsStore(); @@ -31,14 +40,15 @@ export const Dropzone = observer(
-

{msg}

+ {!disabled &&

{msg}

} - + {!disabled && }
{warningMessage &&
{warningMessage}
} diff --git a/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx b/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx index c177d38dd5..495261179d 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx +++ b/packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx @@ -29,7 +29,8 @@ export const FileUploaderRoot = observer((props: FileUploaderContainerProps): Re warningMessage={rootStore.errorMessage} maxSize={rootStore._maxFileSize} acceptFileTypes={prepareAcceptForDropzone(rootStore.acceptedFileTypes)} - maxFilesPerUpload={rootStore._maxFilesPerUpload} + maxFilesPerUpload={rootStore.maxFilesPerUpload ?? 0} + disabled={rootStore.isFileUploadLimitReached} /> )} diff --git a/packages/pluggableWidgets/file-uploader-web/src/package.xml b/packages/pluggableWidgets/file-uploader-web/src/package.xml index 5fc88722c2..6d3a7df305 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/package.xml +++ b/packages/pluggableWidgets/file-uploader-web/src/package.xml @@ -1,6 +1,6 @@ - + diff --git a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts index 8efe71b29e..0aa68b13e4 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts @@ -1,6 +1,7 @@ -import { ListValue, ObjectItem } from "mendix"; -import { FileUploaderContainerProps, UploadModeEnum } from "../../typings/FileUploaderProps"; +import { DynamicValue, ListValue, ObjectItem } from "mendix"; +import { FileUploaderContainerProps, MaxFilePerUploadTypeEnum, 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"; @@ -27,6 +28,8 @@ export class FileUploaderStore { _maxFileSize = 0; _ds?: ListValue; _maxFilesPerUpload: number; + _maxFilePerUploadType: MaxFilePerUploadTypeEnum; + _maxFilesPerUploadExpression: DynamicValue; errorMessage?: string = undefined; @@ -37,6 +40,8 @@ export class FileUploaderStore { this._maxFileSizeMiB = props.maxFileSize; this._maxFileSize = this._maxFileSizeMiB * 1024 * 1024; this._maxFilesPerUpload = props.maxFilesPerUpload; + this._maxFilePerUploadType = props.maxFilePerUploadType; + this._maxFilesPerUploadExpression = props.maxFilesPerUploadExpression; this._uploadMode = props.uploadMode; this.objectCreationHelper = new ObjectCreationHelper(this._widgetName, props.objectCreationTimeout); @@ -79,7 +84,9 @@ export class FileUploaderStore { files: observable, existingItemsLoaded: observable, errorMessage: observable, - allowedFormatsDescription: computed + allowedFormatsDescription: computed, + maxFilesPerUpload: computed, + isFileUploadLimitReached: computed }); this.updateProps(props); @@ -94,6 +101,11 @@ export class FileUploaderStore { this._ds = props.associatedImages; } + // Update max files properties + this._maxFilesPerUpload = props.maxFilesPerUpload; + this._maxFilePerUploadType = props.maxFilePerUploadType; + this._maxFilesPerUploadExpression = props.maxFilesPerUploadExpression; + this.translations.updateProps(props); this.updateProcessor.processUpdate(this._ds); } @@ -113,6 +125,28 @@ export class FileUploaderStore { .join(", "); } + get maxFilesPerUpload(): number | undefined { + if (this._maxFilePerUploadType === "expression") { + const expressionValue = this._maxFilesPerUploadExpression.value; + if (expressionValue && !isNaN(Number(expressionValue))) { + return Number(expressionValue); + } + // Fallback to default if expression is invalid + return undefined; + } + return this._maxFilesPerUpload; + } + + get isFileUploadLimitReached(): boolean { + const activeFiles = this.files.filter( + file => file.fileStatus !== "missing" && file.fileStatus !== "removedFile" + ); + if (!this.maxFilesPerUpload || this.maxFilesPerUpload === 0) { + return false; + } + return activeFiles.length >= this.maxFilesPerUpload; + } + setMessage(msg?: string): void { this.errorMessage = msg; } @@ -128,7 +162,7 @@ export class FileUploaderStore { if (fileRejections.length && fileRejections[0].errors[0].code === "too-many-files") { this.setMessage( - this.translations.get("uploadFailureTooManyFilesMessage", this._maxFilesPerUpload.toString()) + this.translations.get("uploadFailureTooManyFilesMessage", this.maxFilesPerUpload?.toString() ?? "") ); return; } diff --git a/packages/pluggableWidgets/file-uploader-web/src/ui/FileUploader.scss b/packages/pluggableWidgets/file-uploader-web/src/ui/FileUploader.scss index 7538e78309..4f9bf3c3cf 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/ui/FileUploader.scss +++ b/packages/pluggableWidgets/file-uploader-web/src/ui/FileUploader.scss @@ -74,6 +74,13 @@ Place your custom CSS here border: 1.5px solid var(--brand-primary, $file-brand-primary); background-color: var(--color-primary-lighter, $file-color-primary-lighter); } + &.disabled { + border: 1.5px dashed var(--border-color-default, $file-border-color-default); + background-color: var(--bg-color, $file-bg-color); + .file-icon { + opacity: 0.5; + } + } .file-icon { flex: 0 0 34px; diff --git a/packages/pluggableWidgets/file-uploader-web/typings/FileUploaderProps.d.ts b/packages/pluggableWidgets/file-uploader-web/typings/FileUploaderProps.d.ts index ff42ba5a5d..cbe573b835 100644 --- a/packages/pluggableWidgets/file-uploader-web/typings/FileUploaderProps.d.ts +++ b/packages/pluggableWidgets/file-uploader-web/typings/FileUploaderProps.d.ts @@ -5,6 +5,7 @@ */ import { CSSProperties } from "react"; import { ActionValue, DynamicValue, ListValue, ListActionValue, WebIcon } from "mendix"; +import { Big } from "big.js"; export type UploadModeEnum = "files" | "images"; @@ -20,6 +21,8 @@ export interface AllowedFileFormatsType { typeFormatDescription: DynamicValue; } +export type MaxFilePerUploadTypeEnum = "number" | "expression"; + export interface CustomButtonsType { buttonCaption: DynamicValue; buttonIcon: DynamicValue; @@ -58,6 +61,8 @@ export interface FileUploaderContainerProps { createFileAction?: ActionValue; createImageAction?: ActionValue; allowedFileFormats: AllowedFileFormatsType[]; + maxFilePerUploadType: MaxFilePerUploadTypeEnum; + maxFilesPerUploadExpression: DynamicValue; maxFilesPerUpload: number; maxFileSize: number; dropzoneIdleMessage: DynamicValue; @@ -97,6 +102,8 @@ export interface FileUploaderPreviewProps { createFileAction: {} | null; createImageAction: {} | null; allowedFileFormats: AllowedFileFormatsPreviewType[]; + maxFilePerUploadType: MaxFilePerUploadTypeEnum; + maxFilesPerUploadExpression: string; maxFilesPerUpload: number | null; maxFileSize: number | null; dropzoneIdleMessage: string; From 231953bdf2bf42d5b5a01463af956e3c5cf6fc42 Mon Sep 17 00:00:00 2001 From: gjulivan Date: Fri, 18 Jul 2025 10:07:25 +0200 Subject: [PATCH 2/4] fix: remove choices and go with expression --- .../file-uploader-web/CHANGELOG.md | 4 +-- .../src/FileUploader.editorConfig.ts | 13 +-------- .../file-uploader-web/src/FileUploader.xml | 14 +--------- .../src/stores/FileUploaderStore.ts | 27 +++++++------------ .../typings/FileUploaderProps.d.ts | 10 ++----- 5 files changed, 15 insertions(+), 53 deletions(-) diff --git a/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md b/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md index 3f995904d3..d04ec89b89 100644 --- a/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md +++ b/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md @@ -10,9 +10,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - We fixed an issue where file uploader can still add more files when refreshed eventhough the number of maximum uploaded files has been reached. -### Added +### Changed -- We added a configuration to set maximum number of uploaded files through expression. +- We change the max file configuration to set maximum number of uploaded files through expression. ## [2.2.2] - 2025-07-01 diff --git a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts index 341685303a..42e378cabb 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts @@ -40,12 +40,6 @@ export function getProperties( hidePropertiesIn(properties, values, ["customButtons"]); } - if (values.maxFilePerUploadType === "expression") { - hidePropertiesIn(properties, values, ["maxFilesPerUpload"]); - } else { - hidePropertiesIn(properties, values, ["maxFilesPerUploadExpression"]); - } - return properties; } @@ -95,12 +89,7 @@ export function check(values: FileUploaderPreviewProps): Problem[] { } } - if (!values.maxFilesPerUpload || values.maxFilesPerUpload < 1) { - errors.push({ - property: "maxFilesPerUpload", - message: "There must be at least one file per upload allowed." - }); - } + // Note: maxFilesPerUpload validation is handled at runtime since it's an expression if (values.enableCustomButtons) { // check that at max one actions is default diff --git a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml index 2386afebd9..f72de5e2c0 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml +++ b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml @@ -80,23 +80,11 @@ - - Maximum number of files per upload type - - - Number - Expression - - - + Maximum number of files Limit the number of files per upload. - - Maximum number of files - Limit the number of files per upload. - Maximum file size (MB) Reject files that are bigger than specified size. diff --git a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts index 0aa68b13e4..c706117f3b 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts @@ -1,5 +1,5 @@ import { DynamicValue, ListValue, ObjectItem } from "mendix"; -import { FileUploaderContainerProps, MaxFilePerUploadTypeEnum, UploadModeEnum } from "../../typings/FileUploaderProps"; +import { FileUploaderContainerProps, UploadModeEnum } from "../../typings/FileUploaderProps"; import { action, computed, makeObservable, observable } from "mobx"; import { Big } from "big.js"; import { getImageUploaderFormats, parseAllowedFormats } from "../utils/parseAllowedFormats"; @@ -27,9 +27,7 @@ export class FileUploaderStore { _maxFileSizeMiB = 0; _maxFileSize = 0; _ds?: ListValue; - _maxFilesPerUpload: number; - _maxFilePerUploadType: MaxFilePerUploadTypeEnum; - _maxFilesPerUploadExpression: DynamicValue; + _maxFilesPerUpload: DynamicValue; errorMessage?: string = undefined; @@ -40,8 +38,6 @@ export class FileUploaderStore { this._maxFileSizeMiB = props.maxFileSize; this._maxFileSize = this._maxFileSizeMiB * 1024 * 1024; this._maxFilesPerUpload = props.maxFilesPerUpload; - this._maxFilePerUploadType = props.maxFilePerUploadType; - this._maxFilesPerUploadExpression = props.maxFilesPerUploadExpression; this._uploadMode = props.uploadMode; this.objectCreationHelper = new ObjectCreationHelper(this._widgetName, props.objectCreationTimeout); @@ -103,8 +99,6 @@ export class FileUploaderStore { // Update max files properties this._maxFilesPerUpload = props.maxFilesPerUpload; - this._maxFilePerUploadType = props.maxFilePerUploadType; - this._maxFilesPerUploadExpression = props.maxFilesPerUploadExpression; this.translations.updateProps(props); this.updateProcessor.processUpdate(this._ds); @@ -125,23 +119,20 @@ export class FileUploaderStore { .join(", "); } - get maxFilesPerUpload(): number | undefined { - if (this._maxFilePerUploadType === "expression") { - const expressionValue = this._maxFilesPerUploadExpression.value; - if (expressionValue && !isNaN(Number(expressionValue))) { - return Number(expressionValue); - } - // Fallback to default if expression is invalid - return undefined; + get maxFilesPerUpload(): number { + const expressionValue = this._maxFilesPerUpload.value; + if (expressionValue && !isNaN(Number(expressionValue))) { + return Number(expressionValue); } - return this._maxFilesPerUpload; + // Fallback to unlimited + return 0; } get isFileUploadLimitReached(): boolean { const activeFiles = this.files.filter( file => file.fileStatus !== "missing" && file.fileStatus !== "removedFile" ); - if (!this.maxFilesPerUpload || this.maxFilesPerUpload === 0) { + if (this.maxFilesPerUpload === 0) { return false; } return activeFiles.length >= this.maxFilesPerUpload; diff --git a/packages/pluggableWidgets/file-uploader-web/typings/FileUploaderProps.d.ts b/packages/pluggableWidgets/file-uploader-web/typings/FileUploaderProps.d.ts index cbe573b835..c76a4c15a1 100644 --- a/packages/pluggableWidgets/file-uploader-web/typings/FileUploaderProps.d.ts +++ b/packages/pluggableWidgets/file-uploader-web/typings/FileUploaderProps.d.ts @@ -21,8 +21,6 @@ export interface AllowedFileFormatsType { typeFormatDescription: DynamicValue; } -export type MaxFilePerUploadTypeEnum = "number" | "expression"; - export interface CustomButtonsType { buttonCaption: DynamicValue; buttonIcon: DynamicValue; @@ -61,9 +59,7 @@ export interface FileUploaderContainerProps { createFileAction?: ActionValue; createImageAction?: ActionValue; allowedFileFormats: AllowedFileFormatsType[]; - maxFilePerUploadType: MaxFilePerUploadTypeEnum; - maxFilesPerUploadExpression: DynamicValue; - maxFilesPerUpload: number; + maxFilesPerUpload: DynamicValue; maxFileSize: number; dropzoneIdleMessage: DynamicValue; dropzoneAcceptedMessage: DynamicValue; @@ -102,9 +98,7 @@ export interface FileUploaderPreviewProps { createFileAction: {} | null; createImageAction: {} | null; allowedFileFormats: AllowedFileFormatsPreviewType[]; - maxFilePerUploadType: MaxFilePerUploadTypeEnum; - maxFilesPerUploadExpression: string; - maxFilesPerUpload: number | null; + maxFilesPerUpload: string; maxFileSize: number | null; dropzoneIdleMessage: string; dropzoneAcceptedMessage: string; From fa1aa80042a426671f0657c0db919214e1d8e9a6 Mon Sep 17 00:00:00 2001 From: gjulivan Date: Thu, 31 Jul 2025 00:24:01 +0200 Subject: [PATCH 3/4] chore: update code based on feedback --- .../file-uploader-web/src/FileUploader.editorConfig.ts | 2 -- .../file-uploader-web/src/stores/FileUploaderStore.ts | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts index 42e378cabb..b3d8270e40 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts @@ -89,8 +89,6 @@ export function check(values: FileUploaderPreviewProps): Problem[] { } } - // Note: maxFilesPerUpload validation is handled at runtime since it's an expression - if (values.enableCustomButtons) { // check that at max one actions is default const defaultIdx = new Set(); diff --git a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts index c706117f3b..04205ebb52 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts @@ -121,8 +121,8 @@ export class FileUploaderStore { get maxFilesPerUpload(): number { const expressionValue = this._maxFilesPerUpload.value; - if (expressionValue && !isNaN(Number(expressionValue))) { - return Number(expressionValue); + if (expressionValue) { + return expressionValue.toNumber(); } // Fallback to unlimited return 0; @@ -153,7 +153,7 @@ export class FileUploaderStore { if (fileRejections.length && fileRejections[0].errors[0].code === "too-many-files") { this.setMessage( - this.translations.get("uploadFailureTooManyFilesMessage", this.maxFilesPerUpload?.toString() ?? "") + this.translations.get("uploadFailureTooManyFilesMessage", this.maxFilesPerUpload.toString()) ); return; } From e210592f034ec3d33a7451db8e7cbf8f1ea96ea8 Mon Sep 17 00:00:00 2001 From: gjulivan Date: Fri, 1 Aug 2025 09:36:00 +0200 Subject: [PATCH 4/4] fix: max file per upload not working with dynamic value --- .../file-uploader-web/src/stores/FileUploaderStore.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts index 04205ebb52..92bb9bdde6 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts @@ -82,6 +82,7 @@ export class FileUploaderStore { errorMessage: observable, allowedFormatsDescription: computed, maxFilesPerUpload: computed, + _maxFilesPerUpload: observable, isFileUploadLimitReached: computed });