Skip to content

feat(content-uploader): implement cancel all freeze#4665

Merged
mergify[bot] merged 2 commits into
masterfrom
preserve-upload-cancel-state
Jul 9, 2026
Merged

feat(content-uploader): implement cancel all freeze#4665
mergify[bot] merged 2 commits into
masterfrom
preserve-upload-cancel-state

Conversation

@dealwith

@dealwith dealwith commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Context:
In the modernized Uploads Manager the new feature "cancel all" is slow in the UI. Users have to click cancel all and then confirm the in the modal. In order to make operation a bit faster we decided to pause upload at the time user clicks cancel all.

Summary by CodeRabbit

  • New Features
    • Added pause/resume support for in-progress resumable uploads, enabling later continuation.
    • Enhanced “Cancel All” to freeze resumable (chunked) uploads during confirmation and automatically resume on dismissal.
  • Bug Fixes
    • Prevented new digest/part uploads from starting while uploads are paused.
    • Kept item status/progress unchanged while the cancel-all modal is open; resumed behavior now matches the frozen/canceled mix.
  • Tests
    • Expanded unit and UI-flow coverage for pause/unpause, cancel-all freeze/resume, and folder handling.

@dealwith
dealwith requested review from a team as code owners June 30, 2026 14:47
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bf616420-0050-434f-a9f2-ec2dbe6f8918

📥 Commits

Reviewing files that changed from the base of the PR and between a2b499e and f295072.

📒 Files selected for processing (2)
  • src/elements/content-uploader/ContentUploader.tsx
  • src/elements/content-uploader/__tests__/ContentUploader.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/elements/content-uploader/tests/ContentUploader.test.js
  • src/elements/content-uploader/ContentUploader.tsx

Walkthrough

Adds pause and resume support to MultiputUpload and uses it in ContentUploader to freeze in-progress uploads during Cancel All, then resume chunked and plain uploads when the modal is dismissed.

Changes

Pause/Resume Upload Flow

Layer / File(s) Summary
MultiputUpload paused state
src/api/uploads/MultiputUpload.js
Adds isPaused, gates digest and part-upload scheduling on paused state, and introduces pause() and unpause().
MultiputUpload pause tests
src/api/uploads/__tests__/MultiputUpload.test.js
Expands paused-state coverage for scheduling checks and adds unit tests for pause and unpause behavior.
ContentUploader Cancel All pause/resume
src/elements/content-uploader/ContentUploader.tsx
Adds cancel-all paused state, chunked-upload detection, upload gating, and pause/resume handlers for chunked and plain uploads.
ContentUploader Cancel All tests
src/elements/content-uploader/__tests__/ContentUploader.test.js
Updates cancel-all tests to assert frozen uploads, chunked pause and resume calls, folder exclusion, and upload suppression while paused.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: jpan-box, olehrybak

Poem

A bunny pauses with a hop,
Then lets the upload traffic stop.
Chunked hops snooze, plain hops reset,
Until the modal’s done with chat.
🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding cancel-all freeze behavior in ContentUploader.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch preserve-upload-cancel-state

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/api/uploads/__tests__/MultiputUpload.test.js (1)

805-813: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert reset() is called for the paused in-flight parts.

This test verifies pause() calls but not the paired reset() behavior that restores upload-ready counters/state. Add reset expectations so a regression that only pauses without resetting is caught.

Suggested assertions
             expect(multiputUploadTest.numPartsDigestReady).toBe(2);
+            expect(multiputUploadTest.parts[0].reset).toBeCalled();
             expect(multiputUploadTest.parts[0].pause).toBeCalled();
+            expect(multiputUploadTest.parts[1].reset).not.toBeCalled();
             expect(multiputUploadTest.parts[1].pause).not.toBeCalled();
+            expect(multiputUploadTest.parts[2].reset).toBeCalled();
             expect(multiputUploadTest.parts[2].pause).toBeCalled();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/uploads/__tests__/MultiputUpload.test.js` around lines 805 - 813, The
pause test for MultiputUpload only checks that in-flight parts are paused, but
it does not verify the paired reset behavior that should restore upload-ready
state. Update the test around multiputUploadTest.pause() to also assert that
reset() is called on the paused in-flight parts (the same parts that receive
pause()), using the existing parts array and pause/reset spies so regressions
that omit reset() are caught.
src/elements/content-uploader/ContentUploader.tsx (1)

1326-1340: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Flip the pause guard before invoking upload APIs.

Line 1340 sets isCancelAllPaused after api.pause() / api.cancel(). If those calls synchronously trigger progress/error callbacks, upload() can still start pending work before the freeze guard is active.

Proposed fix
     pauseUploadsForCancelAll = () => {
+        this.isCancelAllPaused = true;
         this.itemsRef.current.forEach(item => {
             // Folder containers have no partial state and their child files are
             // tracked as separate queue items handled by this same loop.
             if (item.status !== STATUS_IN_PROGRESS || item.isFolder) {
                 return;
@@
                 api.cancel();
             }
         });
-        this.isCancelAllPaused = true;
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/elements/content-uploader/ContentUploader.tsx` around lines 1326 - 1340,
The cancel-all pause guard in pauseUploadsForCancelAll is being set too late, so
synchronous callbacks from api.pause() or api.cancel() can still let upload()
proceed. Move the isCancelAllPaused assignment to before iterating items and
invoking the upload APIs, and keep the existing STATUS_IN_PROGRESS and isFolder
checks intact so the freeze guard is active before any callbacks fire.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/api/uploads/MultiputUpload.js`:
- Around line 1285-1295: The pause scan in the Multipart upload flow can run
past the end of the parts list if numPartsUploading and the part states get out
of sync, so bound the loop by this.parts.length in the upload pause path. Update
the logic around this.firstUnuploadedPartIndex in MultiputUpload.js so the scan
stops once all parts have been checked, while still resetting and pausing any
PART_STATE_UPLOADING entries and adjusting numPartsUploading/numPartsDigestReady
as needed.

In `@src/elements/content-uploader/ContentUploader.tsx`:
- Around line 1231-1236: The chunked-upload check in
ContentUploader.getIsChunkedUpload is recomputing upload mode from the mutable
chunked prop, which can diverge from the API chosen when the item was queued.
Update the logic around getIsChunkedUpload and any pause/cancel handling to use
the actual upload API capability or a mode flag stored on the UploadItem at
creation time, so active uploads are classified consistently even if props
change.

---

Nitpick comments:
In `@src/api/uploads/__tests__/MultiputUpload.test.js`:
- Around line 805-813: The pause test for MultiputUpload only checks that
in-flight parts are paused, but it does not verify the paired reset behavior
that should restore upload-ready state. Update the test around
multiputUploadTest.pause() to also assert that reset() is called on the paused
in-flight parts (the same parts that receive pause()), using the existing parts
array and pause/reset spies so regressions that omit reset() are caught.

In `@src/elements/content-uploader/ContentUploader.tsx`:
- Around line 1326-1340: The cancel-all pause guard in pauseUploadsForCancelAll
is being set too late, so synchronous callbacks from api.pause() or api.cancel()
can still let upload() proceed. Move the isCancelAllPaused assignment to before
iterating items and invoking the upload APIs, and keep the existing
STATUS_IN_PROGRESS and isFolder checks intact so the freeze guard is active
before any callbacks fire.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8f6b439c-924c-4fa6-ac83-5fb278f125fb

📥 Commits

Reviewing files that changed from the base of the PR and between 200a292 and d714dd5.

📒 Files selected for processing (4)
  • src/api/uploads/MultiputUpload.js
  • src/api/uploads/__tests__/MultiputUpload.test.js
  • src/elements/content-uploader/ContentUploader.tsx
  • src/elements/content-uploader/__tests__/ContentUploader.test.js

Comment on lines +1285 to +1295
let nextUploadIndex = this.firstUnuploadedPartIndex;
while (this.numPartsUploading > 0) {
const part = this.parts[nextUploadIndex];
if (part && part.state === PART_STATE_UPLOADING) {
part.reset();
part.pause();
this.numPartsUploading -= 1;
this.numPartsDigestReady += 1;
}
nextUploadIndex += 1;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the pause scan by the parts array.

Line 1286 loops until numPartsUploading reaches zero, but the loop can run past this.parts.length forever if the counter and part states drift. Since Cancel All calls pause() synchronously, that can hang the UI instead of freezing it.

Suggested bounded scan
-        let nextUploadIndex = this.firstUnuploadedPartIndex;
-        while (this.numPartsUploading > 0) {
-            const part = this.parts[nextUploadIndex];
+        let numPartsPaused = 0;
+        for (let nextUploadIndex = this.firstUnuploadedPartIndex; nextUploadIndex < this.parts.length; nextUploadIndex += 1) {
+            const part = this.parts[nextUploadIndex];
             if (part && part.state === PART_STATE_UPLOADING) {
                 part.reset();
                 part.pause();
-                this.numPartsUploading -= 1;
-                this.numPartsDigestReady += 1;
+                numPartsPaused += 1;
             }
-            nextUploadIndex += 1;
         }
+
+        this.numPartsUploading -= numPartsPaused;
+        this.numPartsDigestReady += numPartsPaused;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let nextUploadIndex = this.firstUnuploadedPartIndex;
while (this.numPartsUploading > 0) {
const part = this.parts[nextUploadIndex];
if (part && part.state === PART_STATE_UPLOADING) {
part.reset();
part.pause();
this.numPartsUploading -= 1;
this.numPartsDigestReady += 1;
}
nextUploadIndex += 1;
}
let numPartsPaused = 0;
for (let nextUploadIndex = this.firstUnuploadedPartIndex; nextUploadIndex < this.parts.length; nextUploadIndex += 1) {
const part = this.parts[nextUploadIndex];
if (part && part.state === PART_STATE_UPLOADING) {
part.reset();
part.pause();
numPartsPaused += 1;
}
}
this.numPartsUploading -= numPartsPaused;
this.numPartsDigestReady += numPartsPaused;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/uploads/MultiputUpload.js` around lines 1285 - 1295, The pause scan
in the Multipart upload flow can run past the end of the parts list if
numPartsUploading and the part states get out of sync, so bound the loop by
this.parts.length in the upload pause path. Update the logic around
this.firstUnuploadedPartIndex in MultiputUpload.js so the scan stops once all
parts have been checked, while still resetting and pausing any
PART_STATE_UPLOADING entries and adjusting numPartsUploading/numPartsDigestReady
as needed.

Comment on lines +1231 to +1236
getIsChunkedUpload = (item: UploadItem): boolean => {
const { chunked } = this.props;
const { file, isFolder } = item;
return Boolean(
chunked && !isFolder && file && file.size > CHUNKED_UPLOAD_MIN_SIZE_BYTES && isMultiputSupported(),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid recomputing chunkedness from mutable props.

Line 1232 classifies an existing item using the current chunked prop, but the concrete API was selected when the item was queued. If chunked changes while uploads are active, this can call api.pause() on a plain API or cancel a real chunked upload instead of pausing it. Prefer checking the actual API capability or storing the upload mode on the item when it is created.

Proposed fix
     getIsChunkedUpload = (item: UploadItem): boolean => {
-        const { chunked } = this.props;
-        const { file, isFolder } = item;
+        const { api, file, isFolder } = item;
         return Boolean(
-            chunked && !isFolder && file && file.size > CHUNKED_UPLOAD_MIN_SIZE_BYTES && isMultiputSupported(),
+            !isFolder &&
+                file &&
+                file.size > CHUNKED_UPLOAD_MIN_SIZE_BYTES &&
+                api &&
+                typeof api.pause === 'function' &&
+                typeof api.unpause === 'function',
         );
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getIsChunkedUpload = (item: UploadItem): boolean => {
const { chunked } = this.props;
const { file, isFolder } = item;
return Boolean(
chunked && !isFolder && file && file.size > CHUNKED_UPLOAD_MIN_SIZE_BYTES && isMultiputSupported(),
);
getIsChunkedUpload = (item: UploadItem): boolean => {
const { api, file, isFolder } = item;
return Boolean(
!isFolder &&
file &&
file.size > CHUNKED_UPLOAD_MIN_SIZE_BYTES &&
api &&
typeof api.pause === 'function' &&
typeof api.unpause === 'function',
);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/elements/content-uploader/ContentUploader.tsx` around lines 1231 - 1236,
The chunked-upload check in ContentUploader.getIsChunkedUpload is recomputing
upload mode from the mutable chunked prop, which can diverge from the API chosen
when the item was queued. Update the logic around getIsChunkedUpload and any
pause/cancel handling to use the actual upload API capability or a mode flag
stored on the UploadItem at creation time, so active uploads are classified
consistently even if props change.

@dealwith
dealwith force-pushed the preserve-upload-cancel-state branch from d714dd5 to a2b499e Compare July 3, 2026 09:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/api/uploads/MultiputUpload.js (1)

1285-1295: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the pause scan by the parts array.

while (this.numPartsUploading > 0) at Line 1286 never decrements the counter once nextUploadIndex runs past this.parts.length (the indexed part becomes undefined, so the if body is skipped). If numPartsUploading and the per-part states ever drift, this loop spins forever, and since Cancel All invokes pause() synchronously it hangs the UI instead of freezing it. Bound the iteration by this.parts.length.

Suggested bounded scan
-        let nextUploadIndex = this.firstUnuploadedPartIndex;
-        while (this.numPartsUploading > 0) {
-            const part = this.parts[nextUploadIndex];
+        let numPartsPaused = 0;
+        for (let nextUploadIndex = this.firstUnuploadedPartIndex; nextUploadIndex < this.parts.length; nextUploadIndex += 1) {
+            const part = this.parts[nextUploadIndex];
             if (part && part.state === PART_STATE_UPLOADING) {
                 part.reset();
                 part.pause();
-                this.numPartsUploading -= 1;
-                this.numPartsDigestReady += 1;
+                numPartsPaused += 1;
             }
-            nextUploadIndex += 1;
         }
+
+        this.numPartsUploading -= numPartsPaused;
+        this.numPartsDigestReady += numPartsPaused;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/uploads/MultiputUpload.js` around lines 1285 - 1295, The pause scan
in `MultiputUpload.pause()` can loop forever because it only checks
`this.numPartsUploading` and keeps advancing `nextUploadIndex` past
`this.parts.length` when no matching `part` is found. Update the loop to also
bound iteration by `this.parts.length` (using `nextUploadIndex` and the
`this.parts` array length) so the scan stops even if `numPartsUploading` and
part states drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/api/uploads/MultiputUpload.js`:
- Around line 1285-1295: The pause scan in `MultiputUpload.pause()` can loop
forever because it only checks `this.numPartsUploading` and keeps advancing
`nextUploadIndex` past `this.parts.length` when no matching `part` is found.
Update the loop to also bound iteration by `this.parts.length` (using
`nextUploadIndex` and the `this.parts` array length) so the scan stops even if
`numPartsUploading` and part states drift.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 13eca6dd-5dbd-419f-9db2-1c0f097c2814

📥 Commits

Reviewing files that changed from the base of the PR and between d714dd5 and a2b499e.

📒 Files selected for processing (4)
  • src/api/uploads/MultiputUpload.js
  • src/api/uploads/__tests__/MultiputUpload.test.js
  • src/elements/content-uploader/ContentUploader.tsx
  • src/elements/content-uploader/__tests__/ContentUploader.test.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/elements/content-uploader/tests/ContentUploader.test.js
  • src/api/uploads/tests/MultiputUpload.test.js
  • src/elements/content-uploader/ContentUploader.tsx

@olehrybak olehrybak left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

looks good

@tjuanitas tjuanitas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the implementation could be renamed to work for all pause / resume but lgtm


isAutoExpanded: boolean = false;

isCancelAllPaused: boolean = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think based on the implementation of the methods, you could have a generic isPaused state and pauseUploads / resumeUploads instead of making it specific to "Cancel All"

@mergify

mergify Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

  • Entered queue2026-07-09 10:54 UTC · Rule: Automatic strict merge · triggered by rule Automatic merge queue
  • Checks passed · in-place
  • Merged2026-07-09 11:05 UTC · at f2950724a4521faf68cbbee73892fe8e9bddae58 · squash

This pull request spent 11 minutes in the queue, including 10 minutes 43 seconds running CI.

Required conditions to merge
  • github-review-approved [🛡 GitHub branch protection]
  • github-review-decision = APPROVED [🛡 GitHub branch protection]
  • any of [🛡 GitHub branch protection]:
    • check-success = Summary
    • check-neutral = Summary
    • check-skipped = Summary
  • any of [🛡 GitHub branch protection]:
    • check-success = lint_test_build
    • check-neutral = lint_test_build
    • check-skipped = lint_test_build
  • any of [🛡 GitHub branch protection]:
    • check-success = license/cla
    • check-neutral = license/cla
    • check-skipped = license/cla
  • any of [🛡 GitHub branch protection]:
    • check-success = lint_pull_request
    • check-neutral = lint_pull_request
    • check-skipped = lint_pull_request

@mergify
mergify Bot requested a review from a team as a code owner July 9, 2026 10:55
@mergify
mergify Bot merged commit 1ebc004 into master Jul 9, 2026
14 checks passed
@mergify
mergify Bot deleted the preserve-upload-cancel-state branch July 9, 2026 11:05
@mergify mergify Bot removed the queued label Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants