diff --git a/src/elements/content-uploader/ContentUploader.tsx b/src/elements/content-uploader/ContentUploader.tsx index 89dc29a610..714af473b2 100644 --- a/src/elements/content-uploader/ContentUploader.tsx +++ b/src/elements/content-uploader/ContentUploader.tsx @@ -13,6 +13,7 @@ import CancelAllUploadsModal from './CancelAllUploadsModal'; import LargeFileWarningModal from './LargeFileWarningModal'; import DroppableContent from './DroppableContent'; import Footer from './Footer'; +import ModernizedUploadsManagerDropZone from './ModernizedUploadsManagerDropZone'; import UploadsManager from './UploadsManager'; import { getUploadItemKey, mapToModernizedUploadItems } from './utils/mapToModernizedUploadItem'; import './ModernizedUploadsManagerPanel.scss'; @@ -76,6 +77,7 @@ export interface ContentUploaderProps { dataTransferItems: Array; fileLimit: number; files?: Array; + canDropOnUploadsManager?: boolean; isDraggingItemsToUploadsManager?: boolean; isFolderUploadEnabled: boolean; isLarge: boolean; @@ -172,6 +174,7 @@ class ContentUploader extends Component { clientName: CLIENT_NAME_CONTENT_UPLOADER, dataTransferItems: [], files: [], + canDropOnUploadsManager: false, fileLimit: FILE_LIMIT_DEFAULT, isDraggingItemsToUploadsManager: false, isFolderUploadEnabled: false, @@ -652,7 +655,8 @@ class ContentUploader extends Component { files: Array, itemUpdateCallback: Function, ) => { - const { rootFolderId } = this.props; + const { enableModernizedUploads, rootFolderId, useUploadsManager } = this.props; + const shouldAddRootFolderIdToUploadOptions = enableModernizedUploads && useUploadsManager; // Convert files from the file API to upload items const newItems = files.map(file => { @@ -678,7 +682,9 @@ class ContentUploader extends Component { }; if (uploadAPIOptions) { - uploadItem.options = uploadAPIOptions; + uploadItem.options = shouldAddRootFolderIdToUploadOptions + ? { folderId: rootFolderId, ...uploadAPIOptions } + : uploadAPIOptions; } this.itemIdsRef.current[getFileId(uploadItem, rootFolderId)] = true; @@ -1741,6 +1747,7 @@ class ContentUploader extends Component { render() { const { className, + canDropOnUploadsManager, enableModernizedUploads, fileLimit, isDraggingItemsToUploadsManager = false, @@ -1794,8 +1801,12 @@ class ContentUploader extends Component { return (
- {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} -
+ this.addDroppedItemsToUploadQueue(droppedItems, this.upload) + } + allowedTypes={['Files']} + isDropEnabled={canDropOnUploadsManager} className={classNames('bcu-modernized-panel', { visible: modernizedPanelState === 'shown', dismissing: modernizedPanelState === 'dismissing', @@ -1833,7 +1844,7 @@ class ContentUploader extends Component { onUpgradeCTAClick={onUpgradeCTAClick} oversizeFiles={oversizeFiles} /> -
+
); } diff --git a/src/elements/content-uploader/ModernizedUploadsManagerDropZone.tsx b/src/elements/content-uploader/ModernizedUploadsManagerDropZone.tsx new file mode 100644 index 0000000000..eea66c5f2d --- /dev/null +++ b/src/elements/content-uploader/ModernizedUploadsManagerDropZone.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import omit from 'lodash/omit'; + +import makeDroppable from '../common/droppable'; +import type { DOMStringList } from '../../common/types/core'; + +export interface ModernizedUploadsManagerDropZoneProps extends React.HTMLAttributes { + addDataTransferItemsToUploadQueue: (droppedItems: DataTransfer) => void; + allowedTypes: Array; + children: React.ReactNode; + className: string; + isDropEnabled?: boolean; +} + +interface DroppableStateProps { + canDrop?: boolean; + isDragging?: boolean; + isOver?: boolean; +} + +const dropDefinition = { + dropValidator: ( + { allowedTypes, isDropEnabled = true }: ModernizedUploadsManagerDropZoneProps, + { types }: { types: Array | DOMStringList }, + ) => { + if (!isDropEnabled) { + return false; + } + + if (types instanceof Array) { + return Array.from(types).some(type => allowedTypes.indexOf(type) > -1); + } + + const allowedList = allowedTypes.filter(allowed => types.contains(allowed)); + return allowedList.length > 0; + }, + + onDrop: (event: DragEvent, { addDataTransferItemsToUploadQueue }: ModernizedUploadsManagerDropZoneProps) => { + const { dataTransfer } = event; + + if (!dataTransfer) { + return; + } + + addDataTransferItemsToUploadQueue(dataTransfer); + }, +} as const; + +const ModernizedUploadsManagerDropZoneComponent = React.forwardRef< + HTMLDivElement, + ModernizedUploadsManagerDropZoneProps & DroppableStateProps +>((props, ref) => { + const { children } = props; + const htmlProps: React.HTMLAttributes = omit(props, [ + 'addDataTransferItemsToUploadQueue', + 'allowedTypes', + 'canDrop', + 'children', + 'isDragging', + 'isDropEnabled', + 'isOver', + ]); + + return ( +
+ {children} +
+ ); +}); + +const ModernizedUploadsManagerDropZone = makeDroppable(dropDefinition)(ModernizedUploadsManagerDropZoneComponent); + +export default ModernizedUploadsManagerDropZone; diff --git a/src/elements/content-uploader/__tests__/ContentUploader.test.js b/src/elements/content-uploader/__tests__/ContentUploader.test.js index 82e1e22bfc..354669af96 100644 --- a/src/elements/content-uploader/__tests__/ContentUploader.test.js +++ b/src/elements/content-uploader/__tests__/ContentUploader.test.js @@ -3,10 +3,12 @@ import { shallow } from 'enzyme'; import { UploadsManager as UploadsManagerBP } from '@box/uploads-manager'; import * as UploaderUtils from '../../../utils/uploads'; import Browser from '../../../utils/Browser'; +import { fireEvent, render, screen } from '../../../test-utils/testing-library'; import { ContentUploaderComponent, CHUNKED_UPLOAD_MIN_SIZE_BYTES } from '../ContentUploader'; import Footer from '../Footer'; import UploadsManager from '../UploadsManager'; import DroppableContent from '../DroppableContent'; +import ModernizedUploadsManagerDropZone from '../ModernizedUploadsManagerDropZone'; import { ERROR_CODE_ITEM_NAME_IN_USE, STATUS_PENDING, @@ -159,6 +161,33 @@ describe('elements/content-uploader/ContentUploader', () => { expect(instance.itemIdsRef.current).toEqual(expected); }); + test('should add rootFolderId to raw file upload options for the modernized uploads manager', () => { + const files = [{ name: 'yoyo', size: 1000 }]; + const wrapper = getWrapper({ + enableModernizedUploads: true, + rootFolderId: '12345', + useUploadsManager: true, + }); + wrapper.instance().upload = jest.fn(); + + wrapper.setProps({ files }); + + expect(wrapper.state().items[0].options.folderId).toBe('12345'); + }); + + test('should not add rootFolderId to raw file upload options outside the modernized uploads manager', () => { + const files = [{ name: 'yoyo', size: 1000 }]; + const wrapper = getWrapper({ + rootFolderId: '12345', + useUploadsManager: true, + }); + wrapper.instance().upload = jest.fn(); + + wrapper.setProps({ files }); + + expect(wrapper.state().items[0].options.folderId).toBeUndefined(); + }); + test('should handle accepting package "files" separate from folders', () => { const mockFile = { name: 'hi' }; Browser.isSafari = jest.fn(() => true); @@ -964,6 +993,127 @@ describe('elements/content-uploader/ContentUploader', () => { }); }); + describe('ModernizedUploadsManagerDropZone', () => { + const renderModernizedUploadsManagerDropZone = props => + render( + +
+ , + ); + + test('prevents browser navigation and queues files dropped on the modernized panel', () => { + const addDataTransferItemsToUploadQueue = jest.fn(); + const dataTransfer = { + items: ['file-item'], + types: ['Files'], + }; + const dragEnterEvent = { + dataTransfer, + preventDefault: jest.fn(), + }; + const dropEvent = { + dataTransfer, + preventDefault: jest.fn(), + }; + const wrapper = shallow( + +
+ , + ); + + wrapper.instance().handleDragEnter(dragEnterEvent); + wrapper.instance().handleDrop(dropEvent); + + expect(dragEnterEvent.preventDefault).toHaveBeenCalled(); + expect(dropEvent.preventDefault).toHaveBeenCalled(); + expect(addDataTransferItemsToUploadQueue).toHaveBeenCalledWith(dataTransfer); + }); + + test('prevents browser navigation without queueing when isDropEnabled is false', () => { + const addDataTransferItemsToUploadQueue = jest.fn(); + const dataTransfer = { + items: ['file-item'], + types: ['Files'], + }; + const dragEnterEvent = { + dataTransfer, + preventDefault: jest.fn(), + }; + const dropEvent = { + dataTransfer, + preventDefault: jest.fn(), + }; + const wrapper = shallow( + +
+ , + ); + + wrapper.instance().handleDragEnter(dragEnterEvent); + wrapper.instance().handleDrop(dropEvent); + + expect(dragEnterEvent.preventDefault).toHaveBeenCalled(); + expect(dropEvent.preventDefault).toHaveBeenCalled(); + expect(addDataTransferItemsToUploadQueue).not.toHaveBeenCalled(); + }); + + test('does not forward droppable state props to the modernized panel DOM element', () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); + const dataTransfer = { + items: ['file-item'], + types: ['Files'], + }; + let consoleErrors = ''; + + try { + renderModernizedUploadsManagerDropZone(); + + fireEvent.dragEnter(screen.getByTestId('bcu-modernized-panel'), { dataTransfer }); + + consoleErrors = consoleError.mock.calls.flat().join('\n'); + } finally { + consoleError.mockRestore(); + } + + expect(consoleErrors).not.toContain('isDragging'); + expect(consoleErrors).not.toContain('isOver'); + }); + + test('does not queue files dropped on the modernized panel when isDropEnabled is false', () => { + const addDataTransferItemsToUploadQueue = jest.fn(); + const dataTransfer = { + items: ['file-item'], + types: ['Files'], + }; + + renderModernizedUploadsManagerDropZone({ + addDataTransferItemsToUploadQueue, + isDropEnabled: false, + }); + + const modernizedPanel = screen.getByTestId('bcu-modernized-panel'); + fireEvent.dragEnter(modernizedPanel, { dataTransfer }); + fireEvent.drop(modernizedPanel, { dataTransfer }); + + expect(addDataTransferItemsToUploadQueue).not.toHaveBeenCalled(); + }); + }); + describe('render()', () => { describe('enableModernizedUploads', () => { test('should render legacy UploadsManager when enableModernizedUploads is false and useUploadsManager is true', () => { @@ -985,6 +1135,13 @@ describe('elements/content-uploader/ContentUploader', () => { expect(wrapper.find(UploadsManagerBP)).toHaveLength(1); expect(wrapper.find(UploadsManager)).toHaveLength(0); expect(wrapper.find(DroppableContent)).toHaveLength(0); + expect(wrapper.find(ModernizedUploadsManagerDropZone)).toHaveLength(1); + }); + + test('should disable dropping on the modernized panel when modernized drop props are omitted', () => { + const wrapper = getWrapper({ enableModernizedUploads: true }); + + expect(wrapper.find(ModernizedUploadsManagerDropZone).prop('isDropEnabled')).toBe(false); }); test('should render modernized UploadsManagerBP even when useUploadsManager is true', () => {