Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions i18n/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,16 @@ be.contentSidebar.editTask.general.title = Modify General Task
be.contentSidebar.mentionUserSelectorLoading = Loading users
# Accessibility role description for the mention user selector input
be.contentSidebar.mentionUserSelectorRoleDescription = Mention a user
# Aria label for the close button on the cancel all uploads modal
be.contentUploader.cancelAllUploadsCloseLabel = Close cancel uploads dialog
# Confirm button for the cancel all uploads modal
be.contentUploader.cancelAllUploadsConfirmButton = Cancel All
# Dismiss button for the cancel all uploads modal
be.contentUploader.cancelAllUploadsKeepButton = Keep Uploading
# Body content for the cancel all uploads confirmation modal
be.contentUploader.cancelAllUploadsModalContent = Files that are still uploading will be canceled. Completed uploads will not be affected.
# Heading for the cancel all uploads confirmation modal
be.contentUploader.cancelAllUploadsModalHeading = Cancel all uploads?
# Label for copy action.
be.copy = Copy
# Label for create action.
Expand Down
35 changes: 30 additions & 5 deletions src/elements/content-uploader/ContentUploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -977,22 +977,41 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
*/
updateViewAndCollection(items: UploadItem[], callback?: () => void) {
const {
enableModernizedUploads,
isPartialUploadEnabled,
isResumableUploadsEnabled,
onComplete,
useUploadsManager,
}: ContentUploaderProps = this.props;
const someUploadIsInProgress = items.some(uploadItem => uploadItem.status !== STATUS_COMPLETE);
// When the modernized flow is on, canceled items are kept in the list
// but treated as terminal so they do not block completion logic. The
// legacy view/state machine sees canceled items as non-COMPLETE which
// would otherwise stall progress and emit a stale success notification.
const isTerminalForModernized = (uploadItem: UploadItem) =>
enableModernizedUploads && uploadItem.status === STATUS_CANCELED;
const someUploadIsInProgress = items.some(
uploadItem => uploadItem.status !== STATUS_COMPLETE && !isTerminalForModernized(uploadItem),
);
const someUploadHasFailed = items.some(uploadItem => uploadItem.status === STATUS_ERROR);
const allItemsArePending = !items.some(uploadItem => uploadItem.status !== STATUS_PENDING);
const noFileIsPendingOrInProgress = items.every(
uploadItem => uploadItem.status !== STATUS_PENDING && uploadItem.status !== STATUS_IN_PROGRESS,
);
const areAllItemsFinished = items.every(
uploadItem => uploadItem.status === STATUS_COMPLETE || uploadItem.status === STATUS_ERROR,
uploadItem =>
uploadItem.status === STATUS_COMPLETE ||
uploadItem.status === STATUS_ERROR ||
isTerminalForModernized(uploadItem),
);
const uploadItemsStatus = isResumableUploadsEnabled ? areAllItemsFinished : noFileIsPendingOrInProgress;

// In the modernized flow, suppress the completion notification when
// no item actually finished successfully (e.g. user canceled every
// upload). This prevents a misleading "upload complete" toast/callback
// from any of the onComplete call sites below.
const hasCompletedItem = items.some(uploadItem => uploadItem.status === STATUS_COMPLETE);
const shouldFireOnComplete = !enableModernizedUploads || hasCompletedItem;

let view = '';
if ((items && items.length === 0) || allItemsArePending) {
view = VIEW_UPLOAD_EMPTY;
Expand All @@ -1001,7 +1020,9 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
view = VIEW_UPLOAD_SUCCESS;

if (!useUploadsManager) {
onComplete(cloneDeep(filesToBeUploaded.map(item => item.boxFile)));
if (shouldFireOnComplete) {
onComplete(cloneDeep(filesToBeUploaded.map(item => item.boxFile)));
}
// Reset item collection after successful upload
items = [];
}
Expand All @@ -1013,7 +1034,9 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
view = VIEW_UPLOAD_SUCCESS;

if (!useUploadsManager) {
onComplete(cloneDeep(items.map(item => item.boxFile)));
if (shouldFireOnComplete) {
onComplete(cloneDeep(items.map(item => item.boxFile)));
}
// Reset item collection after successful upload
items = [];
}
Expand All @@ -1023,7 +1046,9 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
if (this.isAutoExpanded) {
this.resetUploadManagerExpandState();
} // Else manually expanded so don't close
onComplete(items);
if (shouldFireOnComplete) {
onComplete(items);
}
}

const state: Partial<State> = {
Expand Down
66 changes: 66 additions & 0 deletions src/elements/content-uploader/__tests__/ContentUploader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,72 @@ describe('elements/content-uploader/ContentUploader', () => {
expect(complete.api.cancel).not.toHaveBeenCalled();
});

describe('updateViewAndCollection with canceled items', () => {
let onComplete;
let wrapper;
let instance;

beforeEach(() => {
onComplete = jest.fn();
wrapper = getWrapper({
enableModernizedUploads: true,
useUploadsManager: true,
onComplete,
});
instance = wrapper.instance();
});

test('should not fire onComplete when all items are canceled (modernized)', () => {
instance.updateViewAndCollection([
{ status: STATUS_CANCELED, file: { name: 'a' } },
{ status: STATUS_CANCELED, file: { name: 'b' } },
]);
expect(onComplete).not.toHaveBeenCalled();
});

test('should fire onComplete when at least one item completes (modernized)', () => {
instance.updateViewAndCollection([
{ status: STATUS_COMPLETE, file: { name: 'a' } },
{ status: STATUS_CANCELED, file: { name: 'b' } },
]);
expect(onComplete).toHaveBeenCalled();
});

test('should treat canceled items as terminal and resolve to VIEW_UPLOAD_SUCCESS (modernized)', () => {
instance.updateViewAndCollection([
{ status: STATUS_COMPLETE, file: { name: 'a' } },
{ status: STATUS_CANCELED, file: { name: 'b' } },
]);
expect(wrapper.state('view')).toBe(VIEW_UPLOAD_SUCCESS);
});

test('should not fire onComplete on the partial-upload path when all items are canceled (modernized, no manager)', () => {
onComplete = jest.fn();
wrapper = getWrapper({
enableModernizedUploads: true,
isPartialUploadEnabled: true,
useUploadsManager: false,
onComplete,
});
wrapper.instance().updateViewAndCollection([
{ status: STATUS_CANCELED, file: { name: 'a' } },
{ status: STATUS_CANCELED, file: { name: 'b' } },
]);
expect(onComplete).not.toHaveBeenCalled();
});

test('should preserve legacy behavior when modernized flag is off', () => {
onComplete = jest.fn();
wrapper = getWrapper({
enableModernizedUploads: false,
useUploadsManager: true,
onComplete,
});
wrapper.instance().updateViewAndCollection([{ status: STATUS_COMPLETE, file: { name: 'a' } }]);
expect(onComplete).toHaveBeenCalled();
});
});

test('handleUploadsManagerRetryAll should restart errored and canceled items', () => {
const onClickRetry = jest.fn();
const wrapper = getWrapper({ enableModernizedUploads: true, onClickRetry });
Expand Down
Loading