Skip to content

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

Open
dealwith wants to merge 1 commit into
masterfrom
preserve-upload-cancel-state
Open

feat(content-uploader): implement cancel all freeze#4665
dealwith wants to merge 1 commit 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 uploads, with resumable continuation later.
    • Enhanced the “Cancel All” workflow to temporarily freeze resumable (chunked) uploads and resume them when the modal is dismissed.
  • Bug Fixes
    • Ensured no new chunk processing or part uploads begin while uploads are paused.
    • Kept existing progress/status stable while uploads are frozen to avoid confusing UI updates.
  • Tests
    • Expanded automated coverage for pause/resume behavior and cancel-all modal interactions, including 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

Walkthrough

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

Changes

Pause/Resume Upload Flow

Layer / File(s) Summary
MultiputUpload pause/unpause implementation
src/api/uploads/MultiputUpload.js
Adds isPaused, blocks digest and part-upload scheduling while paused, and adds pause()/unpause() to stop and resume in-flight work.
MultiputUpload pause/unpause tests
src/api/uploads/__tests__/MultiputUpload.test.js
Extends paused-state coverage for scheduling gates and adds unit tests for pause, unpause, destroyed no-ops, and double-pausing.
ContentUploader Cancel All freeze/resume wiring
src/elements/content-uploader/ContentUploader.tsx
Adds isCancelAllPaused, getIsChunkedUpload(), early returns in upload entry points, and pause/resume handlers for chunked versus plain items during Cancel All.
ContentUploader Cancel All tests
src/elements/content-uploader/__tests__/ContentUploader.test.js
Updates cancel-all coverage to assert pause/resume behavior, folder exclusion, restart behavior for plain uploads, and upload suppression while paused.

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

Possibly related PRs

  • box/box-ui-elements#4577: Updates ContentUploader cancel-all handling and cancel outcomes in the same upload flow.
  • box/box-ui-elements#4579: Adds the cancel-all confirmation modal flow that this PR hooks into for pause/resume behavior.
  • box/box-ui-elements#4649: Also touches ContentUploader modal gating and chunked-upload branching with getIsChunkedUpload()-style logic.

Suggested labels: ready-to-merge

Suggested reviewers: jpan-box, olehrybak, tjuanitas

Poem

A bunny tiptoes by the queue,
Pausing uploads in a hush or two.
Chunked hops sleep, then wake and race,
While plain ones start again with grace.
🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding a cancel-all freeze for content uploads.
Description check ✅ Passed The description provides the change context and aligns with the repository template content, with the required guidance block included.
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

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Biome (2.5.1)
src/api/uploads/MultiputUpload.js

File contains syntax errors that prevent linting: Line 30: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 31: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 32: 'import type' are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.; Line 48: return types can only be used in TypeScript files; Line 48: expected a semicolon to end the class property, but found none; Line 48: Expected an identifier, a string literal, a number literal, a private field name, or a computed name but instead found '; Line 50: return types can only be used in TypeScript files; Line 52: return types can only be used in TypeScript files; Line 54: return types can only be used in TypeScript files; Line 54: expected a semicolon to end the class property, but found none; Line 54: Expected an identifier, a string literal, a number literal, a private field

... [truncated 12940 characters] ...

utside of a function; Line 1318: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 1318: expected , but instead found :; Line 1318: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 1321: Illegal return statement outside of a function; Line 1325: Illegal return statement outside of a function; Line 1339: expected , but instead found :; Line 1339: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 1341: Illegal return statement outside of a function; Line 1346: Illegal return statement outside of a function; Line 1350: Illegal return statement outside of a function; Line 1353: Illegal return statement outside of a function; Line 1355: Expected a statement but instead found '}'.


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"

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.

4 participants