diff --git a/.prettierignore b/.prettierignore index b43bf86b50..1604bd4b3f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ README.md +flow-typed/* diff --git a/flow-typed/npm/react-intl_v2.x.x.js b/flow-typed/npm/react-intl_v2.x.x.js index bb739aac5a..e778581a15 100644 --- a/flow-typed/npm/react-intl_v2.x.x.js +++ b/flow-typed/npm/react-intl_v2.x.x.js @@ -260,4 +260,5 @@ declare module "react-intl" { > {} declare type IntlShape = $npm$ReactIntl$IntlShape; declare type MessageDescriptor = $npm$ReactIntl$MessageDescriptor; + declare function useIntl(): $npm$ReactIntl$IntlShape; } diff --git a/src/elements/content-explorer/PreviewDialog.js b/src/elements/content-explorer/PreviewDialog.js deleted file mode 100644 index 34daacdccf..0000000000 --- a/src/elements/content-explorer/PreviewDialog.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * @flow - * @file Content Explorer Preview Dialog - * @author Box - */ - -import * as React from 'react'; -import Modal from 'react-modal'; -import { injectIntl } from 'react-intl'; -import type { IntlShape } from 'react-intl'; -import cloneDeep from 'lodash/cloneDeep'; -import messages from '../common/messages'; -import ContentPreview 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'; -import type APICache from '../../utils/Cache'; - -type Props = { - apiHost: string, - appElement: HTMLElement, - appHost: string, - cache: APICache, - canDownload: boolean, - contentPreviewProps: ContentPreviewProps, - currentCollection: Collection, - intl: IntlShape, - isOpen: boolean, - isTouch: boolean, - item: BoxItem, - onCancel: Function, - onDownload: Function, - onPreview: Function, - parentElement: HTMLElement, - previewLibraryVersion: string, - requestInterceptor?: Function, - responseInterceptor?: Function, - sharedLink?: string, - sharedLinkPassword?: string, - staticHost: string, - staticPath: string, - token: Token, -}; - -const PreviewDialog = ({ - item, - isOpen, - parentElement, - appElement, - token, - cache, - currentCollection, - canDownload, - onCancel, - onPreview, - onDownload, - apiHost, - appHost, - staticHost, - staticPath, - previewLibraryVersion, - sharedLink, - sharedLinkPassword, - contentPreviewProps, - requestInterceptor, - responseInterceptor, - intl, -}: Props) => { - const { items }: Collection = currentCollection; - const onLoad = (data: any): void => { - onPreview(cloneDeep(data)); - }; - - if (!item || !items) { - return null; - } - - const files: BoxItem[] = items.filter(({ type }) => type === TYPE_FILE); - return ( - parentElement} - portalClassName={`${CLASS_MODAL} be-modal-preview`} - className={CLASS_MODAL_CONTENT_FULL_BLEED} - overlayClassName={CLASS_MODAL_OVERLAY} - contentLabel={intl.formatMessage(messages.preview)} - onRequestClose={onCancel} - appElement={appElement} - > - - - ); -}; - -export default injectIntl(PreviewDialog); diff --git a/src/elements/content-explorer/PreviewDialog.js.flow b/src/elements/content-explorer/PreviewDialog.js.flow new file mode 100644 index 0000000000..ff79a466fe --- /dev/null +++ b/src/elements/content-explorer/PreviewDialog.js.flow @@ -0,0 +1,110 @@ +/** + * @flow + * @file Content Explorer Preview Dialog + * @author Box + */ + +import * as React from 'react'; +import { useIntl } from 'react-intl'; +import { Modal } from '@box/blueprint-web'; +import cloneDeep from 'lodash/cloneDeep'; + +import ContentPreview from '../content-preview'; +import { TYPE_FILE } from '../../constants'; +import type { ContentPreviewProps } from '../content-preview'; +import type { Token, BoxItem, Collection } from '../../common/types/core'; +import type APICache from '../../utils/Cache'; + +import messages from '../common/messages'; + +type PreviewDialogProps = { + apiHost: string, + appHost: string, + cache: APICache, + canDownload: boolean, + contentPreviewProps: ContentPreviewProps, + currentCollection: Collection, + isOpen: boolean, + isTouch: boolean, + item: BoxItem, + onCancel: any, + onDownload: any, + onPreview: any, + parentElement: HTMLElement, + previewLibraryVersion: string, + responseInterceptor?: any, + requestInterceptor?: any, + sharedLink?: string, + sharedLinkPassword?: string, + staticHost: string, + staticPath: string, + token: Token, +}; + +const PreviewDialog = ({ + apiHost, + appHost, + cache, + canDownload, + contentPreviewProps, + currentCollection, + isOpen, + item, + onCancel, + onDownload, + onPreview, + parentElement, + previewLibraryVersion, + requestInterceptor, + responseInterceptor, + sharedLink, + sharedLinkPassword, + staticHost, + staticPath, + token, +}: PreviewDialogProps) => { + const { formatMessage } = useIntl(); + const { items }: Collection = currentCollection; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const onLoad = (data: any): void => { + if (onPreview) { + onPreview(cloneDeep(data)); + } + }; + + if (!item || !items) { + return null; + } + + const files: BoxItem[] = items.filter(({ type }) => type === TYPE_FILE); + return ( + + + + + + ); +}; + +export default PreviewDialog; diff --git a/src/elements/content-explorer/PreviewDialog.scss b/src/elements/content-explorer/PreviewDialog.scss new file mode 100644 index 0000000000..4b2c5e34eb --- /dev/null +++ b/src/elements/content-explorer/PreviewDialog.scss @@ -0,0 +1,16 @@ +[class^='bp_modal_module_content'].bce-PreviewDialog { + position: fixed; + top: 0; + right: 0; + z-index: 1000; + display: grid; + max-width: 100%; + max-height: 100%; + margin: 0; + inset: 0; + border-radius: 0; + + .be.bcpr { + height: 100vh; + } +} diff --git a/src/elements/content-explorer/PreviewDialog.tsx b/src/elements/content-explorer/PreviewDialog.tsx new file mode 100644 index 0000000000..9df486d07b --- /dev/null +++ b/src/elements/content-explorer/PreviewDialog.tsx @@ -0,0 +1,106 @@ +import * as React from 'react'; +import { useIntl } from 'react-intl'; +import { Modal } from '@box/blueprint-web'; +import cloneDeep from 'lodash/cloneDeep'; + +import ContentPreview, { ContentPreviewProps } from '../content-preview'; +import { TYPE_FILE } from '../../constants'; +import type { Token, BoxItem, Collection } from '../../common/types/core'; +import type APICache from '../../utils/Cache'; + +import messages from '../common/messages'; + +import './PreviewDialog.scss'; + +export interface PreviewDialogProps { + apiHost: string; + appHost: string; + cache: APICache; + canDownload: boolean; + contentPreviewProps: ContentPreviewProps; + currentCollection: Collection; + isOpen: boolean; + isTouch: boolean; + item: BoxItem; + onCancel: () => void; + onDownload: () => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onPreview: (data: any) => void; + parentElement: HTMLElement; + previewLibraryVersion: string; + requestInterceptor?: () => void; + responseInterceptor?: () => void; + sharedLink?: string; + sharedLinkPassword?: string; + staticHost: string; + staticPath: string; + token: Token; +} + +const PreviewDialog = ({ + apiHost, + appHost, + cache, + canDownload, + contentPreviewProps, + currentCollection, + isOpen, + item, + onCancel, + onDownload, + onPreview, + parentElement, + previewLibraryVersion, + requestInterceptor, + responseInterceptor, + sharedLink, + sharedLinkPassword, + staticHost, + staticPath, + token, +}: PreviewDialogProps) => { + const { formatMessage } = useIntl(); + const { items }: Collection = currentCollection; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const onLoad = (data: any): void => { + if (onPreview) { + onPreview(cloneDeep(data)); + } + }; + + if (!item || !items) { + return null; + } + + const files: BoxItem[] = items.filter(({ type }) => type === TYPE_FILE); + return ( + + + + + + ); +}; + +export default PreviewDialog; diff --git a/src/elements/content-explorer/__tests__/PreviewDialog.test.tsx b/src/elements/content-explorer/__tests__/PreviewDialog.test.tsx new file mode 100644 index 0000000000..347e748a59 --- /dev/null +++ b/src/elements/content-explorer/__tests__/PreviewDialog.test.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import userEvent from '@testing-library/user-event'; +import { render, screen, waitFor } from '../../../test-utils/testing-library'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import APICache from '../../../utils/Cache'; + +import PreviewDialog, { PreviewDialogProps } from '../PreviewDialog'; + +describe('elements/content-explorer/PreviewDialog', () => { + const defaultProps = { + apiHost: 'https://api.box.com', + appHost: 'https://app.box.com', + cache: new APICache(), + canDownload: true, + contentPreviewProps: {}, + currentCollection: { items: [{ id: '1', type: 'file' }] }, + isOpen: true, + isTouch: false, + item: { id: '1', type: 'file' }, + onCancel: jest.fn(), + onDownload: jest.fn(), + onPreview: jest.fn(), + parentElement: document.body, + previewLibraryVersion: '1.0.0', + staticHost: 'https://static.box.com', + staticPath: '/static', + token: 'token', + }; + + const renderComponent = (props?: Partial) => + render(); + + test('renders', () => { + renderComponent(); + expect(screen.getByLabelText('Preview')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Close' })); + }); + + test('calls onCancel when modal is closed', async () => { + renderComponent(); + const closeButton = screen.getByRole('button', { name: 'Close' }); + await userEvent.click(closeButton); + expect(defaultProps.onCancel).toHaveBeenCalled(); + }); + + test('does not render when item is null', () => { + const props = { item: null }; + const { container } = renderComponent(props); + expect(container.firstChild).toBeNull(); + }); + + test('does not render when items are null', () => { + const props = { currentCollection: { items: null } }; + const { container } = renderComponent(props); + expect(container.firstChild).toBeNull(); + }); + + test('calls onPreview with cloned data on load', () => { + const data = { id: '1', type: 'file' }; + renderComponent(); + waitFor(() => { + expect(defaultProps.onPreview).toHaveBeenCalledWith(expect.objectContaining(data)); + }); + }); +}); diff --git a/src/elements/content-explorer/stories/PreviewDialog.stories.tsx b/src/elements/content-explorer/stories/PreviewDialog.stories.tsx new file mode 100644 index 0000000000..fbd23cd701 --- /dev/null +++ b/src/elements/content-explorer/stories/PreviewDialog.stories.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; +import { useArgs } from '@storybook/preview-api'; +import { Button } from '@box/blueprint-web'; + +import { addRootElement } from '../../../utils/storybook'; + +import PreviewDialog from '../PreviewDialog'; + +export const previewDialog = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + render: (args: any) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const [, setArgs] = useArgs(); + + const handleOpenModal = () => setArgs({ isOpen: true }); + + const handleCloseModal = () => { + setArgs({ isOpen: false }); + }; + + const { rootElement } = addRootElement(); + + return ( +
+ + + +
+ ); + }, +}; + +export default { + title: 'Elements/ContentExplorer', + component: PreviewDialog, + args: { + isLoading: false, + isOpen: false, + token: global.TOKEN, + }, +};