Skip to content

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

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

[WC-3361] FileUploader: enhance limits and feedback#2208
yordan-st wants to merge 13 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) now show a retry button. The button is enabled when capacity is available and greyed out when the limit is still full. Files do NOT auto-promote — the user must click retry explicitly (confirmed with PM).
  • Upload errors free a concurrent slot, allowing queued files to start uploading automatically.
  • 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:
4. Set total limit = 0 → dropzone never disables regardless of file count
5. Leave total limit empty → same as 0, unlimited

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

Total limit with manual retry:
9. Set total limit = 3, concurrent uploads = unlimited. Drop 5 files → 3 upload, 2 show as rejected with "Maximum file count of 3 reached." and a retry button
10. While at limit (3 active files): retry button on rejected file is greyed out (disabled)
11. Remove 1 uploaded file → retry button on rejected file becomes enabled
12. Click retry → file starts uploading; remaining rejected file still shows greyed retry button
13. Let an upload fail → that file shows upload error; retry buttons on rejected files remain unchanged (no auto-promotion)

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

Other:
15. Read-only mode → dropzone not rendered, no regression
16. 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

This comment was marked as outdated.

Comment thread packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts Outdated
@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

This comment was marked as outdated.

Comment thread packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts Outdated
@yordan-st yordan-st force-pushed the fix/WC-3361_fileuploader-limit-feedback branch from 3d36ddc to 160f8a7 Compare May 20, 2026 13:46
@github-actions

This comment was marked as outdated.

@yordan-st yordan-st force-pushed the fix/WC-3361_fileuploader-limit-feedback branch from 160f8a7 to cd1724d Compare May 22, 2026 12:11
Comment thread packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts Outdated
@yordan-st yordan-st force-pushed the fix/WC-3361_fileuploader-limit-feedback branch 3 times, most recently from f95539e to 9f4390b Compare May 26, 2026 12:39
@yordan-st yordan-st closed this May 26, 2026
@yordan-st yordan-st reopened this May 26, 2026
Comment thread packages/pluggableWidgets/file-uploader-web/src/components/FileEntry.tsx Outdated
Comment thread packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts Outdated
Comment thread packages/pluggableWidgets/file-uploader-web/src/components/FileEntry.tsx Outdated
Comment thread packages/pluggableWidgets/file-uploader-web/src/components/ActionButton.tsx Outdated
@yordan-st yordan-st force-pushed the fix/WC-3361_fileuploader-limit-feedback branch from 9f4390b to cfb076b Compare May 26, 2026 13:35
yordan-st added 11 commits May 27, 2026 13:26
- 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
- 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
@yordan-st yordan-st force-pushed the fix/WC-3361_fileuploader-limit-feedback branch from 3ecb2d5 to 7b0c130 Compare May 27, 2026 11:26
Comment thread packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts Outdated
Comment thread packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts Outdated
Comment thread packages/pluggableWidgets/file-uploader-web/src/stores/FileUploaderStore.ts Outdated
- merge reactions into one
- make translations private
- move rejected message rendering into UploadInfo
@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 Queue drain reaction, sortedFiles, promoteQueuedFiles, processDrop refactor
src/stores/FileStore.ts retry(), setQueued(), canRetry, newRejectedFile, private field encapsulation
src/stores/TranslationsStore.ts Import reorder only
src/stores/__tests__/FileUploaderStore.spec.ts New 973-line test suite
src/components/RetryButton.tsx New retry button component
src/components/ActionsBar.tsx Observer wrap, rejected-state routing to RetryButton
src/components/Dropzone.tsx Removed maxFilesPerUpload prop
src/components/FileEntry.tsx Added maxTotalFiles prop pass-through
src/components/FileUploaderRoot.tsx sortedFiles, warning message logic, dispose hook
src/components/UploadInfo.tsx Added rejected/queued status cases
src/ui/FileUploader.scss Retry button styles
src/utils/useRootStore.ts dispose() cleanup effect
src/assets/retry-icon.svg New SVG asset
FileUploader.xml New properties: maxFilesPerBatch, uploadQueuedMessage, uploadLimitReachedMessage, retryButtonTextMessage
typings/FileUploaderProps.d.ts New props, maxFilesPerUpload made optional
FileUploader.editorConfig.ts maxFilesPerBatch hidden in images mode
CHANGELOG.md Unreleased section

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

⚠️ CI checks could not be fetched (permission denied on gh pr checks). Please verify CI is green before merging.


Findings

🔶 Medium — sortedFiles does not sort rejected files to the end

File: src/stores/FileUploaderStore.ts line 190–196
Problem: The sortedFiles getter only pushes validationError entries to the bottom. rejected files are not included in the sort condition. The CHANGELOG explicitly states: "Files in the list are now ordered with successful uploads above rejected files." — but this is not guaranteed by the sort.

Concrete failure case:

  1. maxTotalFiles = 2, drop 2 files → both upload, files = [done_b, done_a]
  2. Drop 1 more file → capacityExcess = [file3], files.unshift(rejected_file3)files = [rejected_file3, done_b, done_a]
  3. sortedFiles leaves rejected_file3 at index 0 — the rejected file appears above the successful ones.

Fix:

get sortedFiles(): FileStore[] {
    return [...this.files].sort((a, b) => {
        const isErrorA = (a.fileStatus === "validationError" || a.fileStatus === "rejected") ? 1 : 0;
        const isErrorB = (b.fileStatus === "validationError" || b.fileStatus === "rejected") ? 1 : 0;
        return isErrorA - isErrorB;
    });
}

Also add a test to FileUploaderStore.spec.ts:

test("rejected files from a second drop sort to the end", () => {
    const store = buildStore({ maxFilesPerUpload: dynamic(new Big(1)) });
    store.objectCreationHelper.request = jest.fn().mockReturnValue(new Promise(() => {}));

    store.processDrop([makeFile("a.txt")], []);                // 1 uploading
    store.processDrop([makeFile("b.txt")], []);                // b → rejected (over cap)

    const sorted = store.sortedFiles;
    expect(sorted[sorted.length - 1].fileStatus).toBe("rejected");
});

⚠️ Low — Icon-only RetryButton uses title instead of aria-label

File: src/components/RetryButton.tsx line 21–29
Note: The button has no visible text and the inner icon span is aria-hidden. The accessible name is derived solely from the title attribute, which is not reliably announced by all screen readers and is completely inaccessible on touch devices. Per the repo's accessibility guidelines, icon-only buttons require aria-label.

<button
    className="retry-button"
    disabled={!store.canRetry}
    onClick={onClick}
    aria-label={translations.get("retryButtonTextMessage")}  // ← add this
    title={translations.get("retryButtonTextMessage")}       // keep for tooltip
>

⚠️ Low — Manual mock objects in promoteQueuedFiles tests instead of builders

File: src/stores/__tests__/FileUploaderStore.spec.ts lines 1631–1682
Note: The promoteQueuedFiles describe block pushes plain objects cast with as any directly into store.files:

const queued1 = { fileStatus: "queued", upload: jest.fn() } as any;
store.files.push(queued3, queued2, queued1);

The testing guidelines require builders (FileStore.newFile, FileStore.newRejectedFile) instead of manual mocks, since manual mocks miss observable state and don't exercise real FileStore lifecycle. Using real FileStore instances with objectCreationHelper.request mocked is preferred and consistent with the rest of this spec file.


Positives

  • The MobX reaction with comparer.structural for the queue drain is elegant — no manual wiring needed, and it fires on either a freed concurrent slot or new queued files.
  • makeObservable<this, "_mxObject" | "_thumbnailUrl">() correctly lists private observables in the type parameter, avoiding any implicit observable leakage.
  • dispose() is properly wired through useRootStore's useEffect cleanup — no reaction leak on unmount.
  • processDrop cleanly separates capacity files from overflow files rather than the previous approach of marking valid files as errors and using retry to promote them — the concept is now correct and the state machine is cleaner.
  • The test suite (973 lines) covers a wide range of scenarios: queue drain reactivity, canRetry flipping when slots free, dispose halting reactions, and the separation between queued auto-drain and rejected manual-retry. Especially valuable are the async tests using await Promise.resolve() to flush microtasks.
  • XML ↔ TypeScript alignment is complete: all four new XML properties (maxFilesPerBatch, uploadQueuedMessage, uploadLimitReachedMessage, retryButtonTextMessage) are present in both FileUploader.xml and FileUploaderProps.d.ts with matching names and types.

Comment on lines +86 to 88
get maxTotalFiles(): number {
return this._rootStore.maxTotalFiles;
}
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.

File store should not be responsible for passing parameters it doesn't care about to view components. View components should find their own way.

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