diff --git a/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md b/packages/pluggableWidgets/file-uploader-web/CHANGELOG.md index facae6f684..d04ec89b89 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. + +### Changed + +- We change the max file 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..b3d8270e40 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.editorConfig.ts @@ -89,13 +89,6 @@ 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." - }); - } - if (values.enableCustomButtons) { // check that at max one actions is default const defaultIdx = new Set(); diff --git a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml index 93972898df..f72de5e2c0 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml +++ b/packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml @@ -80,9 +80,10 @@ - + 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/FileStore.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts index ff2c02110f..112de33ae8 100644 --- a/packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts +++ b/packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts @@ -69,6 +69,11 @@ export class FileStore { this._objectItem = undefined; } + markError(errorMessage: string): void { + this.fileStatus = "validationError"; + this.errorDescription = errorMessage; + } + canExecute(listAction: ListActionValue): boolean { if (!this._objectItem) { return false; diff --git a/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts b/packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts index 8efe71b29e..9a34132a93 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 { DynamicValue, ListValue, 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"; @@ -26,7 +27,7 @@ export class FileUploaderStore { _maxFileSizeMiB = 0; _maxFileSize = 0; _ds?: ListValue; - _maxFilesPerUpload: number; + _maxFilesPerUpload: DynamicValue; errorMessage?: string = undefined; @@ -79,7 +80,10 @@ export class FileUploaderStore { files: observable, existingItemsLoaded: observable, errorMessage: observable, - allowedFormatsDescription: computed + allowedFormatsDescription: computed, + maxFilesPerUpload: computed, + _maxFilesPerUpload: observable, + isFileUploadLimitReached: computed }); this.updateProps(props); @@ -94,6 +98,9 @@ export class FileUploaderStore { this._ds = props.associatedImages; } + // Update max files properties + this._maxFilesPerUpload = props.maxFilesPerUpload; + this.translations.updateProps(props); this.updateProcessor.processUpdate(this._ds); } @@ -113,6 +120,29 @@ export class FileUploaderStore { .join(", "); } + get maxFilesPerUpload(): number { + const expressionValue = this._maxFilesPerUpload.value; + if (expressionValue) { + return expressionValue.toNumber(); + } + // Fallback to unlimited + return 0; + } + + get isFileUploadLimitReached(): boolean { + const activeFiles = this.files.filter( + file => + file.fileStatus !== "missing" && + file.fileStatus !== "removedFile" && + file.fileStatus !== "validationError" + ); + if (this.maxFilesPerUpload === 0) { + return false; + } + + return activeFiles.length >= this.maxFilesPerUpload; + } + setMessage(msg?: string): void { this.errorMessage = msg; } @@ -128,7 +158,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; } @@ -164,6 +194,12 @@ export class FileUploaderStore { for (const file of acceptedFiles) { const newFileStore = FileStore.newFile(file, this); + if (this.isFileUploadLimitReached) { + newFileStore.markError( + this.translations.get("uploadFailureTooManyFilesMessage", this.maxFilesPerUpload.toString()) + ); + } + this.files.unshift(newFileStore); if (newFileStore.validate()) { 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..c76a4c15a1 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"; @@ -58,7 +59,7 @@ export interface FileUploaderContainerProps { createFileAction?: ActionValue; createImageAction?: ActionValue; allowedFileFormats: AllowedFileFormatsType[]; - maxFilesPerUpload: number; + maxFilesPerUpload: DynamicValue; maxFileSize: number; dropzoneIdleMessage: DynamicValue; dropzoneAcceptedMessage: DynamicValue; @@ -97,7 +98,7 @@ export interface FileUploaderPreviewProps { createFileAction: {} | null; createImageAction: {} | null; allowedFileFormats: AllowedFileFormatsPreviewType[]; - maxFilesPerUpload: number | null; + maxFilesPerUpload: string; maxFileSize: number | null; dropzoneIdleMessage: string; dropzoneAcceptedMessage: string;