Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
39 changes: 39 additions & 0 deletions src/elements/content-uploader/CancelAllUploadsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from 'react';
import { useIntl } from 'react-intl';
import { AlertModal } from '@box/blueprint-web';
import messages from './messages';

export interface CancelAllUploadsModalProps {
isOpen: boolean;
onConfirm: () => void;
onDismiss: () => void;
}

export function CancelAllUploadsModal({ isOpen, onConfirm, onDismiss }: CancelAllUploadsModalProps) {
const { formatMessage } = useIntl();

const handleOpenChange = (open: boolean) => {
if (!open) {
onDismiss();
}
};

return (
<AlertModal
open={isOpen}
onOpenChange={handleOpenChange}
heading={formatMessage(messages.cancelAllUploadsModalHeading)}
textContent={formatMessage(messages.cancelAllUploadsModalContent)}
closeButtonAriaLabel={formatMessage(messages.cancelAllUploadsCloseLabel)}
>
<AlertModal.SecondaryButton onClick={onDismiss}>
{formatMessage(messages.cancelAllUploadsKeepButton)}
</AlertModal.SecondaryButton>
<AlertModal.PrimaryButton variant="destructive" onClick={onConfirm}>
{formatMessage(messages.cancelAllUploadsConfirmButton)}
</AlertModal.PrimaryButton>
</AlertModal>
);
}

export default CancelAllUploadsModal;
30 changes: 28 additions & 2 deletions src/elements/content-uploader/ContentUploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import DroppableContent from './DroppableContent';
import Footer from './Footer';
import UploadsManager from './UploadsManager';
import { mapToModernizedUploadItems } from './utils/mapToModernizedUploadItem';
import CancelAllUploadsModal from './CancelAllUploadsModal';
import API from '../../api';
import Browser from '../../utils/Browser';
import Internationalize from '../common/Internationalize';
Expand Down Expand Up @@ -112,6 +113,7 @@ export interface ContentUploaderProps {

type State = {
errorCode?: string;
isCancelAllModalOpen: boolean;
isUploadsManagerExpanded: boolean;
itemIds: Object;
items: UploadItem[];
Expand Down Expand Up @@ -190,6 +192,7 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
items: [],
errorCode: '',
itemIds: {},
isCancelAllModalOpen: false,
isUploadsManagerExpanded: false,
};
this.id = uniqueid('bcu_');
Expand Down Expand Up @@ -1195,6 +1198,24 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
* Cancel every pending or in-progress upload at once. Items keep their row
* in the list with the canceled status. Only used by the modernized flow.
*/
/**
* 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.
*/
handleCancelAllRequest = () => {
this.setState({ isCancelAllModalOpen: true });
};

handleCancelAllDismiss = () => {
this.setState({ isCancelAllModalOpen: false });
};

handleCancelAllConfirm = () => {
this.setState({ isCancelAllModalOpen: false });
this.handleCancelAllUploads();
};

handleCancelAllUploads = () => {
const cancelable: UploadItem[] = this.itemsRef.current.filter(
item => item.status === STATUS_PENDING || item.status === STATUS_IN_PROGRESS,
Expand Down Expand Up @@ -1411,7 +1432,7 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
theme,
useUploadsManager,
}: ContentUploaderProps = this.props;
const { view, items, errorCode, isUploadsManagerExpanded }: State = this.state;
const { view, items, errorCode, isCancelAllModalOpen, isUploadsManagerExpanded }: State = this.state;
const isEmpty = items.length === 0;
const isVisible = !isEmpty || !!isDraggingItemsToUploadsManager;

Expand All @@ -1436,9 +1457,14 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
onItemCancel={this.handleModernizedItemCancel}
onItemRetry={this.handleModernizedItemRetry}
onItemRemove={this.handleModernizedItemRemove}
onCancelAll={this.handleCancelAllUploads}
onCancelAll={this.handleCancelAllRequest}
onRetryAll={this.handleRetryAllUploads}
/>
<CancelAllUploadsModal
isOpen={isCancelAllModalOpen}
onConfirm={this.handleCancelAllConfirm}
onDismiss={this.handleCancelAllDismiss}
/>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as React from 'react';
import { render, screen, userEvent } from '../../../test-utils/testing-library';
import CancelAllUploadsModal, { type CancelAllUploadsModalProps } from '../CancelAllUploadsModal';

const renderModal = (props: Partial<CancelAllUploadsModalProps> = {}) => {
const defaultProps: CancelAllUploadsModalProps = {
isOpen: true,
onConfirm: jest.fn(),
onDismiss: jest.fn(),
...props,
};
render(<CancelAllUploadsModal {...defaultProps} />);
return defaultProps;
};

describe('elements/content-uploader/CancelAllUploadsModal', () => {
test('renders heading, body, and both action buttons when open', async () => {
renderModal();
expect(await screen.findByRole('alertdialog')).toBeInTheDocument();
expect(screen.getByText('Cancel all uploads?')).toBeInTheDocument();
expect(screen.getByText(/Files that are still uploading will be canceled/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Cancel All' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Keep Uploading' })).toBeInTheDocument();
});

test('calls onConfirm when Cancel All is clicked', async () => {
const props = renderModal();
const user = userEvent();
const button = await screen.findByRole('button', { name: 'Cancel All' });
await user.click(button);
expect(props.onConfirm).toHaveBeenCalledTimes(1);
expect(props.onDismiss).not.toHaveBeenCalled();
});

test('calls onDismiss when Keep Uploading is clicked', async () => {
const props = renderModal();
const user = userEvent();
const button = await screen.findByRole('button', { name: 'Keep Uploading' });
await user.click(button);
expect(props.onDismiss).toHaveBeenCalledTimes(1);
expect(props.onConfirm).not.toHaveBeenCalled();
});

test('does not render dialog when isOpen is false', () => {
renderModal({ isOpen: false });
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
});
});
47 changes: 44 additions & 3 deletions src/elements/content-uploader/__tests__/ContentUploader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -887,7 +887,28 @@ describe('elements/content-uploader/ContentUploader', () => {
expect(cancelMock).not.toHaveBeenCalled();
});

test('handleCancelAllUploads should cancel all in-progress and pending items', () => {
test('onCancelAll should open the confirmation modal instead of canceling directly', () => {
const wrapper = getWrapper({ enableModernizedUploads: true });
const inProgress = {
name: 'a.pdf',
extension: 'pdf',
progress: 25,
status: STATUS_IN_PROGRESS,
file: { name: 'a.pdf' },
api: { cancel: jest.fn() },
};
wrapper.setState({ items: [inProgress] });
const instance = wrapper.instance();
instance.itemsRef.current = [inProgress];

wrapper.find(UploadsManagerBP).prop('onCancelAll')();

expect(wrapper.state('isCancelAllModalOpen')).toBe(true);
expect(inProgress.status).toBe(STATUS_IN_PROGRESS);
expect(inProgress.api.cancel).not.toHaveBeenCalled();
});

test('handleCancelAllConfirm should cancel all in-progress and pending items and close modal', () => {
const wrapper = getWrapper({ enableModernizedUploads: true });
const inProgress = {
name: 'a.pdf',
Expand All @@ -913,12 +934,13 @@ describe('elements/content-uploader/ContentUploader', () => {
file: { name: 'c.pdf' },
api: { cancel: jest.fn() },
};
wrapper.setState({ items: [inProgress, pending, complete] });
wrapper.setState({ items: [inProgress, pending, complete], isCancelAllModalOpen: true });
const instance = wrapper.instance();
instance.itemsRef.current = [inProgress, pending, complete];

wrapper.find(UploadsManagerBP).prop('onCancelAll')();
instance.handleCancelAllConfirm();

expect(wrapper.state('isCancelAllModalOpen')).toBe(false);
expect(inProgress.status).toBe('canceled');
expect(pending.status).toBe('canceled');
expect(complete.status).toBe(STATUS_COMPLETE);
Expand All @@ -927,6 +949,25 @@ describe('elements/content-uploader/ContentUploader', () => {
expect(complete.api.cancel).not.toHaveBeenCalled();
});

test('handleCancelAllDismiss should close the modal without canceling uploads', () => {
const wrapper = getWrapper({ enableModernizedUploads: true });
const inProgress = {
name: 'a.pdf',
status: STATUS_IN_PROGRESS,
file: { name: 'a.pdf' },
api: { cancel: jest.fn() },
};
wrapper.setState({ items: [inProgress], isCancelAllModalOpen: true });
const instance = wrapper.instance();
instance.itemsRef.current = [inProgress];

instance.handleCancelAllDismiss();

expect(wrapper.state('isCancelAllModalOpen')).toBe(false);
expect(inProgress.status).toBe(STATUS_IN_PROGRESS);
expect(inProgress.api.cancel).not.toHaveBeenCalled();
});

test('updateViewAndCollection should not fire onComplete when all items are canceled (modernized)', () => {
const onComplete = jest.fn();
const wrapper = getWrapper({
Expand Down
31 changes: 31 additions & 0 deletions src/elements/content-uploader/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { defineMessages } from 'react-intl';

const messages = defineMessages({
cancelAllUploadsModalHeading: {
id: 'be.contentUploader.cancelAllUploadsModalHeading',
defaultMessage: 'Cancel all uploads?',
description: 'Heading for the cancel all uploads confirmation modal',
},
cancelAllUploadsModalContent: {
id: 'be.contentUploader.cancelAllUploadsModalContent',
defaultMessage: 'Files that are still uploading will be canceled. Completed uploads will not be affected.',
description: 'Body content for the cancel all uploads confirmation modal',
},
cancelAllUploadsConfirmButton: {
id: 'be.contentUploader.cancelAllUploadsConfirmButton',
defaultMessage: 'Cancel All',
description: 'Confirm button for the cancel all uploads modal',
},
cancelAllUploadsKeepButton: {
id: 'be.contentUploader.cancelAllUploadsKeepButton',
defaultMessage: 'Keep Uploading',
description: 'Dismiss button for the cancel all uploads modal',
},
cancelAllUploadsCloseLabel: {
id: 'be.contentUploader.cancelAllUploadsCloseLabel',
defaultMessage: 'Close cancel uploads dialog',
description: 'Aria label for the close button on the cancel all uploads modal',
},
});

export default messages;
Loading