Skip to content

[WC-3361] FileUploader: enhance limits and feedback#2208

Open
yordan-st wants to merge 9 commits into
mainfrom
fix/WC-3361_fileuploader-limit-feedback
Open

[WC-3361] FileUploader: enhance limits and feedback#2208
yordan-st wants to merge 9 commits into
mainfrom
fix/WC-3361_fileuploader-limit-feedback

Conversation

@yordan-st
Copy link
Copy Markdown
Contributor

@yordan-st yordan-st commented May 11, 2026

Pull request type

Bug fix (non-breaking change which fixes an issue)


Description

When the file upload limit was reached, the dropzone silently turned grey with no explanation to the user. Dropping more files than the limit allowed rejected the entire batch, and files stuck in error state had no way to recover.

IMPORTANT NOTE: we need to merge [WC-3363 Fileuploader: dismiss action for validation errors](#2198 (comment)) first

This PR addresses everything below:

  • Silent disabled dropzone -> now shows "Maximum file count of X reached."
  • Drop overflow now splits on remaining capacity. Only the excess are shown as errors
  • Files rejected due to total or batch limit auto-retry when capacity is freed (file removed or error dismissed)
  • Wrong XML description on maxFilesPerUpload fixed
  • No way to set unlimited -> required="false" added; 0 and empty both mean unlimited (default)
  • New "Maximum files per upload batch" property to cap server commits per drop event
  • New "File limit reached" and "Batch limit exceeded" text properties for message customization
  • File list reordered: successful uploads shown above rejected files

What should be covered while testing?

Setup: File Uploader, "Maximum number of files" = 5, "Maximum files per upload batch" = 2, files mode.

File limit feedback:

  1. Upload 1 file → dropzone stays active, no warning shown
  2. Upload until total = limit → dropzone turns grey AND message appears: "Maximum file count of 5 reached."
  3. Remove 1 file while at limit → dropzone re-enables, message disappears
  4. Refresh page with existing files at limit → dropzone immediately grey with message on load, no user action needed

Unlimited behavior:
5. Set total limit = 0 → dropzone never disables regardless of file count
6. Leave total limit empty → same as 0, unlimited

Drop total limit:
7. Set total limit = 3, no batch limit. Drop 5 files → first 3 upload, last 2 appear as errors with "Maximum file count of 3 reached."
8. Remove 1 uploaded file → one of the errored files automatically starts uploading

Batch limit:
9. Set batch limit = 2, drop 5 files → first 2 upload, remaining 3 appear as errors: "File not uploaded. Batch limit of 2 files per drop was reached."
10. Remove 1 uploaded file → one of the batch-errored files automatically starts uploading (respects batch limit, max 2 retry at once)
11. Leave batch limit empty → all dropped files upload regardless of count per drop

List ordering:
12. Upload a mix of valid and invalid files → successful uploads appear above rejected files in the list

Other:
13. Read-only mode → dropzone not rendered, no regression
14. Image upload mode → same limit-reached and retry behavior applies

@yordan-st yordan-st force-pushed the fix/WC-3361_fileuploader-limit-feedback branch from 430b80b to 4d8b53f Compare May 11, 2026 15:02
@yordan-st yordan-st marked this pull request as ready for review May 12, 2026 13:00
@yordan-st yordan-st requested a review from a team as a code owner May 12, 2026 13:00
yordan-st added 7 commits May 12, 2026 15:01
- Show "Maximum file count of X reached" message when dropzone is disabled
- Make maxFilesPerUpload optional (empty/0 = unlimited)
- Add maxFilesPerBatch property to cap server commits per drop event
- Split drops by remaining capacity — excess files shown as errors, not silently rejected
- Auto-retry limit/batch-exceeded files when capacity is freed
- Batch/limit-exceeded files survive dismissValidationErrors and re-queue correctly
- Retry order matches visual list order (newest first)
- Reorder file list: accepted files above rejected files
@yordan-st yordan-st force-pushed the fix/WC-3361_fileuploader-limit-feedback branch from a444b83 to 7d054a3 Compare May 12, 2026 13:01
@github-actions

This comment was marked as outdated.

@github-actions

This comment was marked as outdated.

@yordan-st yordan-st force-pushed the fix/WC-3361_fileuploader-limit-feedback branch from 9fe41dd to 09489e2 Compare May 12, 2026 13:45
@github-actions

This comment was marked as outdated.

@github-actions
Copy link
Copy Markdown

AI Code Review

⚠️ Approved with suggestions — low-severity items only, safe to merge


What was reviewed

File Change
packages/pluggableWidgets/file-uploader-web/CHANGELOG.md New Unreleased entries for all changes
packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml Added maxFilesPerBatch, uploadLimitReachedMessage, uploadBatchLimitExceededMessage; made maxFilesPerUpload optional
packages/pluggableWidgets/file-uploader-web/typings/FileUploaderProps.d.ts New optional props in Container and Preview interfaces
packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts Core logic: batch/capacity split, retryLimitExceededFiles, sortedFiles, warningMessage, MobX reaction for auto-retry, dispose()
packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts errorType field, reset() method, updated markError signature
packages/pluggableWidgets/file-uploader-web/src/stores/TranslationsStore.ts Import reorder only
packages/pluggableWidgets/file-uploader-web/src/components/Dropzone.tsx Removed maxFilesPerUpload prop (limit enforcement moved to store)
packages/pluggableWidgets/file-uploader-web/src/components/FileUploaderRoot.tsx Uses warningMessage and sortedFiles
packages/pluggableWidgets/file-uploader-web/src/utils/useRootStore.ts Adds useEffect cleanup calling dispose()
packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts New test file covering warningMessage, isFileUploadLimitReached, processDrop, retryLimitExceededFiles
Other *.tsx / *.ts files Import-order reordering only (prettier/eslint)

Skipped (out of scope): dist/, pnpm-lock.yaml


Findings

⚠️ Low — Missing store.dispose() in test teardown leaks MobX reaction

File: packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts (whole file)
Problem: buildStore() sets up a MobX reaction that is never cleaned up in tests. Without an afterEach calling store.dispose(), the reaction keeps running across tests and can fire unexpectedly, causing test pollution (especially the splice-triggered tests that rely on counting reaction invocations).
Fix:

describe("FileUploaderStore.warningMessage", () => {
    let store: FileUploaderStore;
    afterEach(() => store.dispose());
    // ... pass store ref from each test
});

Or add a shared afterEach at the top level:

const stores: FileUploaderStore[] = [];
afterEach(() => { stores.forEach(s => s.dispose()); stores.length = 0; });
// in buildStore: const s = new FileUploaderStore(...); stores.push(s); return s;

⚠️ Low — Manual as any stub objects instead of FileStore instances in tests

File: packages/pluggableWidgets/file-uploader-web/src/stores/__tests__/FileUploaderStore.spec.ts lines 78, 96, 114, 125, 134, 153, 167, 180
Problem: Tests push plain { fileStatus: "existingFile" } as any objects directly into store.files. These bypass MobX observability (makeObservable is never called on them), so changes to fileStatus won't trigger computed recalculations in isFileUploadLimitReached/warningMessage. Tests like the splice-based retry tests succeed only because retryLimitExceededFiles is called explicitly via the reaction, not because the computed value reacted to a stub.
Fix: Use FileStore.existingFile(obj("a"), store) (the factory already exists) for active-file placeholders, and FileStore.newFileWithError(file, msg, store, "limitExceeded") for waiting files, so MobX observability is exercised end-to-end.


⚠️ Low — retryLimitExceededFiles uses Number.MAX_SAFE_INTEGER as unlimited sentinel

File: packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts line 203
Problem: Number.MAX_SAFE_INTEGER (~9 × 10¹⁵) is passed into Math.min(capacitySlots, maxFilesPerBatch). While harmless in practice (the waiting.length guard caps the loop), it's an unusual sentinel. A simple Infinity communicates "unlimited" more clearly and is idiomatic for this pattern.
Fix:

const capacitySlots =
    this.maxFilesPerUpload > 0 ? Math.max(0, this.maxFilesPerUpload - activeCount) : Infinity;
const slots = this.maxFilesPerBatch > 0 ? Math.min(capacitySlots, this.maxFilesPerBatch) : capacitySlots;

⚠️ Low — uploadFailureTooManyFilesMessage XML property is now unreferenced in source

File: packages/pluggableWidgets/file-uploader-web/src/FileUploader.xml line 163 (unchanged)
Problem: The old uploadFailureTooManyFilesMessage textTemplate is still in the XML and TypeScript typings but is never read by any store or component after this PR (only referenced in the test's buildProps helper). It appears to be dead UI surface. Leaving it in exposes an unused configuration field to Mendix app developers.
Fix: Determine if this property should be deprecated or removed. If the uploadLimitReachedMessage fully replaces it, add a Studio Pro deprecation note and plan removal in the next major version. At minimum, add a comment in the XML explaining the relationship.


Positives

  • dispose() is correctly wired up with a useEffect cleanup in useRootStore.ts — the MobX reaction will not leak in production.
  • Capacity split logic in processDrop cleanly separates batch-excess from capacity-excess in a single pass, avoiding multiple scans of the file list.
  • sortedFiles computed getter sorts in-place on a copy ([...this.files]), correctly avoiding mutation of the observable array.
  • CHANGELOG entries are thorough — all three changed surfaces (Fixed, Added, Changed) are documented under [Unreleased].
  • XML property keys follow lowerCamelCase convention and TypeScript typings are kept in sync.

}
}
);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks really cumbersome to have essentially upload-throttling mechanism implemented like this. If we want upload to happen automatically in batches, we should make a proper uploading logic that keeps files in the queue and uploads max X files at a time and files should be in a queueing state, not in the error/warning state then.

@@ -116,11 +140,18 @@ export class FileUploaderStore {
}

get maxFilesPerUpload(): number {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is still called PerUpload, this is confusing, rename it to better match the purpose

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THe XML we can't rename unfortunately, but this we def can

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants