diff --git a/i18n/en-US.properties b/i18n/en-US.properties index f6a0cfac0e..f4a7999b83 100644 --- a/i18n/en-US.properties +++ b/i18n/en-US.properties @@ -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. Upgrade your plan 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. diff --git a/src/elements/content-uploader/ContentUploader.tsx b/src/elements/content-uploader/ContentUploader.tsx index 04af3e3720..f982683924 100644 --- a/src/elements/content-uploader/ContentUploader.tsx +++ b/src/elements/content-uploader/ContentUploader.tsx @@ -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'; @@ -113,8 +114,10 @@ export interface ContentUploaderProps { uploadHost: string; useUploadsManager?: boolean; enableModernizedUploads?: boolean; + isUpgradeModalEnabled?: boolean; isExpanded?: boolean; onToggle?: (isExpanded: boolean) => void; + maxFileSize?: number; } type ModernizedPanelState = 'hidden' | 'shown' | 'dismissing'; @@ -122,6 +125,7 @@ type ModernizedPanelState = 'hidden' | 'shown' | 'dismissing'; type State = { errorCode?: string; isCancelAllModalOpen: boolean; + isLargeFileWarningModalOpen: boolean; isUploadsManagerExpanded: boolean; itemIds: Object; items: UploadItem[]; @@ -192,6 +196,7 @@ class ContentUploader extends Component { uploadHost: DEFAULT_HOSTNAME_UPLOAD, useUploadsManager: false, enableModernizedUploads: false, + isUpgradeModalEnabled: false, }; /** @@ -209,6 +214,7 @@ class ContentUploader extends Component { errorCode: '', itemIds: {}, isCancelAllModalOpen: false, + isLargeFileWarningModalOpen: false, isUploadsManagerExpanded: false, modernizedPanelState: 'hidden', }; @@ -277,6 +283,10 @@ class ContentUploader extends Component { item => item.status === STATUS_COMPLETE || item.status === STATUS_CANCELED, ); + if (!hasItemsInUploadQueue && modernizedPanelState === 'shown') { + this.setState({ modernizedPanelState: 'hidden' }); + } + if (hasItemsInUploadQueue && modernizedPanelState === 'hidden') { this.showModernizedPanel(); } @@ -831,6 +841,45 @@ class ContentUploader extends Component { }); }; + /** + * Removes multiple items from the upload queue in a single batch update. + * + * @param {UploadItem[]} itemsToRemove - Items to remove + * @return {void} + */ + removeItemsFromUploadQueue = (itemsToRemove: UploadItem[]): void => { + if (itemsToRemove.length === 0) { + return; + } + + const { onCancel, rootFolderId } = this.props; + + itemsToRemove.forEach(item => { + item.api.cancel(); + + if (item.file) { + const simpleFileId = item.file.name; + const fileWithOptions = item.options ? { file: item.file, options: item.options } : item.file; + const fullFileId = getFileId(fileWithOptions, rootFolderId); + + delete this.itemIdsRef.current[simpleFileId]; + delete this.itemIdsRef.current[fullFileId]; + } else if (item.dedupeKey) { + delete this.itemIdsRef.current[item.dedupeKey]; + } + }); + + const preservedItems = this.itemsRef.current.filter(item => !itemsToRemove.includes(item)); + + onCancel(itemsToRemove); + + this.setState({ + errorCode: '', + itemIds: { ...this.itemIdsRef.current }, + }); + this.updateViewAndCollection(preservedItems); + }; + /** * Aborts uploads in progress and clears upload list. * @@ -856,6 +905,20 @@ class ContentUploader extends Component { * @return {void} */ upload = () => { + const { enableModernizedUploads, isUpgradeModalEnabled, maxFileSize } = this.props; + + if ( + maxFileSize && + enableModernizedUploads && + isUpgradeModalEnabled && + this.getOversizePendingItems().length > 0 + ) { + if (!this.state.isLargeFileWarningModalOpen) { + this.setState({ isLargeFileWarningModalOpen: true }); + } + return; + } + this.itemsRef.current.forEach(uploadItem => { if (uploadItem.status === STATUS_PENDING) { this.uploadFile(uploadItem); @@ -1589,6 +1652,74 @@ class ContentUploader extends Component { 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; + }; + + /** + * Removes oversize pending items and starts uploading the remaining eligible items. + * + * @private + * @return {void} + */ + handleLargeFileWarningUploadRest = (): void => { + this.removeItemsFromUploadQueue(this.getOversizePendingItems()); + + 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); + + this.removeItemsFromUploadQueue(pendingItems); + + this.setState({ isLargeFileWarningModalOpen: false }); + }; + /** * Adds file to the upload queue and starts upload immediately * @@ -1629,8 +1760,16 @@ class ContentUploader extends Component { 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; @@ -1638,6 +1777,12 @@ class ContentUploader extends Component { 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(); const styleClassName = classNames('bcu', className, { 'be-app-element': !useUploadsManager, @@ -1679,6 +1824,15 @@ class ContentUploader extends Component { onConfirm={this.handleCancelAllConfirm} onDismiss={this.handleCancelAllDismiss} /> + ); diff --git a/src/elements/content-uploader/LargeFileWarningModal.scss b/src/elements/content-uploader/LargeFileWarningModal.scss new file mode 100644 index 0000000000..704fdf3691 --- /dev/null +++ b/src/elements/content-uploader/LargeFileWarningModal.scss @@ -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; + } +} diff --git a/src/elements/content-uploader/LargeFileWarningModal.tsx b/src/elements/content-uploader/LargeFileWarningModal.tsx new file mode 100644 index 0000000000..eddc05d45d --- /dev/null +++ b/src/elements/content-uploader/LargeFileWarningModal.tsx @@ -0,0 +1,145 @@ +import * as React from 'react'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { Modal } from '@box/blueprint-web'; + +import getFileSize from '../../utils/getFileSize'; +import messages from './messages'; + +import './LargeFileWarningModal.scss'; + +export type OversizeFile = { + name: string; + size: number; +}; + +export interface LargeFileWarningModalProps { + eligibleCount: number; + isOpen: boolean; + maxFileSize?: number; + onCancel: () => void; + onConfirm: () => void; + onUpgradeCTAClick?: () => void; + oversizeFiles: ReadonlyArray; +} + +const LargeFileWarningModal = ({ + eligibleCount, + isOpen, + maxFileSize, + onCancel, + onConfirm, + onUpgradeCTAClick, + oversizeFiles, +}: LargeFileWarningModalProps) => { + const intl = useIntl(); + const oversizeCount = oversizeFiles.length; + const maxFileSizeLabel = maxFileSize ? getFileSize(maxFileSize, intl.locale) : null; + const scrollRef = React.useRef(null); + const rowVirtualizer = useVirtualizer({ + count: oversizeFiles.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => 20, + overscan: 1, + gap: 12, + }); + + const handleOpenChange = (open: boolean) => { + if (!open) { + onCancel(); + } + }; + + const handleUpgradeCTAClick = (e: React.MouseEvent) => { + e.preventDefault(); + onUpgradeCTAClick?.(); + onCancel(); + }; + + const renderFileList = () => { + return ( +
+
+ {rowVirtualizer.getVirtualItems().map(item => { + const file = oversizeFiles[item.index]; + + return ( +
+ + {file.name} + + + {getFileSize(file.size, intl.locale)} + +
+ ); + })} +
+
+ ); + }; + + return ( + + + + {intl.formatMessage(messages.largeFileWarningHeading, { count: oversizeCount })} + + + +
+

+ + onUpgradeCTAClick ? ( + + ) : ( + {chunks} + ), + }} + /> +

+ {renderFileList()} +
+
+
+ + + {intl.formatMessage(messages.largeFileWarningCancelButton)} + + + {intl.formatMessage(messages.largeFileWarningUploadTheRestButton)} + + + +
+
+ ); +}; + +export default LargeFileWarningModal; diff --git a/src/elements/content-uploader/__tests__/ContentUploader.test.js b/src/elements/content-uploader/__tests__/ContentUploader.test.js index 18277aab46..599f6198b6 100644 --- a/src/elements/content-uploader/__tests__/ContentUploader.test.js +++ b/src/elements/content-uploader/__tests__/ContentUploader.test.js @@ -1697,4 +1697,208 @@ describe('elements/content-uploader/ContentUploader', () => { expect(instance.modernizedDismissTimer).toBeNull(); }); }); + + describe('upload() large-file gate', () => { + const makeFileWithSize = (name, size) => new File([new Uint8Array(size)], name, { type: 'text/plain' }); + + const makePendingFileItem = (name, size) => ({ + api: {}, + extension: 'txt', + file: makeFileWithSize(name, size), + name, + progress: 0, + size, + status: STATUS_PENDING, + }); + + test('should upload immediately when maxFileSize is not configured', () => { + const wrapper = getWrapper(); + const instance = wrapper.instance(); + instance.uploadFile = jest.fn(); + instance.itemsRef.current = [makePendingFileItem('large.txt', 200)]; + + instance.upload(); + + expect(instance.uploadFile).toHaveBeenCalledTimes(1); + expect(wrapper.state('isLargeFileWarningModalOpen')).toBe(false); + }); + + test('should upload immediately when all pending files are within maxFileSize', () => { + const wrapper = getWrapper({ maxFileSize: 100 }); + const instance = wrapper.instance(); + instance.uploadFile = jest.fn(); + instance.itemsRef.current = [makePendingFileItem('small.txt', 50)]; + + instance.upload(); + + expect(instance.uploadFile).toHaveBeenCalledTimes(1); + expect(wrapper.state('isLargeFileWarningModalOpen')).toBe(false); + }); + + test('should open the large file warning modal when any pending file exceeds maxFileSize', () => { + const wrapper = getWrapper({ + enableModernizedUploads: true, + isUpgradeModalEnabled: true, + maxFileSize: 100, + }); + const instance = wrapper.instance(); + instance.uploadFile = jest.fn(); + instance.itemsRef.current = [makePendingFileItem('small.txt', 50), makePendingFileItem('large.txt', 200)]; + + instance.upload(); + + expect(instance.uploadFile).not.toHaveBeenCalled(); + expect(wrapper.state('isLargeFileWarningModalOpen')).toBe(true); + }); + + test('should NOT open the modal when isUpgradeModalEnabled is false even with oversize files', () => { + const wrapper = getWrapper({ + enableModernizedUploads: true, + isUpgradeModalEnabled: false, + maxFileSize: 100, + }); + const instance = wrapper.instance(); + instance.uploadFile = jest.fn(); + instance.itemsRef.current = [makePendingFileItem('large.txt', 200)]; + + instance.upload(); + + expect(instance.uploadFile).toHaveBeenCalledTimes(1); + expect(wrapper.state('isLargeFileWarningModalOpen')).toBe(false); + }); + }); + + describe('addToQueue()', () => { + const makeFileWithSize = (name, size) => new File([new Uint8Array(size)], name, { type: 'text/plain' }); + + test('should not auto-upload pending items when adding a batch with oversize files during an in-progress upload', () => { + const wrapper = getWrapper({ + enableModernizedUploads: true, + isUpgradeModalEnabled: true, + maxFileSize: 100, + }); + const instance = wrapper.instance(); + const inProgressItem = { + api: {}, + extension: 'txt', + file: makeFileWithSize('uploading.txt', 50), + name: 'uploading.txt', + progress: 50, + size: 50, + status: STATUS_IN_PROGRESS, + }; + const oversizeItem = { + api: {}, + extension: 'txt', + file: makeFileWithSize('large.txt', 200), + name: 'large.txt', + progress: 0, + size: 200, + status: STATUS_PENDING, + }; + const eligibleItem = { + api: {}, + extension: 'txt', + file: makeFileWithSize('small.txt', 50), + name: 'small.txt', + progress: 0, + size: 50, + status: STATUS_PENDING, + }; + + instance.itemsRef.current = [inProgressItem]; + wrapper.setState({ view: VIEW_UPLOAD_IN_PROGRESS }); + instance.uploadFile = jest.fn(); + + instance.addToQueue([oversizeItem, eligibleItem], instance.upload); + + expect(wrapper.state('isLargeFileWarningModalOpen')).toBe(true); + expect(instance.uploadFile).not.toHaveBeenCalled(); + expect(eligibleItem.status).toBe(STATUS_PENDING); + }); + }); + + describe('handleLargeFileWarningUploadRest()', () => { + test('should remove oversize pending items and start upload', () => { + const wrapper = getWrapper({ maxFileSize: 100 }); + const instance = wrapper.instance(); + const oversizeItem = { + api: { cancel: jest.fn() }, + extension: 'txt', + file: new File([new Uint8Array(200)], 'large.txt', { type: 'text/plain' }), + name: 'large.txt', + progress: 0, + size: 200, + status: STATUS_PENDING, + }; + const eligibleItem = { + api: { cancel: jest.fn() }, + extension: 'txt', + file: new File([new Uint8Array(50)], 'small.txt', { type: 'text/plain' }), + name: 'small.txt', + progress: 0, + size: 50, + status: STATUS_PENDING, + }; + + instance.itemsRef.current = [oversizeItem, eligibleItem]; + instance.upload = jest.fn(); + wrapper.setState({ + isLargeFileWarningModalOpen: true, + items: [oversizeItem, eligibleItem], + }); + + instance.handleLargeFileWarningUploadRest(); + + expect(oversizeItem.api.cancel).toHaveBeenCalledTimes(1); + expect(eligibleItem.api.cancel).not.toHaveBeenCalled(); + expect(wrapper.state('items')).toEqual([eligibleItem]); + expect(wrapper.state('isLargeFileWarningModalOpen')).toBe(false); + expect(instance.upload).toHaveBeenCalledTimes(1); + }); + }); + + describe('handleLargeFileWarningCancel()', () => { + test('should remove all pending items and close the modal', () => { + const onCancel = jest.fn(); + const wrapper = getWrapper({ maxFileSize: 100, onCancel, rootFolderId: '0' }); + const instance = wrapper.instance(); + const oversizeItem = { + api: { cancel: jest.fn() }, + extension: 'txt', + file: new File([new Uint8Array(200)], 'large.txt', { type: 'text/plain' }), + name: 'large.txt', + progress: 0, + size: 200, + status: STATUS_PENDING, + }; + const eligibleItem = { + api: { cancel: jest.fn() }, + extension: 'txt', + file: new File([new Uint8Array(50)], 'small.txt', { type: 'text/plain' }), + name: 'small.txt', + progress: 0, + size: 50, + status: STATUS_PENDING, + }; + + instance.itemsRef.current = [oversizeItem, eligibleItem]; + instance.itemIdsRef.current = { 'large.txt': true, 'small.txt': true }; + wrapper.setState({ + isLargeFileWarningModalOpen: true, + items: [oversizeItem, eligibleItem], + }); + + instance.handleLargeFileWarningCancel(); + + expect(oversizeItem.api.cancel).toHaveBeenCalledTimes(1); + expect(eligibleItem.api.cancel).toHaveBeenCalledTimes(1); + expect(onCancel).toHaveBeenCalledTimes(1); + expect(onCancel).toHaveBeenCalledWith([oversizeItem, eligibleItem]); + expect(instance.itemIdsRef.current['large.txt']).toBeUndefined(); + expect(instance.itemIdsRef.current['small.txt']).toBeUndefined(); + expect(wrapper.state('items')).toEqual([]); + expect(wrapper.state('isLargeFileWarningModalOpen')).toBe(false); + }); + }); }); diff --git a/src/elements/content-uploader/__tests__/LargeFileWarningModal.test.tsx b/src/elements/content-uploader/__tests__/LargeFileWarningModal.test.tsx new file mode 100644 index 0000000000..a634970940 --- /dev/null +++ b/src/elements/content-uploader/__tests__/LargeFileWarningModal.test.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import { render, screen, userEvent } from '../../../test-utils/testing-library'; +import LargeFileWarningModal, { type LargeFileWarningModalProps } from '../LargeFileWarningModal'; + +jest.mock('@tanstack/react-virtual', () => ({ + useVirtualizer: ({ count }: { count: number }) => ({ + getTotalSize: () => count * 20, + getVirtualItems: () => + Array.from({ length: count }, (_, index) => ({ + index, + key: index, + start: index * 20, + end: (index + 1) * 20, + size: 20, + lane: 0, + })), + }), +})); + +const renderModal = (props: Partial = {}) => { + const defaultProps: LargeFileWarningModalProps = { + eligibleCount: 1, + isOpen: true, + maxFileSize: 100, + onCancel: jest.fn(), + onConfirm: jest.fn(), + oversizeFiles: [{ name: 'large-file.txt', size: 200 }], + ...props, + }; + render(); + return defaultProps; +}; + +describe('elements/content-uploader/LargeFileWarningModal', () => { + test('renders singular heading and oversize file details when one file is oversize', async () => { + renderModal(); + expect(await screen.findByText("File Can't Be Uploaded")).toBeInTheDocument(); + expect(screen.getByText('large-file.txt')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Upload the Rest' })).toBeEnabled(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + }); + + test('renders plural heading when multiple files are oversize', async () => { + renderModal({ + oversizeFiles: [ + { name: 'large-a.txt', size: 200 }, + { name: 'large-b.txt', size: 300 }, + ], + }); + expect(await screen.findByText("Files Can't Be Uploaded")).toBeInTheDocument(); + }); + + test('disables Upload the Rest when no eligible files remain', async () => { + renderModal({ eligibleCount: 0 }); + expect(await screen.findByRole('button', { name: 'Upload the Rest' })).toBeDisabled(); + }); + + test('calls onConfirm when Upload the Rest is clicked', async () => { + const props = renderModal(); + const user = userEvent(); + await user.click(await screen.findByRole('button', { name: 'Upload the Rest' })); + expect(props.onConfirm).toHaveBeenCalledTimes(1); + }); + + test('calls onCancel when Cancel is clicked', async () => { + const props = renderModal(); + const user = userEvent(); + await user.click(await screen.findByRole('button', { name: 'Cancel' })); + expect(props.onCancel).toHaveBeenCalledTimes(1); + }); + + test('calls onUpgradeCTAClick and onCancel when upgrade link is clicked', async () => { + const onUpgradeCTAClick = jest.fn(); + const onCancel = jest.fn(); + renderModal({ onUpgradeCTAClick, onCancel }); + const user = userEvent(); + await user.click(await screen.findByRole('button', { name: 'Upgrade your plan' })); + expect(onUpgradeCTAClick).toHaveBeenCalledTimes(1); + expect(onCancel).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/elements/content-uploader/messages.ts b/src/elements/content-uploader/messages.ts index 89f241b92a..bd04c86f75 100644 --- a/src/elements/content-uploader/messages.ts +++ b/src/elements/content-uploader/messages.ts @@ -26,6 +26,39 @@ const messages = defineMessages({ defaultMessage: 'Close cancel uploads dialog', description: 'Aria label for the close button on the cancel all uploads modal', }, + largeFileWarningHeading: { + id: 'be.contentUploader.largeFileWarning.heading', + defaultMessage: "{count, plural, one {File} other {Files}} Can't Be Uploaded", + description: 'Heading for the warning modal shown when one or more files exceed the upload size limit', + }, + largeFileWarningBody: { + id: 'be.contentUploader.largeFileWarning.body', + defaultMessage: + '{count, plural, one {This file exceeds} other {These files exceed}} the {maxFileSize} limit on your current plan. Upgrade your plan for larger uploads:', + description: + '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.', + }, + largeFileWarningCancelButton: { + id: 'be.contentUploader.largeFileWarning.cancelButton', + defaultMessage: 'Cancel', + description: 'Label for the button that cancels the upload attempt entirely', + }, + largeFileWarningUploadTheRestButton: { + id: 'be.contentUploader.largeFileWarning.uploadTheRestButton', + defaultMessage: 'Upload the Rest', + description: + 'Label for the button that proceeds with uploading only the files that are within the plan size limit', + }, + largeFileWarningCloseAriaLabel: { + id: 'be.contentUploader.largeFileWarning.closeAriaLabel', + defaultMessage: 'Close', + description: 'Accessible label for the close button in the large file warning modal', + }, + largeFileWarningFileListAriaLabel: { + id: 'be.contentUploader.largeFileWarning.fileListAriaLabel', + defaultMessage: 'Files exceeding the upload size limit', + description: 'Accessible label for the list of oversize files inside the warning modal', + }, }); export default messages;