Skip to content

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

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

[WC-3361] FileUploader: enhance limits and feedback#2208
yordan-st wants to merge 14 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 2 times, most recently 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
@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 thread packages/pluggableWidgets/file-uploader-web/src/stores/FileStore.ts 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
src/FileUploader.tsx Wraps tree in new RootStoreProvider
src/FileUploader.xml Adds maxFilesPerBatch, uploadQueuedMessage, uploadLimitReachedMessage, retryButtonTextMessage; makes maxFilesPerUpload optional
src/FileUploader.editorConfig.ts Hides maxFilesPerBatch in single-file mode
src/FileUploader.editorPreview.tsx Import reorder only
src/components/ActionsBar.tsx Wraps with observer; renders RetryButton for rejected files
src/components/Dropzone.tsx Removes maxFiles prop (limit enforcement moved to store)
src/components/FileEntry.tsx Removes removedAfterError CSS mapping
src/components/FileUploaderRoot.tsx Uses useRootStore() via context; switches to sortedFiles
src/components/RetryButton.tsx New component — icon button for retry
src/components/UploadInfo.tsx Adds rejected and queued cases; uses useRootStore()
src/stores/FileStore.ts Replaces new/removedAfterError statuses with queued/rejected; adds canRetry, retry(), setQueued()
src/stores/FileUploaderStore.ts Adds concurrent queue logic, MobX reaction, dispose(), sortedFiles computed
src/stores/TranslationsStore.ts Import reorder only
src/utils/useRootStore.ts (deleted) / useRootStore.tsx (new) Lifts store into React context with RootStoreProvider
src/utils/useTranslationsStore.tsx Import reorder only
typings/FileUploaderProps.d.ts New props aligned with XML changes
src/assets/retry-icon.svg New SVG icon
src/ui/FileUploader.scss Adds .retry-button and .retry-icon styles
src/stores/__tests__/FileUploaderStore.spec.ts 953-line test suite — comprehensive coverage of new behaviour
src/utils/__tests__/DatasourceUpdateProcessor.spec.ts Import reorder only
src/utils/__tests__/parseAllowedFormats.spec.ts Import reorder only
CHANGELOG.md Well-structured entry covering all fixed/added/changed items

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


Findings

⚠️ Low — UploadInfo not wrapped with observer but reads MobX state via useRootStore

File: src/components/UploadInfo.tsx line 11–13
Note: UploadInfo calls useRootStore() which returns a MobX store, and it reads rootStore.maxTotalFiles inside the "rejected" case. The component itself is not wrapped with observer, so it won't reactively re-render if maxTotalFiles changes (e.g. a Studio Pro expression update at runtime). In practice the value is displayed in a status label that doesn't need to update mid-session, but it breaks the MobX reactivity contract and could cause stale messages if the limit changes while files are shown.

Fix: Wrap with observer:

export const UploadInfo = observer(function UploadInfo({ status, error }: UploadInfoProps): ReactElement {

⚠️ Low — sortedFiles only demotes validationError, leaving rejected unsorted relative to other statuses

File: src/stores/FileUploaderStore.ts lines 190–196
Note: The PR description says "successful uploads shown above rejected files", but the sort function only pushes validationError entries to the end; rejected files retain their insertion order relative to done/uploading/queued entries. The sort doesn't distinguish rejected from normal in-flight files. This may be intentional (rejected appear inline with insertion order), but it contradicts the stated goal. If the intent is done > uploading/queued > rejected > validationError, the sort key should encode rejected as well.

Fix (if the intent matches the PR description):

get sortedFiles(): FileStore[] {
    const priority: Partial<Record<FileStatus, number>> = { rejected: 1, validationError: 2 };
    return [...this.files].sort((a, b) =>
        (priority[a.fileStatus] ?? 0) - (priority[b.fileStatus] ?? 0)
    );
}

⚠️ Low — retry-button and retry-icon CSS classes lack the widget name prefix

File: src/ui/FileUploader.scss lines 251–283, src/components/RetryButton.tsx line 23–29
Note: Per the frontend guidelines, all custom classes must be prefixed with the widget name (e.g. widget-file-uploader-retry-button). The existing code in this file already uses entry-details-actions, file-entry, etc. without the full prefix inside the .widget-file-uploader scope — this appears to be an established pattern in this widget. However, retry-button and retry-icon are generic enough that they could conflict with third-party styles applied at the same nesting level. Consider aligning with the existing naming pattern or adding the widget prefix.


⚠️ Low — RetryButton aria-label missing for icon-only button

File: src/components/RetryButton.tsx lines 22–30
Note: The button renders only an icon <span> with aria-hidden. The title attribute is set but title is not reliably announced by all screen readers as a button label. An aria-label is required for assistive technologies to identify the button's purpose.

Fix:

<button
    className="retry-button"
    disabled={!store.canRetry}
    onClick={onClick}
    aria-label={translations.get("retryButtonTextMessage")}
    title={translations.get("retryButtonTextMessage")}
>
    <span className="retry-icon" aria-hidden />
</button>

⚠️ Low — dispose() is never called in RootStoreProvider

File: src/utils/useRootStore.tsx lines 22–26
Note: useInitRootStore already calls rootStore.dispose() in a cleanup useEffect, so this is correctly wired — no issue here. However, dispose() in FileUploaderStore only cleans up _disposePromoteReaction and does not call objectCreationHelper cleanup or updateProcessor cleanup. This is fine if those have no teardown, but worth confirming that there are no pending async operations (e.g. an in-flight objectCreationHelper.request()) that could resolve after unmount and call runInAction on a disposed store. The current runInAction calls in FileStore.upload() have no guard for this case.

Suggestion: This is low risk in practice (Mendix widgets rarely unmount mid-upload), but document the assumption or add an active guard in FileStore.upload() if upload-during-unmount is a realistic scenario.


Positives

  • Excellent test coverage (953 lines): every state transition, edge case, and MobX reactivity path is exercised with specific assertions — no snapshot abuse, no manual mock objects.
  • Clean separation between "rejected by capacity" (rejected status) and "rejected by format/size" (validationError) statuses makes the state machine easier to reason about.
  • comparer.structural on the MobX reaction correctly prevents redundant promoteQueuedFiles calls when counts haven't actually changed.
  • dispose() wired to useEffect cleanup in RootStoreProvider — the MobX reaction won't leak after widget unmount.
  • Making _rootStore and other internals private while exposing them through typed getters is a nice encapsulation improvement over the previous underscore convention.
  • FileStore.newFile / newRejectedFile / newFileWithValidationError factory naming is clear and aligns the factory name with the status it creates.

yordan-st added 14 commits May 28, 2026 15:17
- 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
- merge reactions into one
- make translations private
- move rejected message rendering into UploadInfo
@yordan-st yordan-st force-pushed the fix/WC-3361_fileuploader-limit-feedback branch from 0eacc78 to fa43edf Compare May 28, 2026 13:17
@github-actions
Copy link
Copy Markdown

AI Code Review

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


What was reviewed

File Change
src/stores/FileUploaderStore.ts New queue/slot logic, promoteQueuedFiles, sortedFiles, dispose, reaction
src/stores/FileStore.ts New rejected status, canRetry, retry, setQueued, encapsulation cleanup
src/stores/TranslationsStore.ts Import sort only
src/stores/__tests__/FileUploaderStore.spec.ts New comprehensive test suite (953 lines)
src/components/RetryButton.tsx New retry button component
src/components/ActionsBar.tsx Wrapped in observer; routes rejected files to RetryButton
src/components/Dropzone.tsx Removed maxFilesPerUpload prop, removed maxFiles from useDropzone
src/components/FileEntry.tsx Removed removedAfterError class trigger
src/components/FileUploaderRoot.tsx Switched to context-based useRootStore(), added limit-reached warning
src/components/UploadInfo.tsx Added rejected and queued case rendering; consumes useRootStore
src/utils/useRootStore.tsx Converted from bare hook to Context provider; added dispose cleanup
src/utils/useRootStore.ts Deleted (replaced by .tsx)
src/utils/useTranslationsStore.tsx Import sort only
src/utils/__tests__/DatasourceUpdateProcessor.spec.ts Import sort only
src/utils/__tests__/parseAllowedFormats.spec.ts Import sort only
src/utils/mx-data.ts Import sort only
src/utils/parseAllowedFormats.ts Import sort only
src/utils/prepareAcceptForDropzone.ts Import sort only
src/assets/retry-icon.svg New SVG asset
src/ui/FileUploader.scss New .retry-button / .retry-icon styles
src/FileUploader.tsx Wraps root in RootStoreProvider
src/FileUploader.xml New maxFilesPerBatch, uploadQueuedMessage, uploadLimitReachedMessage, retryButtonTextMessage properties
src/FileUploader.editorConfig.ts Hides maxFilesPerBatch in images mode
src/FileUploader.editorPreview.tsx Import sort only
typings/FileUploaderProps.d.ts New optional props aligned to XML changes
CHANGELOG.md Entry added under [Unreleased]

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

CI checks status: could not retrieve (workflow was not available from gh pr checks in this environment).


Findings

⚠️ Low — UploadInfo takes a hard dependency on RootStoreContext

File: src/components/UploadInfo.tsx lines 3, 13, 24
Note: UploadInfo is a pure display component that receives status and error as props. The addition of useRootStore() to read maxTotalFiles for the rejected case couples this component to the store tree. If UploadInfo is ever rendered outside RootStoreProvider (e.g. in an isolated unit test or a Storybook story) it will throw.

A cleaner approach is to pass maxTotalFiles as a prop at the FileEntry call site, keeping UploadInfo context-free:

// In FileEntry, pass maxTotalFiles down
<UploadInfo status={props.fileStatus} error={props.errorDescription} maxTotalFiles={maxTotalFiles} />

// UploadInfo signature
type UploadInfoProps = { status: FileStatus; error?: string; maxTotalFiles?: number };

⚠️ Low — sortedFiles only moves validationError to the end; PR description says "rejected" sorts below "successful"

File: src/stores/FileUploaderStore.ts lines 190–196
Note: The PR description states "successful uploads shown above rejected files", but the sort only demotes validationErrorrejected files are sorted the same as queued, uploading, and done. If the intended UX is rejected-to-bottom as well, the comparator should include rejected:

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

If this is intentional (rejected shown interleaved with active files), a comment or test clarifying the design would help.


⚠️ Low — RetryButton is icon-only with no accessible label for screen readers

File: src/components/RetryButton.tsx lines 22–30
Note: The button has a title attribute (tooltip) but no aria-label. title is not reliably announced by screen readers. For WCAG 2.2 AA compliance the button needs an accessible name:

<button
    className="retry-button"
    disabled={!store.canRetry}
    onClick={onClick}
    aria-label={translations.get("retryButtonTextMessage")}
    title={translations.get("retryButtonTextMessage")}
>
    <span className="retry-icon" aria-hidden />
</button>

⚠️ Low — removedFile status shown with error CSS class in UploadInfo

File: src/components/UploadInfo.tsx line 30
Note: removeSuccessMessage is "Removed successfully." — a success message — but it's rendered with className="upload-status error". This is pre-existing but was kept unchanged and is now adjacent to new status rendering. Worth fixing here for consistency: use "upload-status" (no error) or "upload-status success".


⚠️ Low — promoteQueuedFiles iterates files array in reverse but comment says "oldest first: last in array = oldest"

File: src/stores/FileUploaderStore.ts lines 211–216
Note: New files are prepended (unshift), so index 0 is the newest and the last element is the oldest. Calling .reverse() on the filtered array puts the oldest at index 0 — correct. However, the comment "oldest first: last in array = oldest" describes the original (pre-reverse) ordering, which is the opposite of how .reverse() re-orders things. The comment is technically accurate but easy to misread as justifying no-reversal. Suggest rewording:

// files are prepended (index 0 = newest), so reverse gives oldest-first order
const queued = [...this.files].filter(f => f.fileStatus === "queued").reverse();

Positives

  • The promotion reaction (reaction(() => ({ uploading, queued }), ...)) is well-scoped and uses comparer.structural to avoid spurious fires — a non-obvious correctness detail done right.
  • dispose() cleanly tears down the reaction and is wired to the React unmount lifecycle in useInitRootStore — no reaction leak.
  • FileStore private field encapsulation (private _file, _objectItem, etc.) is a meaningful improvement that was correctly coupled with the makeObservable<this, "...">() type-narrowing signature.
  • The test suite is extensive (31 describe blocks, 953 lines) and exercises the reactive drain behaviour, the boundary conditions for canRetry, and the end-to-end queue flow — well above the usual coverage for store-level changes in this repo.
  • Splitting processDrop into a capacity classifier (rejected vs. queued) with no upload side-effects, and letting promoteQueuedFiles own all uploading decisions, is a clean separation of concerns.
  • XML required="false" is correctly set on the two new optional expression properties, and defaults in the existing maxFilesPerUpload property are preserved.

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