Skip to content

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

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

[WC-3361] FileUploader: enhance limits and feedback#2208
yordan-st wants to merge 10 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 + Feature (behavior change — non-breaking)

Description

When the file upload limit was reached, the dropzone silently turned grey with no explanation. Dropping more files than the limit rejected the entire batch. There was no proper upload queue — excess files were marked as errors and "retried" via a promotion mechanism, conflating queueing with error state.

This PR addresses:

  • Silent disabled dropzone → now shows "Maximum file count of X reached."
  • Drop overflow now splits on remaining capacity. Only excess files are rejected; the rest upload normally.
  • Replaced the error/retry approach with a proper upload queue. Files waiting for a concurrent slot show a "Waiting..." state, not an error state.
  • New "Maximum concurrent uploads" property (XML key: maxFilesPerBatch) controls how many files upload simultaneously. Excess files queue and drain automatically.
  • New "Upload queued" text property for the waiting state message.
  • New "File limit reached" text property for the limit-reached message.
  • Rejected files (over total limit) auto-promote oldest-first when capacity opens.
  • Upload errors free both a concurrent slot and a total-files slot, keeping the queue draining.
  • maxFilesPerUpload property is now optional — empty or 0 means unlimited.
  • File list reordered: successful uploads shown above rejected files.

No longer depends on WC-3363. The dismissValidationErrors method was removed as part of this PR — rejected files are a distinct state from validation errors and are not dismissable. WC-3363 can merge independently.

What should be covered while testing?

Setup: File Uploader widget, files mode. Start with "Maximum number of files" = 5, "Maximum concurrent uploads" = 2.

File limit feedback:

  1. Upload 1 file → dropzone stays active, no warning shown
  2. Upload until total = 5 → dropzone turns grey AND message appears: "Maximum file count of 5 reached."
  3. Remove 1 file while at limit → dropzone re-enables, message disappears

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

Concurrent upload queue:
7. Set concurrent uploads = 2, drop 5 files → exactly 2 show "Uploading..." with progress bar, remaining 3 show "Waiting..." without progress bar
8. Wait for 1 upload to complete → next queued file automatically starts uploading (now shows "Uploading...")
9. Leave concurrent uploads empty → all dropped files start uploading simultaneously

Total limit with auto-promotion:
10. Set total limit = 3, concurrent uploads = unlimited. Drop 5 files → 3 upload, 2 show as rejected with "Maximum file count of 3 reached."
11. Remove 1 uploaded file → oldest rejected file automatically starts uploading
12. Let an upload fail → that file shows error; oldest rejected file automatically starts uploading

List ordering:
13. Upload a mix of valid and invalid files → successful uploads appear above rejected files

Other:
14. Read-only mode → dropzone not rendered, no regression
15. Image upload mode → same queue and limit 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 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.

Comment thread packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts Outdated
yordan-st added 9 commits May 18, 2026 14:57
- 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 d06abf7 to 8fe5f51 Compare May 18, 2026 12:57
@github-actions
Copy link
Copy Markdown

AI Code Review

🔶 Changes requested — one or more medium-severity items must be addressed


What was reviewed

File Change
src/stores/FileUploaderStore.ts New maxFilesPerBatch, sortedFiles, warningMessage, retryLimitExceededFiles, MobX reaction for auto-retry
src/stores/FileStore.ts New errorType field; reset() action; markError now accepts error type
src/stores/__tests__/FileUploaderStore.spec.ts New test file — 253 lines covering new store behaviours
src/FileUploader.editorConfig.ts Import reorder only
src/FileUploader.editorPreview.tsx Import reorder only
src/FileUploader.xml maxFilesPerUpload made optional; new maxFilesPerBatch, uploadLimitReachedMessage, uploadBatchLimitExceededMessage
src/components/Dropzone.tsx maxFilesPerUpload prop removed (limit enforcement moved to store)
src/components/FileUploaderRoot.tsx Uses rootStore.sortedFiles and rootStore.warningMessage
src/utils/useRootStore.ts Adds dispose() cleanup on unmount
typings/FileUploaderProps.d.ts maxFilesPerUpload optional; maxFilesPerBatch, uploadLimitReachedMessage, uploadBatchLimitExceededMessage added
CHANGELOG.md Unreleased section updated

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

⚠️ CI check results could not be retrieved automatically — verify checks are green before merging.


Findings

🔶 Medium — maxFilesPerBatch not hidden in read-only mode in editorConfig

File: src/FileUploader.editorConfig.ts lines 22–26
Problem: When readOnlyMode is true, the editor config hides maxFilesPerUpload, maxFileSize, and objectCreationTimeout — all upload-time properties. The new maxFilesPerBatch property is upload-time too but is absent from this list, so Studio Pro will show it to users configuring a read-only widget, creating a confusing no-op setting.
Fix:

hidePropertiesIn(properties, values, [
    values.uploadMode === "files" ? "associatedImages" : "associatedFiles",
    "createImageAction",
    "createFileAction",
    "allowedFileFormats",
    "maxFilesPerUpload",
    "maxFilesPerBatch",   // ← add this
    "maxFileSize",
    "objectCreationTimeout"
]);

⚠️ Low — Tests push plain objects into store.files instead of using FileStore factories

File: src/stores/__tests__/FileUploaderStore.spec.ts lines 77–80, 96–99, 113–116, 132–138
Note: These tests cast raw objects ({ fileStatus: "existingFile" } as any) and push them into the MobX observable array. This bypasses the FileStore constructor and makeObservable call, so the pushed items are plain non-observable objects. It works for current assertions (count-based filtering), but breaks down for any future test that needs fileStatus changes on those objects to be reactive, and is fragile against FileStore structural refactors. Using the existing static factories is straightforward:

// instead of:
store.files.push({ fileStatus: "existingFile", _objectItem: obj("a") } as any);

// use:
const fileA = FileStore.existingFile(obj("a"), store);
store.files.push(fileA);

FileStore.existingFile calls fetchMxObject asynchronously — stub mx-data at the top of the test file if needed (as the DatasourceUpdateProcessor.spec.ts already does for the same module).


Positives

  • dispose() method added to FileUploaderStore and wired up in useRootStore via a cleanup useEffect — the MobX reaction no longer leaks on widget unmount.
  • New errorType discriminant on FileStore is properly registered as observable in makeObservable and correctly updated in all three creation paths (newFileWithError, markError, and the constructor).
  • Moving limit enforcement from react-dropzone's maxFiles option into the store is the right call — it eliminates the silent all-or-nothing rejection and gives fine-grained control over which files pass vs. error.
  • retryLimitExceededFiles is registered as a MobX action, so all state mutations inside it are batched and the retry reaction won't cascade.
  • CHANGELOG entry is thorough and covers fixed, added, and changed categories with user-facing language.
  • New XML properties use lowerCamelCase keys (maxFilesPerBatch, uploadLimitReachedMessage, uploadBatchLimitExceededMessage) consistent with the repo convention, and TypeScript typings are kept in sync.

- Add "queued" and "rejected" statuses; remove "new", "removedAfterError"
- Remove errorType field and dismissValidationErrors
- maxFilesPerBatch (XML) → maxConcurrentUploads (TS)
- maxFilesPerUpload (XML) → maxTotalFiles (TS)
- processDrop is pure classifier; two reactions drain the queue
- Add uploadQueuedMessage XML property
- Remove uploadBatchLimitExceededMessage XML property
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