From 506fada9b4c0c5b023e6817fff436a3f8ee9f206 Mon Sep 17 00:00:00 2001 From: Greg Wong Date: Mon, 27 Jan 2025 01:04:49 -0500 Subject: [PATCH] chore(content-explorer): Migrate ContentExplorer --- scripts/styleguide.config.js | 2 +- ...ContentExplorer.js => ContentExplorer.tsx} | 254 +++---- .../content-explorer/PreviewDialog.tsx | 7 +- src/elements/content-explorer/ShareDialog.tsx | 4 +- .../__tests__/ContentExplorer.test.js | 719 ------------------ .../__tests__/ContentExplorer.test.tsx | 498 ++++++++++++ .../{constants.js => constants.ts} | 0 .../content-explorer/{index.js => index.ts} | 1 - .../stories/__mocks__/mockMetadata.ts | 144 ++++ .../stories/__mocks__/mockRootFolder.ts | 522 ++++++++++++- .../stories/__mocks__/mockSubFolder.ts | 252 +++++- .../tests/ContentExplorer-visual.stories.js | 45 +- .../content-uploader/ContentUploader.tsx | 5 +- src/elements/index.js | 2 +- src/elements/wrappers/ContentExplorer.js | 1 + 15 files changed, 1562 insertions(+), 894 deletions(-) rename src/elements/content-explorer/{ContentExplorer.js => ContentExplorer.tsx} (91%) delete mode 100644 src/elements/content-explorer/__tests__/ContentExplorer.test.js create mode 100644 src/elements/content-explorer/__tests__/ContentExplorer.test.tsx rename src/elements/content-explorer/{constants.js => constants.ts} (100%) rename src/elements/content-explorer/{index.js => index.ts} (83%) create mode 100644 src/elements/content-explorer/stories/__mocks__/mockMetadata.ts diff --git a/scripts/styleguide.config.js b/scripts/styleguide.config.js index e40221498f..2d68162db7 100644 --- a/scripts/styleguide.config.js +++ b/scripts/styleguide.config.js @@ -12,7 +12,7 @@ const allSections = [ { name: 'Elements', components: () => [ - '../src/elements/content-explorer/ContentExplorer.js', + '../src/elements/content-explorer/ContentExplorer.tsx', '../src/elements/content-picker/ContentPicker.js', '../src/elements/content-preview/ContentPreview.js', '../src/elements/content-sharing/ContentSharing.js', diff --git a/src/elements/content-explorer/ContentExplorer.js b/src/elements/content-explorer/ContentExplorer.tsx similarity index 91% rename from src/elements/content-explorer/ContentExplorer.js rename to src/elements/content-explorer/ContentExplorer.tsx index 41c4848983..27b6f0ed34 100644 --- a/src/elements/content-explorer/ContentExplorer.js +++ b/src/elements/content-explorer/ContentExplorer.tsx @@ -1,9 +1,3 @@ -/** - * @flow - * @file Content Explorer Component - * @author Box - */ - import 'regenerator-runtime/runtime'; import React, { Component } from 'react'; import classNames from 'classnames'; @@ -14,6 +8,7 @@ import getProp from 'lodash/get'; import noop from 'lodash/noop'; import uniqueid from 'lodash/uniqueId'; import { TooltipProvider } from '@box/blueprint-web'; +import { AxiosRequestConfig, AxiosResponse } from 'axios'; import CreateFolderDialog from '../common/create-folder-dialog'; import UploadDialog from '../common/upload-dialog'; import Header from '../common/header'; @@ -22,7 +17,6 @@ import SubHeader from '../common/sub-header/SubHeader'; import makeResponsive from '../common/makeResponsive'; import openUrlInsideIframe from '../../utils/iframe'; import Internationalize from '../common/Internationalize'; -// $FlowFixMe TypeScript file import ThemingStyles from '../common/theming'; import API from '../../api'; import MetadataQueryAPIHelper from '../../features/metadata-based-view/MetadataQueryAPIHelper'; @@ -32,7 +26,6 @@ import ShareDialog from './ShareDialog'; import RenameDialog from './RenameDialog'; import DeleteConfirmationDialog from './DeleteConfirmationDialog'; import Content from './Content'; -// $FlowFixMe TypeScript file import { isThumbnailAvailable } from '../common/utils'; import { isFocusableElement, isInputElement, focus } from '../../utils/dom'; import { FILE_SHARED_LINK_FIELDS_TO_FETCH } from '../../utils/fields'; @@ -71,7 +64,6 @@ import { VIEW_MODE_GRID, } from '../../constants'; import type { ViewMode } from '../common/flowTypes'; -// $FlowFixMe TypeScript file import type { Theme } from '../common/theming'; import type { MetadataQuery, FieldsToShow } from '../../common/types/metadataQueries'; import type { MetadataFieldValue } from '../../common/types/metadata'; @@ -87,6 +79,8 @@ import type { BoxItemPermission, BoxItem, } from '../../common/types/core'; +import type { ContentPreviewProps } from '../content-preview'; +import type { ContentUploaderProps } from '../content-uploader'; import '../common/fonts.scss'; import '../common/base.scss'; @@ -96,96 +90,96 @@ import './ContentExplorer.scss'; const GRID_VIEW_MAX_COLUMNS = 7; const GRID_VIEW_MIN_COLUMNS = 1; -type Props = { - apiHost: string, - appHost: string, - autoFocus: boolean, - canCreateNewFolder: boolean, - canDelete: boolean, - canDownload: boolean, - canPreview: boolean, - canRename: boolean, - canSetShareAccess: boolean, - canShare: boolean, - canUpload: boolean, - className: string, - contentPreviewProps: ContentPreviewProps, - contentUploaderProps: ContentUploaderProps, - currentFolderId?: string, - defaultView: DefaultView, - features: FeatureConfig, - fieldsToShow?: FieldsToShow, - initialPage: number, - initialPageSize: number, - isLarge: boolean, - isMedium: boolean, - isSmall: boolean, - isTouch: boolean, - isVeryLarge: boolean, - language?: string, - logoUrl?: string, - measureRef?: Function, - messages?: StringMap, - metadataQuery?: MetadataQuery, - onCreate: Function, - onDelete: Function, - onDownload: Function, - onNavigate: Function, - onPreview: Function, - onRename: Function, - onSelect: Function, - onUpload: Function, - previewLibraryVersion: string, - requestInterceptor?: Function, - responseInterceptor?: Function, - rootFolderId: string, - sharedLink?: string, - sharedLinkPassword?: string, - sortBy: SortBy, - sortDirection: SortDirection, - staticHost: string, - staticPath: string, - theme?: Theme, - token: Token, - uploadHost: string, -}; +export interface ContentExplorerProps { + apiHost?: string; + appHost?: string; + autoFocus?: boolean; + canCreateNewFolder?: boolean; + canDelete?: boolean; + canDownload?: boolean; + canPreview?: boolean; + canRename?: boolean; + canSetShareAccess?: boolean; + canShare?: boolean; + canUpload?: boolean; + className?: string; + contentPreviewProps?: ContentPreviewProps; + contentUploaderProps?: ContentUploaderProps; + currentFolderId?: string; + defaultView?: DefaultView; + features?: FeatureConfig; + fieldsToShow?: FieldsToShow; + initialPage?: number; + initialPageSize?: number; + isLarge?: boolean; + isMedium?: boolean; + isSmall?: boolean; + isTouch?: boolean; + isVeryLarge?: boolean; + language?: string; + logoUrl?: string; + measureRef?: (ref: Element | null) => void; + messages?: StringMap; + metadataQuery?: MetadataQuery; + onCreate?: (item: BoxItem) => void; + onDelete?: (item: BoxItem) => void; + onDownload?: (item: BoxItem) => void; + onNavigate?: (item: BoxItem) => void; + onPreview?: (data: unknown) => void; + onRename?: (item: BoxItem) => void; + onSelect?: (item: BoxItem) => void; + onUpload?: (item: BoxItem) => void; + previewLibraryVersion?: string; + requestInterceptor?: (response: AxiosResponse) => void; + responseInterceptor?: (config: AxiosRequestConfig) => void; + rootFolderId?: string; + sharedLink?: string; + sharedLinkPassword?: string; + sortBy?: SortBy; + sortDirection?: SortDirection; + staticHost?: string; + staticPath?: string; + theme?: Theme; + token: Token; + uploadHost?: string; +} type State = { - currentCollection: Collection, - currentOffset: number, - currentPageNumber: number, - currentPageSize: number, - errorCode: string, - focusedRow: number, - gridColumnCount: number, - isCreateFolderModalOpen: boolean, - isDeleteModalOpen: boolean, - isLoading: boolean, - isPreviewModalOpen: boolean, - isRenameModalOpen: boolean, - isShareModalOpen: boolean, - isUploadModalOpen: boolean, - markers: Array, - rootName: string, - searchQuery: string, - selected?: BoxItem, - sortBy: SortBy, - sortDirection: SortDirection, - view: View, + currentCollection: Collection; + currentOffset: number; + currentPageNumber: number; + currentPageSize: number; + errorCode: string; + focusedRow: number; + gridColumnCount: number; + isCreateFolderModalOpen: boolean; + isDeleteModalOpen: boolean; + isLoading: boolean; + isPreviewModalOpen: boolean; + isRenameModalOpen: boolean; + isShareModalOpen: boolean; + isUploadModalOpen: boolean; + markers: Array; + rootName: string; + searchQuery: string; + selected?: BoxItem; + sortBy: SortBy; + sortDirection: SortDirection; + view: View; }; const localStoreViewMode = 'bce.defaultViewMode'; -class ContentExplorer extends Component { +class ContentExplorer extends Component { id: string; api: API; state: State; - props: Props; + props: ContentExplorerProps; - table: any; + table: React.Component; rootElement: HTMLElement; @@ -240,7 +234,7 @@ class ContentExplorer extends Component { * @private * @return {ContentExplorer} */ - constructor(props: Props) { + constructor(props: ContentExplorerProps) { super(props); const { @@ -257,7 +251,7 @@ class ContentExplorer extends Component { sortDirection, token, uploadHost, - }: Props = props; + }: ContentExplorerProps = props; this.api = new API({ apiHost, @@ -327,9 +321,9 @@ class ContentExplorer extends Component { * @return {void} */ componentDidMount() { - const { currentFolderId, defaultView }: Props = this.props; - this.rootElement = ((document.getElementById(this.id): any): HTMLElement); - this.appElement = ((this.rootElement.firstElementChild: any): HTMLElement); + const { currentFolderId, defaultView }: ContentExplorerProps = this.props; + this.rootElement = document.getElementById(this.id) as HTMLElement; + this.appElement = this.rootElement.firstElementChild as HTMLElement; switch (defaultView) { case DEFAULT_VIEW_RECENTS: @@ -351,8 +345,8 @@ class ContentExplorer extends Component { * @inheritdoc * @return {void} */ - componentDidUpdate({ currentFolderId: prevFolderId }: Props, prevState: State): void { - const { currentFolderId }: Props = this.props; + componentDidUpdate({ currentFolderId: prevFolderId }: ContentExplorerProps, prevState: State): void { + const { currentFolderId }: ContentExplorerProps = this.props; const { currentCollection: { id }, }: State = prevState; @@ -397,7 +391,7 @@ class ContentExplorer extends Component { * @return {void} */ showMetadataQueryResults() { - const { metadataQuery = {} }: Props = this.props; + const { metadataQuery = {} }: ContentExplorerProps = this.props; const { currentPageNumber, markers }: State = this.state; const metadataQueryClone = cloneDeep(metadataQuery); @@ -449,7 +443,7 @@ class ContentExplorer extends Component { * @param {Error} error error object * @return {void} */ - errorCallback = (error: any) => { + errorCallback = (error: unknown) => { this.setState({ view: VIEW_ERROR, }); @@ -465,7 +459,7 @@ class ContentExplorer extends Component { * @return {void} */ finishNavigation() { - const { autoFocus }: Props = this.props; + const { autoFocus }: ContentExplorerProps = this.props; const { currentCollection: { percentLoaded }, }: State = this.state; @@ -520,7 +514,7 @@ class ContentExplorer extends Component { * @return {void} */ fetchFolderSuccessCallback(collection: Collection, triggerNavigationEvent: boolean): void { - const { onNavigate, rootFolderId }: Props = this.props; + const { onNavigate, rootFolderId }: ContentExplorerProps = this.props; const { boxItem, id, name }: Collection = collection; const { selected }: State = this.state; const rootName = id === rootFolderId ? name : ''; @@ -549,8 +543,8 @@ class ContentExplorer extends Component { * @param {Boolean|void} [triggerNavigationEvent] To trigger navigate event * @return {void} */ - fetchFolder = (id?: string, triggerNavigationEvent?: boolean = true) => { - const { rootFolderId }: Props = this.props; + fetchFolder = (id?: string, triggerNavigationEvent: boolean = true) => { + const { rootFolderId }: ContentExplorerProps = this.props; const { currentCollection: { id: currentId }, currentOffset, @@ -610,7 +604,7 @@ class ContentExplorer extends Component { } const { id, type }: BoxItem = item; - const { isTouch }: Props = this.props; + const { isTouch }: ContentExplorerProps = this.props; if (type === TYPE_FOLDER) { this.fetchFolder(id); @@ -667,7 +661,7 @@ class ContentExplorer extends Component { * @return {void} */ search = (query: string) => { - const { rootFolderId }: Props = this.props; + const { rootFolderId }: ContentExplorerProps = this.props; const { currentCollection: { id }, currentOffset, @@ -733,7 +727,7 @@ class ContentExplorer extends Component { * @return {void} */ showRecents(triggerNavigationEvent: boolean = true): void { - const { rootFolderId }: Props = this.props; + const { rootFolderId }: ContentExplorerProps = this.props; // Reset search state, the view and show busy indicator this.setState({ @@ -765,7 +759,7 @@ class ContentExplorer extends Component { const { currentCollection: { id, permissions }, }: State = this.state; - const { canUpload }: Props = this.props; + const { canUpload }: ContentExplorerProps = this.props; if (!canUpload || !id || !permissions) { return; } @@ -803,7 +797,7 @@ class ContentExplorer extends Component { */ changeShareAccess = (access: Access) => { const { selected }: State = this.state; - const { canSetShareAccess }: Props = this.props; + const { canSetShareAccess }: ContentExplorerProps = this.props; if (!selected || !canSetShareAccess) { return; } @@ -843,7 +837,7 @@ class ContentExplorer extends Component { }; /** - * Sets state with currentCollection updated to have items.selected properties + * Sets state with currentCollection updated to have `items.selected` properties * set according to the given selected param. Also updates the selected item in the * currentCollection. selectedItem will be set to the selected state * item if it is in currentCollection, otherwise it will be set to undefined. @@ -854,12 +848,17 @@ class ContentExplorer extends Component { * @param {Function} [callback] - callback function that should be called after setState occurs * @return {void} */ - async updateCollection(collection: Collection, selectedItem: ?BoxItem, callback: Function = noop): Object { + async updateCollection( + collection: Collection, + selectedItem?: BoxItem | null, + callback: () => void = noop, + ): Promise { const newCollection: Collection = cloneDeep(collection); const { items = [] } = newCollection; + const fileAPI = this.api.getFileAPI(false); const selectedId = selectedItem ? selectedItem.id : null; - let newSelectedItem: ?BoxItem; + let newSelectedItem: BoxItem | null | undefined; const itemThumbnails = await Promise.all( items.map(item => { @@ -876,7 +875,7 @@ class ContentExplorer extends Component { ...currentItem, selected: isSelected, thumbnailUrl, - }; + } as const; if (item.type === TYPE_FILE && thumbnailUrl && !isThumbnailAvailable(newItem)) { this.attemptThumbnailGeneration(newItem); @@ -925,7 +924,7 @@ class ContentExplorer extends Component { updateItemInCollection = (newItem: BoxItem): void => { const { currentCollection } = this.state; const { items = [] } = currentCollection; - const newCollection = { ...currentCollection }; + const newCollection = { ...currentCollection } as const; newCollection.items = items.map(item => (item.id === newItem.id ? newItem : item)); this.setState({ currentCollection: newCollection }); @@ -939,10 +938,10 @@ class ContentExplorer extends Component { * @param {Function|void} [onSelect] - optional on select callback * @return {void} */ - select = (item: BoxItem, callback: Function = noop): void => { + select = (item: BoxItem, callback: (item: BoxItem) => void = noop): void => { const { selected, currentCollection }: State = this.state; const { items = [] } = currentCollection; - const { onSelect }: Props = this.props; + const { onSelect }: ContentExplorerProps = this.props; if (item === selected) { callback(item); @@ -988,7 +987,7 @@ class ContentExplorer extends Component { */ previewCallback = (): void => { const { selected }: State = this.state; - const { canPreview }: Props = this.props; + const { canPreview }: ContentExplorerProps = this.props; if (!selected || !canPreview) { return; } @@ -1025,7 +1024,7 @@ class ContentExplorer extends Component { */ downloadCallback = (): void => { const { selected }: State = this.state; - const { canDownload, onDownload }: Props = this.props; + const { canDownload, onDownload }: ContentExplorerProps = this.props; if (!selected || !canDownload) { return; } @@ -1040,7 +1039,7 @@ class ContentExplorer extends Component { return; } - const openUrl: Function = (url: string) => { + const openUrl = (url: string) => { openUrlInsideIframe(url); onDownload(cloneDeep([selected])); }; @@ -1070,7 +1069,7 @@ class ContentExplorer extends Component { */ deleteCallback = (): void => { const { selected, isDeleteModalOpen }: State = this.state; - const { canDelete, onDelete }: Props = this.props; + const { canDelete, onDelete }: ContentExplorerProps = this.props; if (!selected || !canDelete) { return; } @@ -1122,9 +1121,10 @@ class ContentExplorer extends Component { * @param {string} value new item name * @return {void} */ - renameCallback = (nameWithoutExt: string, extension: string): void => { + renameCallback = (nameWithoutExt?: string, extension?: string): void => { const { selected, isRenameModalOpen }: State = this.state; - const { canRename, onRename }: Props = this.props; + const { canRename, onRename }: ContentExplorerProps = this.props; + if (!selected || !canRename) { return; } @@ -1135,6 +1135,7 @@ class ContentExplorer extends Component { } const { can_rename }: BoxItemPermission = permissions; + if (!can_rename) { return; } @@ -1188,7 +1189,7 @@ class ContentExplorer extends Component { */ createFolderCallback = (name?: string): void => { const { isCreateFolderModalOpen, currentCollection }: State = this.state; - const { canCreateNewFolder, onCreate }: Props = this.props; + const { canCreateNewFolder, onCreate }: ContentExplorerProps = this.props; if (!canCreateNewFolder) { return; } @@ -1295,7 +1296,6 @@ class ContentExplorer extends Component { // if there is no shared link, create one with enterprise default access if (!item[FIELD_SHARED_LINK] && getProp(item, FIELD_PERMISSIONS_CAN_SHARE, false)) { - // $FlowFixMe await this.api.getAPI(item.type).share(item, undefined, (sharedItem: BoxItem) => { updatedItem = sharedItem; }); @@ -1312,7 +1312,7 @@ class ContentExplorer extends Component { */ shareCallback = (): void => { const { selected }: State = this.state; - const { canShare }: Props = this.props; + const { canShare }: ContentExplorerProps = this.props; if (!selected || !canShare) { return; @@ -1338,7 +1338,7 @@ class ContentExplorer extends Component { * @param {Component} react component * @return {void} */ - tableRef = (table: React$Component<*, *>): void => { + tableRef = (table: React.Component): void => { this.table = table; }; @@ -1386,12 +1386,12 @@ class ContentExplorer extends Component { * @private * @return {void} */ - onKeyDown = (event: SyntheticKeyboardEvent) => { + onKeyDown = (event: React.KeyboardEvent) => { if (isInputElement(event.target)) { return; } - const { rootFolderId }: Props = this.props; + const { rootFolderId }: ContentExplorerProps = this.props; const key = event.key.toLowerCase(); switch (key) { @@ -1540,8 +1540,8 @@ class ContentExplorer extends Component { updateMetadata = ( item: BoxItem, field: string, - oldValue: ?MetadataFieldValue, - newValue: ?MetadataFieldValue, + oldValue?: MetadataFieldValue | null, + newValue?: MetadataFieldValue | null, ): void => { this.metadataQueryAPIHelper.updateMetadata( item, @@ -1555,7 +1555,7 @@ class ContentExplorer extends Component { ); }; - updateMetadataSuccessCallback = (item: BoxItem, field: string, newValue: ?MetadataFieldValue): void => { + updateMetadataSuccessCallback = (item: BoxItem, field: string, newValue?: MetadataFieldValue | null): void => { const { currentCollection }: State = this.state; const { items = [], nextMarker } = currentCollection; const updatedItems = items.map(collectionItem => { @@ -1625,7 +1625,7 @@ class ContentExplorer extends Component { theme, token, uploadHost, - }: Props = this.props; + }: ContentExplorerProps = this.props; const { currentCollection, diff --git a/src/elements/content-explorer/PreviewDialog.tsx b/src/elements/content-explorer/PreviewDialog.tsx index f1db79bfb9..0e2132de29 100644 --- a/src/elements/content-explorer/PreviewDialog.tsx +++ b/src/elements/content-explorer/PreviewDialog.tsx @@ -3,6 +3,7 @@ import { useIntl } from 'react-intl'; import Modal from 'react-modal'; import cloneDeep from 'lodash/cloneDeep'; +import { AxiosRequestConfig, AxiosResponse } from 'axios'; import ContentPreview, { ContentPreviewProps } from '../content-preview'; import { TYPE_FILE, CLASS_MODAL_CONTENT_FULL_BLEED, CLASS_MODAL_OVERLAY, CLASS_MODAL } from '../../constants'; import type { Token, BoxItem, Collection } from '../../common/types/core'; @@ -22,12 +23,12 @@ export interface PreviewDialogProps { isTouch: boolean; item: BoxItem; onCancel: () => void; - onDownload: () => void; + onDownload: (item: BoxItem) => void; onPreview: (data: unknown) => void; parentElement: HTMLElement; previewLibraryVersion: string; - requestInterceptor?: () => void; - responseInterceptor?: () => void; + requestInterceptor?: (response: AxiosResponse) => void; + responseInterceptor?: (config: AxiosRequestConfig) => void; sharedLink?: string; sharedLinkPassword?: string; staticHost: string; diff --git a/src/elements/content-explorer/ShareDialog.tsx b/src/elements/content-explorer/ShareDialog.tsx index edb254095d..5b1934cb8d 100644 --- a/src/elements/content-explorer/ShareDialog.tsx +++ b/src/elements/content-explorer/ShareDialog.tsx @@ -6,7 +6,7 @@ import { Button, Modal as BlueprintModal, Text } from '@box/blueprint-web'; import ShareAccessSelect from '../common/share-access-select'; import { CLASS_MODAL_CONTENT, CLASS_MODAL_OVERLAY, CLASS_MODAL } from '../../constants'; -import type { BoxItem } from '../../common/types/core'; +import type { Access, BoxItem } from '../../common/types/core'; import messages from '../common/messages'; @@ -19,7 +19,7 @@ export interface ShareDialogProps { isOpen: boolean; item: BoxItem; onCancel: () => void; - onShareAccessChange: () => void; + onShareAccessChange: (access: Access) => void; parentElement: HTMLElement; } diff --git a/src/elements/content-explorer/__tests__/ContentExplorer.test.js b/src/elements/content-explorer/__tests__/ContentExplorer.test.js deleted file mode 100644 index 8a70cdc3ad..0000000000 --- a/src/elements/content-explorer/__tests__/ContentExplorer.test.js +++ /dev/null @@ -1,719 +0,0 @@ -import React, { act } from 'react'; -import cloneDeep from 'lodash/cloneDeep'; -import { mount } from 'enzyme'; -import noop from 'lodash/noop'; -import { ContentExplorerComponent as ContentExplorer } from '../ContentExplorer'; -import UploadDialog from '../../common/upload-dialog'; -import { isThumbnailAvailable } from '../../common/utils'; -import CONTENT_EXPLORER_FOLDER_FIELDS_TO_FETCH from '../constants'; -import { VIEW_MODE_GRID } from '../../../constants'; - -jest.mock('../../common/utils'); -jest.mock('../../common/header/Header', () => 'mock-header'); -jest.mock('../../common/sub-header/SubHeader', () => 'mock-subheader'); -jest.mock('../Content', () => 'mock-content'); -jest.mock('../../common/upload-dialog/UploadDialog', () => 'mock-uploaddialog'); -jest.mock('../../common/create-folder-dialog/CreateFolderDialog', () => 'mock-createfolderdialog'); -jest.mock('../DeleteConfirmationDialog', () => 'mock-deletedialog'); -jest.mock('../RenameDialog', () => 'mock-renamedialog'); -jest.mock('../ShareDialog', () => 'mock-sharedialog'); -jest.mock('../PreviewDialog', () => 'mock-previewdialog'); - -describe('elements/content-explorer/ContentExplorer', () => { - let rootElement; - const getWrapper = (props = {}) => mount(, { attachTo: rootElement }); - - beforeEach(() => { - rootElement = document.createElement('div'); - rootElement.appendChild(document.createElement('div')); - document.body.appendChild(rootElement); - }); - - afterEach(() => { - document.body.removeChild(rootElement); - }); - - describe('uploadSuccessHandler()', () => { - test('should force reload the files list', () => { - const wrapper = getWrapper(); - const instance = wrapper.instance(); - - act(() => { - instance.setState({ - currentCollection: { - id: '123', - }, - }); - }); - - instance.fetchFolder = jest.fn(); - - act(() => { - instance.uploadSuccessHandler(); - }); - - expect(instance.fetchFolder).toHaveBeenCalledWith('123', false); - }); - }); - - describe('changeViewMode()', () => { - const localStoreViewMode = 'bce.defaultViewMode'; - - test('should change to grid view', () => { - const wrapper = getWrapper(); - const instance = wrapper.instance(); - instance.store.setItem = jest.fn(); - instance.changeViewMode(VIEW_MODE_GRID); - expect(instance.store.setItem).toHaveBeenCalledWith(localStoreViewMode, VIEW_MODE_GRID); - }); - }); - - describe('fetchFolder()', () => { - const getFolder = jest.fn(); - const getFolderAPI = jest.fn().mockReturnValue({ - getFolder, - }); - - let wrapper; - let instance; - - test('should fetch folder without representations field if grid view is not enabled', () => { - wrapper = getWrapper(); - instance = wrapper.instance(); - instance.api = { getFolderAPI }; - instance.setState = jest.fn(); - instance.fetchFolder(); - expect(instance.setState).toHaveBeenCalled(); - expect(getFolder).toHaveBeenCalledWith( - '0', - 50, - 0, - 'name', - 'ASC', - expect.any(Function), - expect.any(Function), - { forceFetch: true, fields: CONTENT_EXPLORER_FOLDER_FIELDS_TO_FETCH }, - ); - }); - }); - - describe('fetchFolderSuccessCallback()', () => { - const collection = { name: 'collection ' }; - - test('updateCollection should be called with a callback', () => { - const wrapper = getWrapper(); - const instance = wrapper.instance(); - instance.closeModals = jest.fn(); - instance.updateCollection = jest.fn(); - - instance.fetchFolderSuccessCallback(collection, false); - expect(instance.closeModals).toHaveBeenCalled(); - expect(instance.updateCollection).toHaveBeenCalledWith(collection, undefined, expect.any(Function)); - }); - }); - - describe('recentsSuccessCallback()', () => { - const collection = { name: 'collection ' }; - - test('navigation event should not be triggered if argument set to false', () => { - const wrapper = getWrapper(); - const instance = wrapper.instance(); - instance.updateCollection = jest.fn(); - - instance.recentsSuccessCallback(collection, false); - expect(instance.updateCollection).toHaveBeenCalledWith(collection); - }); - - test('navigation event should be triggered if argument set to true ', () => { - const wrapper = getWrapper(); - const instance = wrapper.instance(); - instance.updateCollection = jest.fn(); - - instance.recentsSuccessCallback(collection, true); - expect(instance.updateCollection).toHaveBeenCalledWith(collection, undefined, instance.finishNavigation); - }); - }); - - describe('updateCollection()', () => { - describe('selection', () => { - const item1 = { id: 1 }; - const item2 = { id: 2 }; - const collection = { boxItem: {}, id: '0', items: [item1, item2], name: 'name' }; - - let wrapper; - let instance; - - beforeEach(() => { - wrapper = getWrapper(); - instance = wrapper.instance(); - instance.setState({ currentCollection: collection, selected: undefined }); - instance.setState = jest.fn(); - }); - - test('should set same collection and no selected item to state if no items present in collection', () => { - const noItemsCollection = { ...collection, items: undefined }; - const expectedCollection = { ...collection, items: [] }; - - instance.updateCollection(noItemsCollection, { id: 3 }).then(() => { - expect(instance.setState).toHaveBeenCalledWith( - { currentCollection: expectedCollection, selected: undefined }, - noop, - ); - }); - }); - - test('should update the collection items selected to false even if selected item is not in the collection', () => { - const expectedItem1 = { id: 1, selected: false, thumbnailUrl: null }; - const expectedItem2 = { id: 2, selected: false, thumbnailUrl: null }; - const expectedCollection = { - boxItem: {}, - id: '0', - items: [expectedItem1, expectedItem2], - name: 'name', - }; - - instance.updateCollection(collection, { id: 3 }).then(() => { - expect(instance.setState).toHaveBeenCalledWith( - { currentCollection: expectedCollection, selected: undefined }, - noop, - ); - }); - }); - - test('should update the collection items selected to false except for the selected item in the collection', () => { - const expectedItem1 = { id: 1, selected: false, thumbnailUrl: null }; - const expectedItem2 = { id: 2, selected: true, thumbnailUrl: null }; - const expectedCollection = { - boxItem: {}, - id: '0', - items: [expectedItem1, expectedItem2], - name: 'name', - }; - - instance.updateCollection(collection, { id: 2 }).then(() => { - expect(instance.setState).toHaveBeenCalledWith( - { currentCollection: expectedCollection, selected: expectedItem2 }, - noop, - ); - }); - }); - - test('should update the selected item in the collection', () => { - const expectedItem1 = { id: 1, selected: false, thumbnailUrl: null }; - const expectedItem2 = { id: 2, selected: true, newProperty: 'newProperty', thumbnailUrl: null }; - const expectedCollection = { - boxItem: {}, - id: '0', - items: [expectedItem1, expectedItem2], - name: 'name', - }; - - instance.updateCollection(collection, { id: 2, newProperty: 'newProperty' }).then(() => { - expect(instance.setState).toHaveBeenCalledWith( - { - currentCollection: expectedCollection, - selected: { ...expectedItem2, newProperty: 'newProperty' }, - }, - noop, - ); - }); - }); - }); - - describe('thumbnails', () => { - const baseItem = { id: '1', selected: true, type: 'file' }; - const baseCollection = { - boxItem: {}, - id: '0', - items: [baseItem], - name: 'collectionName', - selected: baseItem, - }; - const thumbnailUrl = 'thumbnailUrl'; - const callback = jest.fn(); - - let wrapper; - let instance; - let collection; - let item; - - beforeEach(() => { - collection = cloneDeep(baseCollection); - item = cloneDeep(baseItem); - }); - - test('should add thumbnailUrl', () => { - const getThumbnailUrl = jest.fn().mockReturnValue(thumbnailUrl); - const getFileAPI = jest.fn().mockReturnValue({ - getThumbnailUrl, - }); - wrapper = getWrapper(); - instance = wrapper.instance(); - instance.api = { getFileAPI }; - instance.setState = jest.fn(); - - return instance.updateCollection(collection, item, callback).then(() => { - const newSelected = { ...item, thumbnailUrl }; - const newCollection = { ...collection, items: [newSelected] }; - - expect(instance.setState).toHaveBeenCalledWith( - { currentCollection: newCollection, selected: newSelected }, - callback, - ); - }); - }); - test('should not call attemptThumbnailGeneration if thumbnail is null', () => { - const getThumbnailUrl = jest.fn().mockReturnValue(null); - const getFileAPI = jest.fn().mockReturnValue({ - getThumbnailUrl, - }); - - wrapper = getWrapper(); - instance = wrapper.instance(); - instance.api = { getFileAPI }; - instance.setState = jest.fn(); - instance.attemptThumbnailGeneration = jest.fn(); - - return instance.updateCollection(collection, item, callback).then(() => { - expect(instance.attemptThumbnailGeneration).not.toHaveBeenCalled(); - }); - }); - - test('should not call attemptThumbnailGeneration if isThumbnailAvailable is true', () => { - const getThumbnailUrl = jest.fn().mockReturnValue(null); - const getFileAPI = jest.fn().mockReturnValue({ - getThumbnailUrl, - }); - - wrapper = getWrapper(); - instance = wrapper.instance(); - instance.api = { getFileAPI }; - instance.setState = jest.fn(); - instance.attemptThumbnailGeneration = jest.fn(); - isThumbnailAvailable.mockReturnValue(true); - - return instance.updateCollection(collection, item, callback).then(() => { - expect(instance.attemptThumbnailGeneration).not.toHaveBeenCalled(); - }); - }); - - test('should call attemptThumbnailGeneration if isThumbnailAvailable is false', () => { - const getThumbnailUrl = jest.fn().mockReturnValue(thumbnailUrl); - const getFileAPI = jest.fn().mockReturnValue({ - getThumbnailUrl, - }); - - wrapper = getWrapper(); - instance = wrapper.instance(); - instance.api = { getFileAPI }; - instance.setState = jest.fn(); - instance.attemptThumbnailGeneration = jest.fn(); - isThumbnailAvailable.mockReturnValue(false); - - return instance.updateCollection(collection, item, callback).then(() => { - expect(instance.attemptThumbnailGeneration).toHaveBeenCalled(); - }); - }); - - test('should not call attemptThumbnailGeneration or getThumbnailUrl if item is not file', () => { - const getThumbnailUrl = jest.fn().mockReturnValue(thumbnailUrl); - const getFileAPI = jest.fn().mockReturnValue({ - getThumbnailUrl, - }); - - wrapper = getWrapper(); - instance = wrapper.instance(); - instance.api = { getFileAPI }; - instance.setState = jest.fn(); - instance.attemptThumbnailGeneration = jest.fn(); - isThumbnailAvailable.mockReturnValue(false); - - collection.items[0].type = 'folder'; - return instance.updateCollection(collection, item, callback).then(() => { - expect(instance.attemptThumbnailGeneration).not.toHaveBeenCalled(); - expect(getThumbnailUrl).not.toHaveBeenCalled(); - }); - }); - }); - - describe('attemptThumbnailGeneration()', () => { - const entry1 = { name: 'entry1', updated: false }; - const entry2 = { name: 'entry2', updated: false }; - const itemWithRepresentation = { representations: { entries: [entry1, entry2] } }; - const itemWithoutRepresentation = { name: 'item' }; - - let wrapper; - let instance; - - test('should not update item in collection if grid view is not enabled', () => { - wrapper = getWrapper(); - instance = wrapper.instance(); - instance.updateItemInCollection = jest.fn(); - return instance.attemptThumbnailGeneration(itemWithRepresentation).then(() => { - expect(instance.updateItemInCollection).not.toHaveBeenCalled(); - }); - }); - - test('should not update item in collection if item does not have representation', () => { - wrapper = getWrapper(); - instance = wrapper.instance(); - instance.updateItemInCollection = jest.fn(); - return instance.attemptThumbnailGeneration(itemWithoutRepresentation).then(() => { - expect(instance.updateItemInCollection).not.toHaveBeenCalled(); - }); - }); - - test('should not update item in collection if updated representation matches given representation', () => { - wrapper = getWrapper(); - instance = wrapper.instance(); - instance.updateItemInCollection = jest.fn(); - instance.api = { - getFileAPI: jest - .fn() - .mockReturnValue({ generateRepresentation: jest.fn().mockReturnValue(entry1) }), - }; - return instance.attemptThumbnailGeneration(itemWithRepresentation).then(() => { - expect(instance.updateItemInCollection).not.toHaveBeenCalled(); - }); - }); - - test('should update item in collection if representation is updated', () => { - wrapper = getWrapper(); - instance = wrapper.instance(); - instance.updateItemInCollection = jest.fn(); - instance.api = { - getFileAPI: jest.fn().mockReturnValue({ - generateRepresentation: jest.fn().mockReturnValue({ ...entry1, updated: true }), - }), - }; - return instance.attemptThumbnailGeneration(itemWithRepresentation).then(() => { - expect(instance.updateItemInCollection).toHaveBeenCalledWith({ - ...itemWithRepresentation, - representations: { entries: [{ ...entry1, updated: true }, entry2] }, - }); - }); - }); - }); - - describe('updateItemInCollection()', () => { - const item1 = { id: '1', updated: false }; - const item2 = { id: '2', updated: false }; - const baseCollection = { items: [item1, item2] }; - - let wrapper; - let instance; - - beforeEach(() => { - wrapper = getWrapper(); - instance = wrapper.instance(); - act(() => { - instance.setState({ currentCollection: baseCollection }); - }); - instance.setState = jest.fn(); - }); - - test('should not update collection if matching id is not present in collection', () => { - const item3 = { id: '3', updated: true }; - act(() => { - instance.updateItemInCollection(item3); - }); - expect(instance.setState).toHaveBeenCalledWith({ currentCollection: baseCollection }); - }); - - test('should update collection if matching id is present in collection', () => { - const newItem2 = { id: '2', updated: true }; - act(() => { - instance.updateItemInCollection(newItem2); - }); - expect(instance.setState).toHaveBeenCalledWith({ - currentCollection: { ...baseCollection, items: [item1, newItem2] }, - }); - }); - }); - }); - - describe('lifecycle methods', () => { - test('componentDidUpdate', () => { - const props = { - currentFolderId: '123', - }; - - const wrapper = getWrapper(props); - const instance = wrapper.instance(); - instance.fetchFolder = jest.fn(); - - wrapper.setProps({ currentFolderId: '345' }); - - expect(instance.fetchFolder).toBeCalledWith('345'); - }); - }); - - describe('getMaxNumberOfGridViewColumnsForWidth()', () => { - test('should be able to display 7 columns if isVeryLarge', () => { - const wrapper = getWrapper({ isVeryLarge: true }); - const instance = wrapper.instance(); - expect(instance.getMaxNumberOfGridViewColumnsForWidth()).toBe(7); - }); - - test('should only be able to display 5 columns if isLarge', () => { - const wrapper = getWrapper({ isLarge: true }); - const instance = wrapper.instance(); - expect(instance.getMaxNumberOfGridViewColumnsForWidth()).toBe(5); - }); - - test('should only be able to display 3 columns if isMedium', () => { - const wrapper = getWrapper({ isMedium: true }); - const instance = wrapper.instance(); - expect(instance.getMaxNumberOfGridViewColumnsForWidth()).toBe(3); - }); - - test('should only be able to display 1 column if isSmall', () => { - const wrapper = getWrapper({ isSmall: true }); - const instance = wrapper.instance(); - expect(instance.getMaxNumberOfGridViewColumnsForWidth()).toBe(1); - }); - }); - - describe('updateMetadata()', () => { - test('should update metadata for given Box item, field, old and new values', () => { - const item = {}; - const field = 'amount'; - const oldValue = 'abc'; - const newValue = 'pqr'; - - const wrapper = getWrapper(); - const instance = wrapper.instance(); - instance.metadataQueryAPIHelper = { - updateMetadata: jest.fn(), - }; - - instance.updateMetadata(item, field, oldValue, newValue); - expect(instance.metadataQueryAPIHelper.updateMetadata).toHaveBeenCalledWith( - item, - field, - oldValue, - newValue, - expect.any(Function), - instance.errorCallback, - ); - }); - }); - - describe('updateMetadataSuccessCallback()', () => { - test('should correctly update the current collection and set the state', () => { - const boxItem = { id: 2 }; - const field = 'amount'; - const newValue = 111.22; - const collectionItem1 = { - id: 1, - metadata: { - enterprise: { - fields: [ - { - name: 'name', - key: 'name', - value: 'abc', - type: 'string', - }, - { - name: 'amount', - key: 'amount', - value: 100.34, - type: 'float', - }, - ], - }, - }, - }; - const collectionItem2 = { - id: 2, - metadata: { - enterprise: { - fields: [ - { - name: 'name', - key: 'name', - value: 'pqr', - type: 'string', - }, - { - name: 'amount', - key: 'amount', - value: 354.23, - type: 'float', - }, - ], - }, - }, - }; - const clonedCollectionItem2 = cloneDeep(collectionItem2); - const nextMarker = 'markermarkermarkermarkermarkermarker'; - const currentCollection = { - items: [collectionItem1, collectionItem2], - nextMarker, - }; - const wrapper = getWrapper(); - - // update the metadata - clonedCollectionItem2.metadata.enterprise.fields.find(item => item.key === field).value = newValue; - - const updatedItems = [collectionItem1, clonedCollectionItem2]; - - act(() => { - wrapper.setState({ currentCollection }); - }); - - const instance = wrapper.instance(); - instance.setState = jest.fn(); - act(() => { - instance.updateMetadataSuccessCallback(boxItem, field, newValue); - }); - expect(instance.setState).toHaveBeenCalledWith({ - currentCollection: { - items: updatedItems, - nextMarker, - percentLoaded: 100, - }, - }); - }); - }); - - describe('handleSharedLinkSuccess()', () => { - const getApiShareMock = jest.fn().mockImplementation((item, access, callback) => callback()); - const getApiMock = jest.fn().mockReturnValue({ share: getApiShareMock }); - const updateCollectionMock = jest.fn(); - - const boxItem = { - shared_link: 'not null', - permissions: { - can_share: true, - can_set_share_access: false, - }, - type: 'file', - }; - - let wrapper; - let instance; - - beforeEach(() => { - wrapper = getWrapper(); - instance = wrapper.instance(); - instance.api = { getAPI: getApiMock }; - instance.updateCollection = updateCollectionMock; - }); - - afterEach(() => { - getApiMock.mockClear(); - getApiShareMock.mockClear(); - updateCollectionMock.mockClear(); - }); - - test('should create shared link if it does not exist', async () => { - await instance.handleSharedLinkSuccess({ ...boxItem, shared_link: null }); - - expect(getApiMock).toBeCalledTimes(1); - expect(getApiShareMock).toBeCalledTimes(1); - expect(updateCollectionMock).toBeCalledTimes(1); - }); - - test('should not create shared link if it already exists', async () => { - await instance.handleSharedLinkSuccess(boxItem); - - expect(getApiMock).not.toBeCalled(); - expect(getApiShareMock).not.toBeCalled(); - expect(updateCollectionMock).toBeCalledTimes(1); - }); - }); - - describe('render()', () => { - test('should render UploadDialog with contentUploaderProps', () => { - const contentUploaderProps = { - apiHost: 'https://api.box.com', - chunked: false, - }; - const wrapper = getWrapper({ canUpload: true, contentUploaderProps }); - act(() => { - wrapper.setState({ - currentCollection: { - permissions: { - can_upload: true, - }, - }, - }); - }); - const uploadDialogElement = wrapper.find(UploadDialog); - expect(uploadDialogElement.length).toBe(1); - expect(uploadDialogElement.prop('contentUploaderProps')).toEqual(contentUploaderProps); - }); - - test('should render test id for e2e testing', () => { - const wrapper = getWrapper(); - expect(wrapper.find('[data-testid="content-explorer"]')).toHaveLength(1); - }); - }); - - describe('deleteCallback', () => { - const getApiDeleteMock = jest.fn(); - const getApiMock = jest.fn().mockReturnValue({ deleteItem: getApiDeleteMock }); - const refreshCollectionMock = jest.fn(); - const onDeleteMock = jest.fn(); - const boxItem = { - id: '123', - parent: { - id: '122', - }, - permissions: { - can_delete: true, - }, - type: 'file', - }; - - let wrapper; - let instance; - - beforeEach(() => { - wrapper = getWrapper({ - canDelete: true, - onDelete: onDeleteMock, - }); - instance = wrapper.instance(); - instance.api = { getAPI: getApiMock, getCache: jest.fn() }; - instance.refreshCollection = refreshCollectionMock; - act(() => { - instance.setState({ - selected: boxItem, - isDeleteModalOpen: true, - }); - }); - }); - - afterEach(() => { - getApiMock.mockClear(); - getApiDeleteMock.mockClear(); - refreshCollectionMock.mockClear(); - }); - - test('should call refreshCollection and onDelete callback on success', async () => { - getApiDeleteMock.mockImplementation((item, successCallback) => successCallback()); - act(() => { - instance.deleteCallback(); - }); - expect(getApiMock).toBeCalledTimes(1); - expect(getApiDeleteMock).toBeCalledTimes(1); - expect(onDeleteMock).toBeCalledTimes(1); - expect(refreshCollectionMock).toBeCalledTimes(1); - }); - - test('should call refreshCollection on error', async () => { - getApiDeleteMock.mockImplementation((item, successCallback, errorCallback) => errorCallback()); - act(() => { - instance.deleteCallback(); - }); - - expect(getApiMock).toBeCalledTimes(1); - expect(getApiDeleteMock).toBeCalledTimes(1); - expect(onDeleteMock).not.toBeCalled(); - expect(refreshCollectionMock).toBeCalledTimes(1); - }); - }); -}); diff --git a/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx b/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx new file mode 100644 index 0000000000..8fcee38c3e --- /dev/null +++ b/src/elements/content-explorer/__tests__/ContentExplorer.test.tsx @@ -0,0 +1,498 @@ +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { render, screen, waitFor, within } from '../../../test-utils/testing-library'; +import { ContentExplorerComponent as ContentExplorer, ContentExplorerProps } from '../ContentExplorer'; +import { mockRootFolder, mockRootFolderSharedLink } from '../stories/__mocks__/mockRootFolder'; +import { mockMetadata, mockSchema } from '../stories/__mocks__/mockMetadata'; +import mockSubFolder from '../stories/__mocks__/mockSubFolder'; + +jest.mock('../../../utils/Xhr', () => { + return jest.fn().mockImplementation(() => { + return { + get: jest.fn(({ url }) => { + switch (url) { + case 'https://api.box.com/2.0/folders/69083462919': + return Promise.resolve({ data: mockRootFolder }); + case 'https://api.box.com/2.0/folders/73426618530': + return Promise.resolve({ + data: mockSubFolder, + }); + case 'https://api.box.com/2.0/metadata_templates/enterprise/templateName/schema': + return Promise.resolve({ data: mockSchema }); + default: + return Promise.reject(new Error('Not Found')); + } + }), + post: jest.fn(({ url }) => { + switch (url) { + case 'https://api.box.com/2.0/metadata_queries/execute_read': + return Promise.resolve({ data: mockMetadata }); + default: + return Promise.reject(new Error('Not Found')); + } + }), + put: jest.fn(({ url }) => { + switch (url) { + case 'https://api.box.com/2.0/folders/73426618530': + return Promise.resolve({ + data: mockRootFolderSharedLink, + }); + default: + return Promise.reject(new Error('Not Found')); + } + }), + delete: jest.fn(({ url }) => { + switch (url) { + case 'https://api.box.com/2.0/folders/73426618530?recursive=true': + return Promise.resolve({ data: {} }); + default: + return Promise.reject(new Error('Not Found')); + } + }), + abort: jest.fn(), + }; + }); +}); + +jest.mock( + '@box/react-virtualized/dist/es/AutoSizer', + () => + ({ children }) => + children({ height: 600, width: 1200 }), +); + +jest.mock('../PreviewDialog', () => props => { + props.onPreview(); + return 'mock-content-preview'; +}); + +describe('elements/content-explorer/ContentExplorer', () => { + let rootElement: HTMLDivElement; + + const renderComponent = (props: Partial = {}) => { + return render(); + }; + + beforeEach(() => { + rootElement = document.createElement('div'); + rootElement.appendChild(document.createElement('div')); + document.body.appendChild(rootElement); + }); + + afterEach(() => { + jest.clearAllMocks(); + document.body.removeChild(rootElement); + }); + + describe('render', () => { + test('should render the component', async () => { + renderComponent(); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + expect(screen.getByRole('button', { name: 'Preview Test Folder' })).toBeInTheDocument(); + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText('Modified')).toBeInTheDocument(); + expect(screen.getByText('Size')).toBeInTheDocument(); + expect(screen.getByText('An Ordered Folder')).toBeInTheDocument(); + expect(screen.getByText('Modified Tue Apr 16 2019 by Preview')).toBeInTheDocument(); + expect(screen.getByRole('gridcell', { name: '191.33 MB' })).toBeInTheDocument(); + }); + + test('shoulder render grid view mode', async () => { + renderComponent(); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + expect(screen.getByRole('button', { name: 'Preview Test Folder' })).toBeInTheDocument(); + + const gridButton = screen.getByRole('button', { name: 'Switch to Grid View' }); + await userEvent.click(gridButton); + + expect(screen.queryByText('Name')).not.toBeInTheDocument(); + expect(screen.queryByText('Modified')).not.toBeInTheDocument(); + expect(screen.queryByText('Size')).not.toBeInTheDocument(); + + expect(screen.getByText('An Ordered Folder')).toBeInTheDocument(); + expect(screen.getByText(/Apr 16, 2019\s+by Preview/)).toBeInTheDocument(); + }); + }); + + describe('Upload', () => { + test('should upload a new item', async () => { + renderComponent({ canUpload: true }); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + const addButton = screen.getByRole('button', { name: 'Add' }); + await userEvent.click(addButton); + + const uploadButton = screen.getByText('Upload'); + await userEvent.click(uploadButton); + + expect(screen.getByText('Drag and drop files')).toBeInTheDocument(); + expect(screen.getByText('Browse your device')).toBeInTheDocument(); + }); + + test('should not render upload button when canUpload is false', async () => { + renderComponent({ canUpload: false }); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + const addButton = screen.getByRole('button', { name: 'Add' }); + await userEvent.click(addButton); + + expect(screen.queryByText('Upload')).not.toBeInTheDocument(); + }); + }); + + describe('New Folder', () => { + test('should open new folder dialog', async () => { + const onCreate = jest.fn(); + renderComponent({ canCreateNewFolder: true, onCreate }); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + const addButton = screen.getByRole('button', { name: 'Add' }); + await userEvent.click(addButton); + + const uploadButton = screen.getByText('New Folder'); + await userEvent.click(uploadButton); + + expect(screen.getByText('Please enter a name.')).toBeInTheDocument(); + + expect(screen.getByRole('button', { name: 'Create' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + }); + + test('should not render new folder button when canCreateNewFolder is false', async () => { + renderComponent({ canCreateNewFolder: false }); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + const addButton = screen.getByRole('button', { name: 'Add' }); + await userEvent.click(addButton); + + expect(screen.queryByText('New Folder')).not.toBeInTheDocument(); + }); + }); + + describe('Rename item', () => { + test('should open rename dialog', async () => { + const onRename = jest.fn(); + renderComponent({ onRename }); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + const moreOptionsButton = screen.getAllByRole('button', { name: 'More options' })[0]; + await userEvent.click(moreOptionsButton); + let renameButton = screen.getByText('Rename'); + expect(renameButton).toBeInTheDocument(); + await userEvent.click(renameButton); + + const input = screen.getByRole('textbox', { name: 'Please enter a new name for An Ordered Folder:' }); + expect(input).toBeInTheDocument(); + + renameButton = screen.getByRole('button', { name: 'Rename' }); + expect(renameButton).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + await userEvent.clear(input); + await userEvent.type(input, 'New Ordered Folder'); + await userEvent.click(renameButton); + + expect(onRename).toHaveBeenCalledWith({ + ...mockRootFolder.item_collection.entries[0], + selected: true, + thumbnailUrl: null, + }); + }); + + test('should not render rename button when canRename is false', async () => { + renderComponent({ canRename: false }); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + const moreOptionsButton = screen.getAllByRole('button', { name: 'More options' })[0]; + await userEvent.click(moreOptionsButton); + + expect(screen.queryByText('Rename')).not.toBeInTheDocument(); + }); + }); + + describe('Share', () => { + test('should create share link', async () => { + renderComponent({ isSmall: true }); + + await waitFor(() => { + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + const moreOptionsButton = screen.getAllByRole('button', { name: 'More options' })[0]; + await userEvent.click(moreOptionsButton); + + const shareButton = within(screen.getByRole('menu')).getByText('Share'); + expect(shareButton).toBeInTheDocument(); + await userEvent.click(shareButton); + + expect(screen.getByText('Shared Link:')).toBeInTheDocument(); + const input = screen.getByRole('textbox'); + expect(input).toHaveValue('https://example.com/share-link'); + expect(screen.getByRole('button', { name: 'Copy' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument(); + }); + + test('should not render share button when canShare is false', async () => { + renderComponent({ canShare: false }); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + const moreOptionsButton = screen.getAllByRole('button', { name: 'More options' })[0]; + await userEvent.click(moreOptionsButton); + + expect(screen.queryByText('Share')).not.toBeInTheDocument(); + }); + }); + + describe('Delete', () => { + test('should delete item', async () => { + const onDelete = jest.fn(); + renderComponent({ canCreateNewFolder: true, onDelete }); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + const moreOptionsButton = screen.getAllByRole('button', { name: 'More options' })[0]; + await userEvent.click(moreOptionsButton); + const deleteButton = screen.getByText('Delete'); + expect(deleteButton).toBeInTheDocument(); + await userEvent.click(deleteButton); + + expect( + screen.getByText('Are you sure you want to delete An Ordered Folder and all its contents?'), + ).toBeInTheDocument(); + + const deleteButtonConfirm = screen.getByRole('button', { name: 'Delete' }); + expect(deleteButtonConfirm).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + + await userEvent.click(deleteButtonConfirm); + + expect(onDelete).toHaveBeenCalledWith([ + { ...mockRootFolder.item_collection.entries[0], selected: true, thumbnailUrl: null }, + ]); + }); + + test('should not render delete button when canDelete is false', async () => { + renderComponent({ canDelete: false }); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + const moreOptionsButton = screen.getAllByRole('button', { name: 'More options' })[0]; + await userEvent.click(moreOptionsButton); + + expect(screen.queryByText('Delete')).not.toBeInTheDocument(); + }); + }); + + describe('Download', () => { + test('should download item', async () => { + const onDownload = jest.fn(); + renderComponent({ canDownload: true, onDownload }); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + const moreOptionsButton = screen.getAllByRole('button', { name: 'More options' })[2]; + await userEvent.click(moreOptionsButton); + + const downloadButton = screen.getByText('Download'); + expect(downloadButton).toBeInTheDocument(); + await userEvent.click(downloadButton); + + expect(onDownload).toHaveBeenCalledWith([ + { + ...mockRootFolder.item_collection.entries[3], + selected: true, + thumbnailUrl: + 'https://dl.boxcloud.com/api/2.0/internal_files/416044542013/versions/439751948413/representations/jpg_1024x1024/content/?access_token=token', + }, + ]); + }); + + test('should not render download button when canDownload is false', async () => { + renderComponent({ canDownload: false }); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + const moreOptionsButton = screen.getAllByRole('button', { name: 'More options' })[3]; + await userEvent.click(moreOptionsButton); + + expect(screen.queryByText('Download')).not.toBeInTheDocument(); + }); + }); + + describe('Metadata View', () => { + test('should render metadata view', async () => { + const templateName = 'templateName'; + const metadataSource = `enterprise_0.${templateName}`; + const metadataSourceFieldName = `metadata.${metadataSource}`; + const metadataQuery = { + from: metadataSource, + ancestor_folder_id: 0, + fields: [`${metadataSourceFieldName}.industry`, `${metadataSourceFieldName}.last_contacted_at`], + }; + const fieldsToShow = [ + { key: `${metadataSourceFieldName}.industry`, canEdit: false, displayName: 'Industry Alias' }, + { key: `${metadataSourceFieldName}.last_contacted_at`, canEdit: true }, + ]; + + renderComponent({ + metadataQuery, + fieldsToShow, + defaultView: 'metadata', + }); + // two separate promises need to be resolved before the component is ready + await waitFor(() => { + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + }); + + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText('Industry Alias')).toBeInTheDocument(); + expect(screen.getByText('Last Contacted At')).toBeInTheDocument(); + expect(screen.getByText('File1')).toBeInTheDocument(); + expect(screen.getByText('File2')).toBeInTheDocument(); + expect(screen.getByText('Technology')).toBeInTheDocument(); + expect(screen.getByText('November 16, 2023')).toBeInTheDocument(); + expect(screen.getByText('Healthcare')).toBeInTheDocument(); + expect(screen.getByText('November 1, 2023')).toBeInTheDocument(); + }); + }); + + describe('Preview', () => { + test('should render preview', async () => { + const onPreview = jest.fn(); + renderComponent({ onPreview }); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + const firstRow = screen.getByRole('row', { name: 'An Ordered Folder' }); + expect(firstRow).toBeInTheDocument(); + await userEvent.click(firstRow); + + const textFile = screen.getByRole('row', { name: 'XSS.txt' }); + expect(textFile).toBeInTheDocument(); + await userEvent.click(textFile); + + expect(onPreview).toHaveBeenCalled(); + }); + }); + + // describe('Search', () => { + // test('should search', async () => {}); + // }); + + describe('OnKeyDown', () => { + test('should focus search input on "/" key press', async () => { + renderComponent(); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + await userEvent.tab(); + await userEvent.keyboard('/'); + + expect(screen.getByRole('searchbox')).toHaveFocus(); + }); + + test('should focus search input on "arrowdown" key press', async () => { + renderComponent(); + const contentExplorer = screen.getByTestId('content-explorer'); + await waitFor(() => { + expect(contentExplorer).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + await userEvent.click(screen.getByRole('button', { name: 'Switch to List View' })); + contentExplorer.focus(); + await userEvent.keyboard('[ArrowDown]'); + // row 0 is the header row + expect(screen.getAllByRole('row')[1]).toHaveFocus(); + }); + + test('should focus search input on "b" key press', async () => { + renderComponent(); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + await userEvent.tab(); + await userEvent.keyboard('gb'); + + expect( + screen.getByRole('button', { + name: 'Preview Test Folder', + }), + ).toHaveFocus(); + }); + + test('should focus search input on "u" key press', async () => { + renderComponent(); + + await waitFor(() => { + expect(screen.getByTestId('content-explorer')).toBeInTheDocument(); + expect(screen.getByText('Please wait while the items load...')).toBeInTheDocument(); + }); + + await userEvent.tab(); + await userEvent.keyboard('gu'); + + expect(screen.getByLabelText('Upload')).toHaveFocus(); + }); + }); +}); diff --git a/src/elements/content-explorer/constants.js b/src/elements/content-explorer/constants.ts similarity index 100% rename from src/elements/content-explorer/constants.js rename to src/elements/content-explorer/constants.ts diff --git a/src/elements/content-explorer/index.js b/src/elements/content-explorer/index.ts similarity index 83% rename from src/elements/content-explorer/index.js rename to src/elements/content-explorer/index.ts index fae748270c..1100909099 100644 --- a/src/elements/content-explorer/index.js +++ b/src/elements/content-explorer/index.ts @@ -1,2 +1 @@ -// @flow export { default } from './ContentExplorer'; diff --git a/src/elements/content-explorer/stories/__mocks__/mockMetadata.ts b/src/elements/content-explorer/stories/__mocks__/mockMetadata.ts new file mode 100644 index 0000000000..97cd652113 --- /dev/null +++ b/src/elements/content-explorer/stories/__mocks__/mockMetadata.ts @@ -0,0 +1,144 @@ +const mockMetadata = { + entries: [ + { + name: 'File1', + etag: '2', + metadata: { + enterprise_0: { + templateName: { + $scope: 'enterprise_0', + role: ['Business Owner', 'Marketing'], + $template: 'templateName', + $parent: 'file_1188899160835', + name: 'something', + industry: 'Technology', + last_contacted_at: '2023-11-16T00:00:00.000Z', + $version: 6, + }, + }, + }, + id: '1188899160835', + modified_at: '2023-04-12T10:06:04-07:00', + type: 'file', + }, + { + name: 'File2', + etag: '1', + metadata: { + enterprise_0: { + templateName: { + $scope: 'enterprise_0', + role: ['Developer'], + $template: 'templateName', + $parent: 'file_1318276254035', + name: '1', + industry: 'Healthcare', + last_contacted_at: '2023-11-01T00:00:00.000Z', + $version: 1, + }, + }, + }, + id: '1318276254035', + modified_at: '2023-09-26T14:04:52-07:00', + type: 'file', + }, + { + name: 'File3', + etag: '0', + metadata: { + enterprise_0: { + templateName: { + $scope: 'enterprise_0', + $template: 'templateName', + $parent: 'folder_218662304788', + $version: 0, + }, + }, + }, + id: '218662304788', + modified_at: '2024-06-13T15:53:23-07:00', + type: 'folder', + }, + ], + limit: 50, +}; + +const mockSchema = { + id: '26b5527a-bdf5-4e30-a1cb-310bff2b02b9', + type: 'metadata_template', + templateKey: 'templateName', + scope: 'enterprise_0', + displayName: 'templateName', + hidden: false, + copyInstanceOnItemCopy: false, + fields: [ + { + id: '56b6f00e-5db3-4875-a31d-14b20f63c0ea', + type: 'string', + key: 'name', + displayName: 'Name', + hidden: false, + description: 'The customer name', + }, + { + id: '07d3c06c-5db4-4f3f-821e-19219ba70ed3', + type: 'date', + key: 'last_contacted_at', + displayName: 'Last Contacted At', + hidden: false, + description: 'When this customer was last contacted at', + }, + { + id: 'b03f5855-d269-4dcf-8d14-dcae89b25aa6', + type: 'enum', + key: 'industry', + displayName: 'Industry', + hidden: false, + options: [ + { + id: 'f552fe3f-0ccb-4ae1-9c21-508c49be3750', + key: 'Technology', + }, + { + id: '53006247-72b4-4719-931a-7f9327a6e31d', + key: 'Healthcare', + }, + { + id: 'bda9a5fb-8069-4977-87fd-870b8503095a', + key: 'Legal', + }, + ], + }, + { + id: '1436c58a-5df5-44b5-b854-9a49e8f50e30', + type: 'multiSelect', + key: 'role', + displayName: 'Contact Role', + hidden: false, + options: [ + { + id: '6dc57bab-b62d-4aec-8be0-fb8becae9b9a', + key: 'Developer', + }, + { + id: '5373e8e1-1e8a-4649-9e06-370407772aa1', + key: 'Business Owner', + }, + { + id: '78bdac5b-2639-4d68-9081-b8f2cdf6a9a1', + key: 'Marketing', + }, + { + id: 'e3500aa8-5643-46b7-b58d-fbd7caa9d927', + key: 'Legal', + }, + { + id: 'e7ef8b21-b6ec-4c9f-9899-4415d98e5c45', + key: 'Sales', + }, + ], + }, + ], +}; + +export { mockMetadata, mockSchema }; diff --git a/src/elements/content-explorer/stories/__mocks__/mockRootFolder.ts b/src/elements/content-explorer/stories/__mocks__/mockRootFolder.ts index 1ff6d9b26d..cc00f69f04 100644 --- a/src/elements/content-explorer/stories/__mocks__/mockRootFolder.ts +++ b/src/elements/content-explorer/stories/__mocks__/mockRootFolder.ts @@ -73,11 +73,11 @@ const mockRootFolder = { parent: null, permissions: { can_download: true, - can_upload: false, - can_rename: false, - can_delete: false, + can_upload: true, + can_rename: true, + can_delete: true, can_share: false, - can_invite_collaborator: false, + can_invite_collaborator: true, can_set_share_access: false, }, path_collection: { @@ -114,7 +114,127 @@ const mockRootFolder = { id: '73426618530', etag: '3', name: 'An Ordered Folder', - size: 202621773, + size: 200621773, + parent: { + type: 'folder', + id: '69083462919', + sequence_id: '2', + etag: '2', + name: 'Preview Test Folder', + }, + permissions: { + can_download: true, + can_upload: true, + can_rename: true, + can_delete: true, + can_share: true, + can_invite_collaborator: true, + can_set_share_access: false, + }, + path_collection: { + total_count: 2, + entries: [ + { + type: 'folder', + id: '0', + sequence_id: null, + etag: null, + name: 'All Files', + }, + { + type: 'folder', + id: '69083462919', + sequence_id: '2', + etag: '2', + name: 'Preview Test Folder', + }, + ], + }, + modified_at: '2019-04-16T15:44:44-07:00', + created_at: '2019-04-16T15:44:14-07:00', + modified_by: { + type: 'user', + id: '7503712462', + name: 'Preview', + login: 'preview@boxdemo.com', + }, + has_collaborations: true, + is_externally_owned: false, + shared_link: { + url: 'https://example.com/share-link', + permissions: { + can_preview: true, + can_download: true, + can_edit: false, + }, + }, + watermark_info: { + is_watermarked: false, + }, + }, + { + type: 'folder', + id: '73426618531', + etag: '3', + name: 'Archive', + size: 1231231, + parent: { + type: 'folder', + id: '69083462919', + sequence_id: '2', + etag: '2', + name: 'Preview Test Folder', + }, + permissions: { + can_download: true, + can_upload: true, + can_rename: true, + can_delete: true, + can_share: false, + can_invite_collaborator: true, + can_set_share_access: false, + }, + path_collection: { + total_count: 2, + entries: [ + { + type: 'folder', + id: '0', + sequence_id: null, + etag: null, + name: 'All Files', + }, + { + type: 'folder', + id: '69083462919', + sequence_id: '2', + etag: '2', + name: 'Preview Test Folder', + }, + ], + }, + modified_at: '2020-12-16T03:21:44-07:00', + created_at: '2020-11-12T09:33:22-07:00', + modified_by: { + type: 'user', + id: '7503712462', + name: 'Preview', + login: 'preview@boxdemo.com', + }, + has_collaborations: true, + is_externally_owned: false, + shared_link: null, + watermark_info: { + is_watermarked: false, + }, + archive_type: 'archive', + }, + { + type: 'folder', + id: '73426618532', + etag: '3', + name: 'Archived Folder', + size: 1031231, parent: { type: 'folder', id: '69083462919', @@ -150,6 +270,375 @@ const mockRootFolder = { }, ], }, + modified_at: '2020-12-17T05:21:44-07:00', + created_at: '2020-11-12T12:33:22-07:00', + modified_by: { + type: 'user', + id: '7503712462', + name: 'Preview', + login: 'preview@boxdemo.com', + }, + has_collaborations: true, + is_externally_owned: false, + shared_link: null, + watermark_info: { + is_watermarked: false, + }, + archive_type: 'folder_archive', + }, + { + type: 'file', + id: '416044542013', + etag: '1', + name: 'Book Sample.pdf', + size: 144481, + parent: { + type: 'folder', + id: '69083462919', + sequence_id: '2', + etag: '2', + name: 'Preview Test Folder', + }, + extension: 'pdf', + permissions: { + can_download: true, + can_preview: true, + can_upload: false, + can_comment: true, + can_rename: true, + can_delete: true, + can_share: false, + can_set_share_access: false, + can_invite_collaborator: false, + can_annotate: false, + can_view_annotations_all: true, + can_view_annotations_self: true, + can_create_annotations: true, + can_view_annotations: true, + }, + path_collection: { + total_count: 2, + entries: [ + { + type: 'folder', + id: '0', + sequence_id: null, + etag: null, + name: 'All Files', + }, + { + type: 'folder', + id: '69083462919', + sequence_id: '2', + etag: '2', + name: 'Preview Test Folder', + }, + ], + }, + modified_at: '2022-12-07T22:13:30-08:00', + created_at: '2019-03-05T12:47:51-08:00', + modified_by: { + type: 'user', + id: '7503712462', + name: 'Preview', + login: 'preview@boxdemo.com', + }, + has_collaborations: true, + is_externally_owned: false, + authenticated_download_url: 'https://dl.boxcloud.com/api/2.0/files/416044542013/content', + is_download_available: true, + representations: { + entries: [ + { + representation: 'jpg', + properties: { + dimensions: '1024x1024', + paged: 'false', + thumb: 'false', + }, + info: { + url: 'https://api.box.com/2.0/internal_files/416044542013/versions/439751948413/representations/jpg_1024x1024', + }, + status: { + state: 'success', + }, + content: { + url_template: + 'https://dl.boxcloud.com/api/2.0/internal_files/416044542013/versions/439751948413/representations/jpg_1024x1024/content/{+asset_path}', + }, + }, + ], + }, + file_version: { + type: 'file_version', + id: '439751948413', + sha1: '81fa3796742c6d194ddc54e9424f855f78009cf1', + }, + sha1: '81fa3796742c6d194ddc54e9424f855f78009cf1', + shared_link: { + url: 'https://example.com/share-link', + permissions: { + can_preview: true, + can_download: true, + can_edit: false, + }, + }, + watermark_info: { + is_watermarked: false, + }, + }, + { + type: 'file', + id: '415542803939', + etag: '3', + name: 'Document (PDF).pdf', + size: 792687, + parent: { + type: 'folder', + id: '69083462919', + sequence_id: '2', + etag: '2', + name: 'Preview Test Folder', + }, + extension: 'pdf', + permissions: { + can_download: true, + can_preview: true, + can_upload: false, + can_comment: true, + can_rename: false, + can_delete: false, + can_share: false, + can_set_share_access: false, + can_invite_collaborator: false, + can_annotate: false, + can_view_annotations_all: true, + can_view_annotations_self: true, + can_create_annotations: true, + can_view_annotations: true, + }, + path_collection: { + total_count: 2, + entries: [ + { + type: 'folder', + id: '0', + sequence_id: null, + etag: null, + name: 'All Files', + }, + { + type: 'folder', + id: '69083462919', + sequence_id: '2', + etag: '2', + name: 'Preview Test Folder', + }, + ], + }, + modified_at: '2022-12-17T23:59:57-08:00', + created_at: '2019-03-04T15:16:01-08:00', + modified_by: { + type: 'user', + id: '7503712462', + name: 'JP', + login: 'jp@boxdemo.com', + }, + has_collaborations: true, + is_externally_owned: false, + authenticated_download_url: 'https://dl.boxcloud.com/api/2.0/files/415542803939/content', + is_download_available: true, + representations: { + entries: [ + { + representation: 'jpg', + properties: { + dimensions: '1024x1024', + paged: 'false', + thumb: 'false', + }, + info: { + url: 'https://api.box.com/2.0/internal_files/415542803939/versions/780895440222/representations/jpg_1024x1024', + }, + status: { + state: 'success', + }, + content: { + url_template: + 'https://dl.boxcloud.com/api/2.0/internal_files/415542803939/versions/780895440222/representations/jpg_1024x1024/content/{+asset_path}', + }, + }, + ], + }, + file_version: { + type: 'file_version', + id: '780895440222', + sha1: '9650d7a6213181771fd38e761e2c2a330848a5fc', + }, + sha1: '9650d7a6213181771fd38e761e2c2a330848a5fc', + shared_link: null, + watermark_info: { + is_watermarked: false, + }, + }, + { + type: 'folder', + id: '118171106008', + etag: '0', + name: 'Annotations', + size: 772687, + parent: { + type: 'folder', + id: '69083462919', + sequence_id: '2', + etag: '2', + name: 'Preview Test Folder', + }, + permissions: { + can_download: true, + can_upload: false, + can_rename: false, + can_delete: false, + can_share: false, + can_invite_collaborator: false, + can_set_share_access: false, + }, + path_collection: { + total_count: 2, + entries: [ + { + type: 'folder', + id: '0', + sequence_id: null, + etag: null, + name: 'All Files', + }, + { + type: 'folder', + id: '69083462919', + sequence_id: '2', + etag: '2', + name: 'Preview Test Folder', + }, + ], + }, + modified_at: '2023-08-03T15:55:16-07:00', + created_at: '2020-07-17T15:13:58-07:00', + modified_by: { + type: 'user', + id: '9588453240', + name: 'Preview', + login: 'preview-scrum@boxdemo.com', + }, + has_collaborations: true, + is_externally_owned: false, + shared_link: null, + watermark_info: { + is_watermarked: false, + }, + }, + ], + offset: 0, + limit: 50, + order: [ + { + by: 'type', + direction: 'ASC', + }, + { + by: 'name', + direction: 'ASC', + }, + ], + }, +}; + +const mockRootFolderSharedLink = { + type: 'folder', + id: '69083462919', + etag: '2', + name: 'Preview Test Folder', + size: 1301485279, + parent: null, + permissions: { + can_download: true, + can_upload: true, + can_rename: true, + can_delete: true, + can_share: false, + can_invite_collaborator: true, + can_set_share_access: false, + }, + path_collection: { + total_count: 1, + entries: [ + { + type: 'folder', + id: '0', + sequence_id: null, + etag: null, + name: 'All Files', + }, + ], + }, + modified_at: '2024-01-16T09:50:27-08:00', + created_at: '2019-03-04T11:23:26-08:00', + modified_by: { + type: 'user', + id: '7505500060', + name: 'Preview', + login: 'preview@boxdemo.com', + }, + has_collaborations: true, + is_externally_owned: false, + shared_link: null, + watermark_info: { + is_watermarked: false, + }, + item_collection: { + total_count: 2, + entries: [ + { + type: 'folder', + id: '73426618530', + etag: '3', + name: 'An Ordered Folder', + size: 202621773, + parent: { + type: 'folder', + id: '69083462919', + sequence_id: '2', + etag: '2', + name: 'Preview Test Folder', + }, + permissions: { + can_download: true, + can_upload: true, + can_rename: true, + can_delete: true, + can_share: true, + can_invite_collaborator: true, + can_set_share_access: false, + }, + path_collection: { + total_count: 2, + entries: [ + { + type: 'folder', + id: '0', + sequence_id: null, + etag: null, + name: 'All Files', + }, + { + type: 'folder', + id: '69083462919', + sequence_id: '2', + etag: '2', + name: 'Preview Test Folder', + }, + ], + }, modified_at: '2019-04-16T15:44:44-07:00', created_at: '2019-04-16T15:44:14-07:00', modified_by: { @@ -160,7 +649,14 @@ const mockRootFolder = { }, has_collaborations: true, is_externally_owned: false, - shared_link: null, + shared_link: { + url: 'https://example.com/share-link', + permissions: { + can_preview: true, + can_download: true, + can_edit: false, + }, + }, watermark_info: { is_watermarked: false, }, @@ -180,11 +676,11 @@ const mockRootFolder = { }, permissions: { can_download: true, - can_upload: false, - can_rename: false, - can_delete: false, + can_upload: true, + can_rename: true, + can_delete: true, can_share: false, - can_invite_collaborator: false, + can_invite_collaborator: true, can_set_share_access: false, }, path_collection: { @@ -227,7 +723,7 @@ const mockRootFolder = { id: '73426618532', etag: '3', name: 'Archived Folder', - size: 1231231, + size: 1201231, parent: { type: 'folder', id: '69083462919', @@ -300,7 +796,7 @@ const mockRootFolder = { can_comment: true, can_rename: true, can_delete: true, - can_share: true, + can_share: false, can_set_share_access: false, can_invite_collaborator: false, can_annotate: false, @@ -396,4 +892,4 @@ const mockRootFolder = { }, }; -export { mockEmptyRootFolder, mockRootFolder }; +export { mockEmptyRootFolder, mockRootFolder, mockRootFolderSharedLink }; diff --git a/src/elements/content-explorer/stories/__mocks__/mockSubFolder.ts b/src/elements/content-explorer/stories/__mocks__/mockSubFolder.ts index 4881c5523c..958214f074 100644 --- a/src/elements/content-explorer/stories/__mocks__/mockSubFolder.ts +++ b/src/elements/content-explorer/stories/__mocks__/mockSubFolder.ts @@ -44,17 +44,24 @@ const mockSubFolder = { modified_by: { type: 'user', id: '7503712462', - name: 'Preview', - login: 'preview@boxdemo.com', + name: 'JP', + login: 'jp@boxdemo.com', }, has_collaborations: true, is_externally_owned: false, - shared_link: null, + shared_link: { + url: 'https://example.com/share-link', + permissions: { + can_preview: true, + can_download: true, + can_edit: false, + }, + }, watermark_info: { is_watermarked: false, }, item_collection: { - total_count: 1, + total_count: 3, entries: [ { type: 'file', @@ -117,8 +124,8 @@ const mockSubFolder = { modified_by: { type: 'user', id: '7503712462', - name: 'Preview', - login: 'preview@boxdemo.com', + name: 'JP', + login: 'jp@boxdemo.com', }, has_collaborations: true, is_externally_owned: false, @@ -138,6 +145,239 @@ const mockSubFolder = { is_watermarked: false, }, }, + { + type: 'file', + id: '441230775630', + etag: '1', + name: 'Video - Skills.mp4', + size: 199816197, + parent: { + type: 'folder', + id: '73426618530', + sequence_id: '3', + etag: '3', + name: 'An Ordered Folder', + }, + extension: 'mp4', + permissions: { + can_download: true, + can_preview: true, + can_upload: false, + can_comment: true, + can_rename: false, + can_delete: false, + can_share: false, + can_set_share_access: false, + can_invite_collaborator: false, + can_annotate: false, + can_view_annotations_all: true, + can_view_annotations_self: true, + can_create_annotations: false, + can_view_annotations: false, + }, + path_collection: { + total_count: 3, + entries: [ + { + type: 'folder', + id: '0', + sequence_id: null, + etag: null, + name: 'All Files', + }, + { + type: 'folder', + id: '69083462919', + sequence_id: '2', + etag: '2', + name: 'Preview Test Folder', + }, + { + type: 'folder', + id: '73426618530', + sequence_id: '3', + etag: '3', + name: 'An Ordered Folder', + }, + ], + }, + modified_at: '2019-04-16T15:44:18-07:00', + created_at: '2019-04-16T15:44:18-07:00', + modified_by: { + type: 'user', + id: '7503712462', + name: 'JP', + login: 'jp@boxdemo.com', + }, + has_collaborations: true, + is_externally_owned: false, + authenticated_download_url: 'https://dl.boxcloud.com/api/2.0/files/441230775630/content', + is_download_available: true, + representations: { + entries: [ + { + representation: 'jpg', + properties: { + dimensions: '1024x1024', + paged: 'false', + thumb: 'false', + }, + info: { + url: 'https://api.box.com/2.0/internal_files/441230775630/versions/466700607630/representations/jpg_1024x1024', + }, + status: { + state: 'success', + }, + content: { + url_template: + 'https://dl.boxcloud.com/api/2.0/internal_files/441230775630/versions/466700607630/representations/jpg_1024x1024/content/{+asset_path}', + }, + }, + ], + }, + file_version: { + type: 'file_version', + id: '466700607630', + sha1: '1cdb4f54bef441cb875abec95855bfd19c5f8508', + }, + sha1: '1cdb4f54bef441cb875abec95855bfd19c5f8508', + shared_link: null, + watermark_info: { + is_watermarked: false, + }, + }, + { + type: 'file', + id: '441230773230', + etag: '1', + name: 'XSS.txt', + size: 33425, + parent: { + type: 'folder', + id: '73426618530', + sequence_id: '3', + etag: '3', + name: 'An Ordered Folder', + }, + extension: 'txt', + permissions: { + can_download: true, + can_preview: true, + can_upload: false, + can_comment: true, + can_rename: false, + can_delete: false, + can_share: false, + can_set_share_access: false, + can_invite_collaborator: false, + can_annotate: false, + can_view_annotations_all: true, + can_view_annotations_self: true, + can_create_annotations: false, + can_view_annotations: false, + }, + path_collection: { + total_count: 3, + entries: [ + { + type: 'folder', + id: '0', + sequence_id: null, + etag: null, + name: 'All Files', + }, + { + type: 'folder', + id: '69083462919', + sequence_id: '2', + etag: '2', + name: 'Preview Test Folder', + }, + { + type: 'folder', + id: '73426618530', + sequence_id: '3', + etag: '3', + name: 'An Ordered Folder', + }, + ], + }, + modified_at: '2019-04-16T15:44:17-07:00', + created_at: '2019-04-16T15:44:17-07:00', + modified_by: { + type: 'user', + id: '7503712462', + name: 'JP', + login: 'jp@boxdemo.com', + }, + has_collaborations: true, + is_externally_owned: false, + authenticated_download_url: 'https://dl.boxcloud.com/api/2.0/files/441230773230/content', + is_download_available: true, + representations: { + entries: [ + { + representation: 'png', + properties: { + dimensions: '1024x1024', + paged: 'true', + thumb: 'false', + }, + info: { + url: 'https://api.box.com/2.0/internal_files/441230773230/versions/466700605230/representations/png_paged_1024x1024', + }, + status: { + state: 'success', + }, + content: { + url_template: + 'https://dl.boxcloud.com/api/2.0/internal_files/441230773230/versions/466700605230/representations/png_paged_1024x1024/content/{+asset_path}', + }, + metadata: { + pages: 9, + }, + }, + { + representation: 'pdf', + properties: {}, + info: { + url: 'https://api.box.com/2.0/internal_files/441230773230/versions/466700605230/representations/pdf', + }, + status: { + state: 'success', + }, + content: { + url_template: + 'https://dl.boxcloud.com/api/2.0/internal_files/441230773230/versions/466700605230/representations/pdf/content/{+asset_path}', + }, + }, + { + representation: 'text', + properties: {}, + info: { + url: 'https://api.box.com/2.0/internal_files/441230773230/versions/466700605230/representations/text', + }, + status: { + state: 'success', + }, + content: { + url_template: + 'https://dl.boxcloud.com/api/2.0/internal_files/441230773230/versions/466700605230/representations/text/content/{+asset_path}', + }, + }, + ], + }, + file_version: { + type: 'file_version', + id: '466700605230', + sha1: '8358fa238c349cb4ab1741cb41d90b5f43927723', + }, + sha1: '8358fa238c349cb4ab1741cb41d90b5f43927723', + shared_link: null, + watermark_info: { + is_watermarked: false, + }, + }, ], offset: 0, limit: 50, diff --git a/src/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js b/src/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js index 7e6fbdd9ce..3181323120 100644 --- a/src/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js +++ b/src/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js @@ -42,15 +42,17 @@ export const openDeleteConfirmationDialog = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const moreOptionsButton = await canvas.findByRole('button', { name: 'More options' }); - await userEvent.click(moreOptionsButton); + const moreOptionsButton = await canvas.findAllByRole('button', { name: 'More options' }); + await userEvent.click(moreOptionsButton[0]); const dropdown = await screen.findByRole('menu'); const deleteButton = within(dropdown).getByText('Delete'); expect(deleteButton).toBeInTheDocument(); - await userEvent.click(deleteButton); - expect(await screen.findByText('Are you sure you want to delete Book Sample.pdf?')).toBeInTheDocument(); + await userEvent.click(deleteButton); + expect( + await screen.findByText('Are you sure you want to delete An Ordered Folder and all its contents?'), + ).toBeInTheDocument(); }, }; @@ -58,20 +60,25 @@ export const closeDeleteConfirmationDialog = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const moreOptionsButton = await canvas.findByRole('button', { name: 'More options' }); - await userEvent.click(moreOptionsButton); + const moreOptionsButton = await canvas.findAllByRole('button', { name: 'More options' }); + await userEvent.click(moreOptionsButton[0]); const dropdown = await screen.findByRole('menu'); const deleteButton = within(dropdown).getByText('Delete'); expect(deleteButton).toBeInTheDocument(); + await userEvent.click(deleteButton); - expect(await screen.findByText('Are you sure you want to delete Book Sample.pdf?')).toBeInTheDocument(); + expect( + await screen.findByText('Are you sure you want to delete An Ordered Folder and all its contents?'), + ).toBeInTheDocument(); const cancelButton = screen.getByText('Cancel'); await userEvent.click(cancelButton); await waitFor(() => { - expect(screen.queryByText('Are you sure you want to delete Book Sample.pdf?')).not.toBeInTheDocument(); + expect( + screen.queryByText('Are you sure you want to delete An Ordered Folder and all its contents?'), + ).not.toBeInTheDocument(); }); }, }; @@ -80,15 +87,15 @@ export const openRenameDialog = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const moreOptionsButton = await canvas.findByRole('button', { name: 'More options' }); - await userEvent.click(moreOptionsButton); + const moreOptionsButton = await canvas.findAllByRole('button', { name: 'More options' }); + await userEvent.click(moreOptionsButton[0]); const dropdown = await screen.findByRole('menu'); const renameButton = within(dropdown).getByText('Rename'); expect(renameButton).toBeInTheDocument(); await userEvent.click(renameButton); - expect(await screen.findByText('Please enter a new name for Book Sample:')).toBeInTheDocument(); + expect(await screen.findByText('Please enter a new name for An Ordered Folder:')).toBeInTheDocument(); }, }; @@ -96,20 +103,20 @@ export const closeRenameDialog = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const moreOptionsButton = await canvas.findByRole('button', { name: 'More options' }); - await userEvent.click(moreOptionsButton); + const moreOptionsButton = await canvas.findAllByRole('button', { name: 'More options' }); + await userEvent.click(moreOptionsButton[0]); const dropdown = await screen.findByRole('menu'); const renameButton = within(dropdown).getByText('Rename'); expect(renameButton).toBeInTheDocument(); await userEvent.click(renameButton); - expect(await screen.findByText('Please enter a new name for Book Sample:')).toBeInTheDocument(); + expect(await screen.findByText('Please enter a new name for An Ordered Folder:')).toBeInTheDocument(); const cancelButton = screen.getByText('Cancel'); await userEvent.click(cancelButton); await waitFor(() => { - expect(screen.queryByText('Please enter a new name for Book Sample:')).not.toBeInTheDocument(); + expect(screen.queryByText('Please enter a new name for An Ordered Folder:')).not.toBeInTheDocument(); }); }, }; @@ -118,8 +125,8 @@ export const openShareDialog = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const moreOptionsButton = await canvas.findByRole('button', { name: 'More options' }); - await userEvent.click(moreOptionsButton); + const moreOptionsButton = await canvas.findAllByRole('button', { name: 'More options' }); + await userEvent.click(moreOptionsButton[0]); const dropdown = await screen.findByRole('menu'); const shareButton = within(dropdown).getByText('Share'); @@ -137,8 +144,8 @@ export const closeShareDialog = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const moreOptionsButton = await canvas.findByRole('button', { name: 'More options' }); - await userEvent.click(moreOptionsButton); + const moreOptionsButton = await canvas.findAllByRole('button', { name: 'More options' }); + await userEvent.click(moreOptionsButton[0]); const dropdown = await screen.findByRole('menu'); const shareButton = within(dropdown).getByText('Share'); diff --git a/src/elements/content-uploader/ContentUploader.tsx b/src/elements/content-uploader/ContentUploader.tsx index df4a59a294..b352b5a5e9 100644 --- a/src/elements/content-uploader/ContentUploader.tsx +++ b/src/elements/content-uploader/ContentUploader.tsx @@ -6,6 +6,7 @@ import getProp from 'lodash/get'; import noop from 'lodash/noop'; import uniqueid from 'lodash/uniqueId'; import { TooltipProvider } from '@box/blueprint-web'; +import { AxiosRequestConfig, AxiosResponse } from 'axios'; import DroppableContent from './DroppableContent'; import Footer from './Footer'; import UploadsManager from './UploadsManager'; @@ -90,8 +91,8 @@ export interface ContentUploaderProps { onUpgradeCTAClick?: () => void; onUpload: (item?: UploadItem | BoxItem) => void; overwrite: boolean; - requestInterceptor?: Function; - responseInterceptor?: Function; + requestInterceptor?: (response: AxiosResponse) => void; + responseInterceptor?: (config: AxiosRequestConfig) => void; rootFolderId: string; sharedLink?: string; sharedLinkPassword?: string; diff --git a/src/elements/index.js b/src/elements/index.js index 2e6a3a2d81..b6a3e3cd4b 100644 --- a/src/elements/index.js +++ b/src/elements/index.js @@ -1,5 +1,5 @@ // @flow - +// $FlowFixMe export { default as ContentExplorer } from './content-explorer'; export { default as ContentPreview } from './content-preview'; export { default as ContentPicker, ContentPickerPopup } from './content-picker'; diff --git a/src/elements/wrappers/ContentExplorer.js b/src/elements/wrappers/ContentExplorer.js index e50bf62271..03a082f6c6 100644 --- a/src/elements/wrappers/ContentExplorer.js +++ b/src/elements/wrappers/ContentExplorer.js @@ -9,6 +9,7 @@ import * as React from 'react'; // eslint-disable-next-line react/no-deprecated import { render } from 'react-dom'; import ES6Wrapper from './ES6Wrapper'; +// $FlowFixMe import ContentExplorerReactComponent from '../content-explorer'; import type { BoxItem } from '../../common/types/core';