-
Notifications
You must be signed in to change notification settings - Fork 346
feat(content-uploader): implement cancel all freeze #4665
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -157,6 +157,8 @@ class ContentUploader extends Component<ContentUploaderProps, State> { | |||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| isAutoExpanded: boolean = false; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| isCancelAllPaused: boolean = false; | ||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| modernizedDismissTimer: ReturnType<typeof setTimeout> | null = null; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| isPanelHovered: boolean = false; | ||||||||||||||||||||||||||||||||||||
|
|
@@ -890,6 +892,10 @@ class ContentUploader extends Component<ContentUploaderProps, State> { | |||||||||||||||||||||||||||||||||||
| upload = () => { | ||||||||||||||||||||||||||||||||||||
| const { enableModernizedUploads, isUpgradeModalEnabled, maxFileSize } = this.props; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (this.isCancelAllPaused) { | ||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||
| maxFileSize && | ||||||||||||||||||||||||||||||||||||
| enableModernizedUploads && | ||||||||||||||||||||||||||||||||||||
|
|
@@ -899,6 +905,7 @@ class ContentUploader extends Component<ContentUploaderProps, State> { | |||||||||||||||||||||||||||||||||||
| if (!this.state.isLargeFileWarningModalOpen) { | ||||||||||||||||||||||||||||||||||||
| this.setState({ isLargeFileWarningModalOpen: true }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -919,6 +926,10 @@ class ContentUploader extends Component<ContentUploaderProps, State> { | |||||||||||||||||||||||||||||||||||
| const { enableModernizedUploads, overwrite, rootFolderId } = this.props; | ||||||||||||||||||||||||||||||||||||
| const { api, file, options } = item; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (this.isCancelAllPaused) { | ||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const numItemsUploading = this.itemsRef.current.filter(item_t => item_t.status === STATUS_IN_PROGRESS).length; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (numItemsUploading >= UPLOAD_CONCURRENCY) { | ||||||||||||||||||||||||||||||||||||
|
|
@@ -1255,6 +1266,23 @@ class ContentUploader extends Component<ContentUploaderProps, State> { | |||||||||||||||||||||||||||||||||||
| this.updateViewAndCollection(updatedItems); | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * Whether an item uploads through the chunked (multiput) API rather than the | ||||||||||||||||||||||||||||||||||||
| * plain oneshot API. This is the same decision getUploadAPI makes when picking | ||||||||||||||||||||||||||||||||||||
| * the API instance, and it determines which items can be paused/resumed in place | ||||||||||||||||||||||||||||||||||||
| * versus cancelled and restarted from scratch. | ||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||
| * @param {UploadItem} item - The upload item | ||||||||||||||||||||||||||||||||||||
| * @return {boolean} | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
| 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(), | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1278
to
+1283
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * Updates item based on its status. | ||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||
|
|
@@ -1263,11 +1291,9 @@ class ContentUploader extends Component<ContentUploaderProps, State> { | |||||||||||||||||||||||||||||||||||
| * @return {void} | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
| onClick = (item: UploadItem) => { | ||||||||||||||||||||||||||||||||||||
| const { chunked, isResumableUploadsEnabled, onClickCancel, onClickResume, onClickRetry } = this.props; | ||||||||||||||||||||||||||||||||||||
| const { file, status, error } = item; | ||||||||||||||||||||||||||||||||||||
| const isChunkedUpload = | ||||||||||||||||||||||||||||||||||||
| chunked && !item.isFolder && file.size > CHUNKED_UPLOAD_MIN_SIZE_BYTES && isMultiputSupported(); | ||||||||||||||||||||||||||||||||||||
| const isResumable = isResumableUploadsEnabled && isChunkedUpload && item.api.sessionId; | ||||||||||||||||||||||||||||||||||||
| const { isResumableUploadsEnabled, onClickCancel, onClickResume, onClickRetry } = this.props; | ||||||||||||||||||||||||||||||||||||
| const { status, error } = item; | ||||||||||||||||||||||||||||||||||||
| const isResumable = isResumableUploadsEnabled && this.getIsChunkedUpload(item) && item.api.sessionId; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| switch (status) { | ||||||||||||||||||||||||||||||||||||
| case STATUS_IN_PROGRESS: | ||||||||||||||||||||||||||||||||||||
|
|
@@ -1338,19 +1364,68 @@ class ContentUploader extends Component<ContentUploaderProps, State> { | |||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * Open the Cancel All confirmation modal. Wired as the onCancelAll prop | ||||||||||||||||||||||||||||||||||||
| * passed to the modernized uploads manager so the action requires explicit | ||||||||||||||||||||||||||||||||||||
| * confirmation before destroying in-progress uploads. | ||||||||||||||||||||||||||||||||||||
| * Freeze in-progress uploads while the Cancel All confirmation modal is open. | ||||||||||||||||||||||||||||||||||||
| * Chunked uploads are paused so they can resume from the same session; plain | ||||||||||||||||||||||||||||||||||||
| * uploads have no partial server state, so they are cancelled in the background | ||||||||||||||||||||||||||||||||||||
| * and restarted from scratch if the user chooses to keep uploading. Item status | ||||||||||||||||||||||||||||||||||||
| * and progress are intentionally left untouched so the rows stay frozen in place. | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
| pauseUploadsForCancelAll = () => { | ||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| const { api } = item; | ||||||||||||||||||||||||||||||||||||
| if (this.getIsChunkedUpload(item)) { | ||||||||||||||||||||||||||||||||||||
| api.pause(); | ||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||
| api.cancel(); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| this.isCancelAllPaused = true; | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * Resume uploads frozen by pauseUploadsForCancelAll when the user dismisses the | ||||||||||||||||||||||||||||||||||||
| * modal. Chunked uploads continue from their session; plain uploads (cancelled in | ||||||||||||||||||||||||||||||||||||
| * the background) are restarted from scratch. Pending items that never started are | ||||||||||||||||||||||||||||||||||||
| * picked up by the trailing upload() call. | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
| resumeUploadsForCancelAll = () => { | ||||||||||||||||||||||||||||||||||||
| this.isCancelAllPaused = false; | ||||||||||||||||||||||||||||||||||||
| this.itemsRef.current.forEach(item => { | ||||||||||||||||||||||||||||||||||||
| if (item.status !== STATUS_IN_PROGRESS || item.isFolder) { | ||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| if (this.getIsChunkedUpload(item)) { | ||||||||||||||||||||||||||||||||||||
| item.api.unpause(); | ||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||
| this.resetFile(item); | ||||||||||||||||||||||||||||||||||||
| this.uploadFile(item); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| this.upload(); | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * Open the Cancel All confirmation modal and freeze in-progress uploads. Wired as | ||||||||||||||||||||||||||||||||||||
| * the onCancelAll prop passed to the modernized uploads manager so the action | ||||||||||||||||||||||||||||||||||||
| * requires explicit confirmation before destroying in-progress uploads. | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
| handleCancelAllClick = () => { | ||||||||||||||||||||||||||||||||||||
| this.pauseUploadsForCancelAll(); | ||||||||||||||||||||||||||||||||||||
| this.setState({ isCancelAllModalOpen: true }); | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| handleCancelAllDismiss = () => { | ||||||||||||||||||||||||||||||||||||
| this.setState({ isCancelAllModalOpen: false }); | ||||||||||||||||||||||||||||||||||||
| this.resumeUploadsForCancelAll(); | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| handleCancelAllConfirm = () => { | ||||||||||||||||||||||||||||||||||||
| this.isCancelAllPaused = false; | ||||||||||||||||||||||||||||||||||||
| this.setState({ isCancelAllModalOpen: false }); | ||||||||||||||||||||||||||||||||||||
| this.handleUploadsManagerCancelAll(); | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
@@ -1373,17 +1448,15 @@ class ContentUploader extends Component<ContentUploaderProps, State> { | |||||||||||||||||||||||||||||||||||
| * chunked uploads, otherwise restart from scratch. Shared by RetryAll and per-item retry. | ||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
| retryUploadsManagerItem = (item: UploadItem) => { | ||||||||||||||||||||||||||||||||||||
| const { chunked, isResumableUploadsEnabled, onClickCancel, onClickResume, onClickRetry } = this.props; | ||||||||||||||||||||||||||||||||||||
| const { file, api, error } = item; | ||||||||||||||||||||||||||||||||||||
| const { isResumableUploadsEnabled, onClickCancel, onClickResume, onClickRetry } = this.props; | ||||||||||||||||||||||||||||||||||||
| const { api, error } = item; | ||||||||||||||||||||||||||||||||||||
| // Name-in-use cannot be resolved by retrying — drop the item like the legacy onClick path. | ||||||||||||||||||||||||||||||||||||
| if (error?.code === ERROR_CODE_ITEM_NAME_IN_USE) { | ||||||||||||||||||||||||||||||||||||
| this.removeFileFromUploadQueue(item); | ||||||||||||||||||||||||||||||||||||
| onClickCancel(item); | ||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| const isChunkedUpload = | ||||||||||||||||||||||||||||||||||||
| chunked && !item.isFolder && file.size > CHUNKED_UPLOAD_MIN_SIZE_BYTES && isMultiputSupported(); | ||||||||||||||||||||||||||||||||||||
| const isResumable = isResumableUploadsEnabled && isChunkedUpload && api?.sessionId; | ||||||||||||||||||||||||||||||||||||
| const isResumable = isResumableUploadsEnabled && this.getIsChunkedUpload(item) && api?.sessionId; | ||||||||||||||||||||||||||||||||||||
| if (isResumable) { | ||||||||||||||||||||||||||||||||||||
| item.bytesUploadedOnLastResume = api.totalUploadedBytes; | ||||||||||||||||||||||||||||||||||||
| this.resumeFile(item); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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
numPartsUploadingreaches zero, but the loop can run pastthis.parts.lengthforever if the counter and part states drift. Since Cancel All callspause()synchronously, that can hang the UI instead of freezing it.Suggested bounded scan
📝 Committable suggestion
🤖 Prompt for AI Agents