Skip to content
Open
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
12 changes: 12 additions & 0 deletions i18n/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,18 @@ be.contentUploader.cancelAllUploads.confirmButton = Cancel All
be.contentUploader.cancelAllUploads.heading = Cancel all uploads?
# Dismiss button for the cancel all uploads modal
be.contentUploader.cancelAllUploads.keepUploadingButton = Keep Uploading
# Body text in the warning modal informing the user that one or more files exceed their plan upload size limit. Contains a link to upgrade the plan.
be.contentUploader.largeFileWarning.body = {count, plural, one {This file exceeds} other {These files exceed}} the {maxFileSize} limit on your current plan. <link>Upgrade your plan</link> for larger uploads:
# Label for the button that cancels the upload attempt entirely
be.contentUploader.largeFileWarning.cancelButton = Cancel
# Accessible label for the close button in the large file warning modal
be.contentUploader.largeFileWarning.closeAriaLabel = Close
# Accessible label for the list of oversize files inside the warning modal
be.contentUploader.largeFileWarning.fileListAriaLabel = Files exceeding the upload size limit
# Heading for the warning modal shown when one or more files exceed the upload size limit
be.contentUploader.largeFileWarning.heading = {count, plural, one {File} other {Files}} Can't Be Uploaded
# Label for the button that proceeds with uploading only the files that are within the plan size limit
be.contentUploader.largeFileWarning.uploadTheRestButton = Upload the Rest
# Label for copy action.
be.copy = Copy
# Label for create action.
Expand Down
138 changes: 133 additions & 5 deletions src/elements/content-uploader/ContentUploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { UploadsManager as UploadsManagerBP } from '@box/uploads-manager';
import { TooltipProvider } from '@box/blueprint-web';
import { AxiosRequestConfig, AxiosResponse } from 'axios';
import CancelAllUploadsModal from './CancelAllUploadsModal';
import LargeFileWarningModal from './LargeFileWarningModal';
import DroppableContent from './DroppableContent';
import Footer from './Footer';
import UploadsManager from './UploadsManager';
Expand Down Expand Up @@ -115,13 +116,15 @@ export interface ContentUploaderProps {
enableModernizedUploads?: boolean;
isExpanded?: boolean;
onToggle?: (isExpanded: boolean) => void;
maxFileSize?: number;

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.

isn't it coming from the backend in a string?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We get an error about the exceeding size limit from the upload API (though without the specific size limit), but the point of this PR is to check the size on the client side and prevent the upload if it's exceeding the limit.

}

type ModernizedPanelState = 'hidden' | 'shown' | 'dismissing';

type State = {
errorCode?: string;
isCancelAllModalOpen: boolean;
isLargeFileWarningModalOpen: boolean;
isUploadsManagerExpanded: boolean;
itemIds: Object;
items: UploadItem[];
Expand Down Expand Up @@ -209,6 +212,7 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
errorCode: '',
itemIds: {},
isCancelAllModalOpen: false,
isLargeFileWarningModalOpen: false,
isUploadsManagerExpanded: false,
modernizedPanelState: 'hidden',
};
Expand All @@ -234,7 +238,7 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
const { files, isPrepopulateFilesEnabled } = this.props;
// isPrepopulateFilesEnabled is a prop used to pre-populate files without clicking upload button.
if (isPrepopulateFilesEnabled && files && files.length > 0) {
this.addFilesToUploadQueue(files, this.upload);
this.addFilesToUploadQueue(files, this.maybeUpload);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}

Expand Down Expand Up @@ -277,6 +281,10 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
item => item.status === STATUS_COMPLETE || item.status === STATUS_CANCELED,
);

if (!hasItemsInUploadQueue && modernizedPanelState === 'shown') {
this.setState({ modernizedPanelState: 'hidden' });
}

if (hasItemsInUploadQueue && modernizedPanelState === 'hidden') {
this.showModernizedPanel();
}
Expand Down Expand Up @@ -730,7 +738,7 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
const { view } = this.state;
// Automatically start upload if other files are being uploaded
if (view === VIEW_UPLOAD_IN_PROGRESS) {
this.upload();
this.maybeUpload();
}
});
};
Expand Down Expand Up @@ -1589,6 +1597,103 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
this.finalizeModernizedDismiss();
};

/**
* Returns pending upload items that exceed the configured max file size.
*
* @private
* @return {UploadItem[]}
*/
getOversizePendingItems = (): UploadItem[] => {
const { maxFileSize } = this.props;

if (!maxFileSize) {
return [];
}

return this.itemsRef.current.filter(
item => item.status === STATUS_PENDING && item.file && item.file.size > maxFileSize,
);
};

/**
* Returns the number of pending upload items that are eligible to upload.
*
* @private
* @return {number}
*/
getEligiblePendingCount = (): number => {
const { maxFileSize } = this.props;

return this.itemsRef.current.filter(item => {
if (item.status !== STATUS_PENDING) {
return false;
}

if (!item.file || !maxFileSize) {
return true;
}

return item.file.size <= maxFileSize;
}).length;
};

/**
* Starts upload immediately when no oversize files are present; otherwise opens
* the large file warning modal and waits for user confirmation.
*
* @private
* @return {void}
*/
maybeUpload = (): void => {
const { maxFileSize, enableModernizedUploads } = this.props;

if (!maxFileSize || !enableModernizedUploads) {
this.upload();
return;
}

const oversizeItems = this.getOversizePendingItems();

if (oversizeItems.length === 0) {
this.upload();
return;
}

if (!this.state.isLargeFileWarningModalOpen) {
this.setState({ isLargeFileWarningModalOpen: true });
}
};

/**
* Removes oversize pending items and starts uploading the remaining eligible items.
*
* @private
* @return {void}
*/
handleLargeFileWarningUploadRest = (): void => {
const oversizeItems = this.getOversizePendingItems();

oversizeItems.forEach(item => this.removeFileFromUploadQueue(item));

this.setState({ isLargeFileWarningModalOpen: false }, () => {
this.upload();
});
};

/**
* Cancels the upload attempt by removing all pending items from the queue.
*
* @private
* @return {void}
*/
handleLargeFileWarningCancel = (): void => {
const pendingItems = this.itemsRef.current.filter(item => item.status === STATUS_PENDING);

pendingItems.forEach(item => this.removeFileFromUploadQueue(item));

this.setState({ isLargeFileWarningModalOpen: false });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

/**
* Adds file to the upload queue and starts upload immediately
*
Expand All @@ -1600,8 +1705,8 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
files?: Array<UploadFileWithAPIOptions | File>,
dataTransferItems?: Array<DataTransferItem | UploadDataTransferItemWithAPIOptions>,
): void => {
this.addFilesToUploadQueue(files, this.upload);
this.addDataTransferItemsToUploadQueue(dataTransferItems, this.upload);
this.addFilesToUploadQueue(files, this.maybeUpload);
this.addDataTransferItemsToUploadQueue(dataTransferItems, this.maybeUpload);
};

/**
Expand Down Expand Up @@ -1629,15 +1734,29 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
rootFolderId,
theme,
useUploadsManager,
maxFileSize,
}: ContentUploaderProps = this.props;
const { view, items, errorCode, isCancelAllModalOpen, modernizedPanelState }: State = this.state;
const {
view,
items,
errorCode,
isCancelAllModalOpen,
isLargeFileWarningModalOpen,
modernizedPanelState,
}: State = this.state;
const isUploadsManagerExpanded = this.getIsExpanded();
const isEmpty = items.length === 0;
const isVisible = !isEmpty || !!isDraggingItemsToUploadsManager;

const hasFiles = items.length !== 0;
const isLoading = items.some(item => item.status === STATUS_IN_PROGRESS);
const isDone = items.every(item => item.status === STATUS_COMPLETE || item.status === STATUS_STAGED);
const oversizePendingItems = this.getOversizePendingItems();
const oversizeFiles = oversizePendingItems.map(item => ({
name: item.name,
size: item.file?.size ?? 0,
}));
const eligiblePendingCount = this.getEligiblePendingCount();
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const styleClassName = classNames('bcu', className, {
'be-app-element': !useUploadsManager,
Expand Down Expand Up @@ -1679,6 +1798,15 @@ class ContentUploader extends Component<ContentUploaderProps, State> {
onConfirm={this.handleCancelAllConfirm}
onDismiss={this.handleCancelAllDismiss}
/>
<LargeFileWarningModal
eligibleCount={eligiblePendingCount}
isOpen={isLargeFileWarningModalOpen}
maxFileSize={maxFileSize}
onCancel={this.handleLargeFileWarningCancel}
onConfirm={this.handleLargeFileWarningUploadRest}
onUpgradeCTAClick={onUpgradeCTAClick}
oversizeFiles={oversizeFiles}
/>
</div>
</div>
);
Expand Down
69 changes: 69 additions & 0 deletions src/elements/content-uploader/LargeFileWarningModal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
.bcu-large-file-warning-modal {
&-body {
display: flex;
flex-direction: column;
gap: var(--space-4);
}

&-description {
margin: 0;
}

&-upgradeLink {
background: none;
border: 0;
color: var(--text-cta-link);
cursor: pointer;
font: inherit;
font-weight: var(--font-weights-bold);
padding: 0;
text-decoration: none;

&:hover,
&:focus-visible {
color: var(--text-cta-link-hover);
text-decoration: underline;
}
}

&-fileListContainer {
border: var(--border-1) solid var(--border-card-border);
border-radius: var(--radius-4);
max-height: 140px;
overflow-y: auto;
padding: var(--space-5);
display: flex;
}

&-fileListInner {
position: relative;
width: 100%;
}

&-fileListRow {
align-items: center;
column-gap: var(--space-3);
display: flex;
justify-content: space-between;
left: 0;
min-height: 20px;
position: absolute;
top: 0;
width: 100%;
}

&-fileName {
flex: 1 1 auto;
font-weight: var(--font-weights-semibold);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

&-fileSize {
color: var(--text-text-on-light-secondary);
flex: 0 0 auto;
white-space: nowrap;
}
}
Loading
Loading