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.flow similarity index 71% rename from src/elements/content-explorer/PreviewDialog.js rename to src/elements/content-explorer/PreviewDialog.js.flow index 34daacdccf..46cf48ac52 100644 --- a/src/elements/content-explorer/PreviewDialog.js +++ b/src/elements/content-explorer/PreviewDialog.js.flow @@ -5,35 +5,36 @@ */ import * as React from 'react'; +import { useIntl } from 'react-intl'; 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 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'; import type APICache from '../../utils/Cache'; -type Props = { +import messages from '../common/messages'; + +import './PreviewDialog.scss'; + +type PreviewDialogProps = { 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, + onCancel: any, + onDownload: any, + onPreview: any, parentElement: HTMLElement, previewLibraryVersion: string, - requestInterceptor?: Function, - responseInterceptor?: Function, + responseInterceptor?: any, + requestInterceptor?: any, sharedLink?: string, sharedLinkPassword?: string, staticHost: string, @@ -42,32 +43,35 @@ type Props = { }; 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) => { + appElement, + 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 => { - onPreview(cloneDeep(data)); + if (onPreview) { + onPreview(cloneDeep(data)); + } }; if (!item || !items) { @@ -82,34 +86,33 @@ const PreviewDialog = ({ portalClassName={`${CLASS_MODAL} be-modal-preview`} className={CLASS_MODAL_CONTENT_FULL_BLEED} overlayClassName={CLASS_MODAL_OVERLAY} - contentLabel={intl.formatMessage(messages.preview)} + contentLabel={formatMessage(messages.preview)} onRequestClose={onCancel} appElement={appElement} > ); }; - -export default injectIntl(PreviewDialog); +export default PreviewDialog; diff --git a/src/elements/content-explorer/PreviewDialog.tsx b/src/elements/content-explorer/PreviewDialog.tsx new file mode 100644 index 0000000000..c01b60302d --- /dev/null +++ b/src/elements/content-explorer/PreviewDialog.tsx @@ -0,0 +1,113 @@ +import * as React from 'react'; +import { useIntl } from 'react-intl'; +import Modal from 'react-modal'; +import cloneDeep from 'lodash/cloneDeep'; + +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'; +import type APICache from '../../utils/Cache'; + +import messages from '../common/messages'; + +export interface PreviewDialogProps { + appElement: HTMLElement; + 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 = ({ + appElement, + 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 ( + parentElement} + portalClassName={`${CLASS_MODAL} be-modal-preview`} + className={CLASS_MODAL_CONTENT_FULL_BLEED} + overlayClassName={CLASS_MODAL_OVERLAY} + contentLabel={formatMessage(messages.preview)} + onRequestClose={onCancel} + appElement={appElement} + > + + + ); +}; + +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..a93e3f0298 --- /dev/null +++ b/src/elements/content-explorer/__tests__/PreviewDialog.test.tsx @@ -0,0 +1,80 @@ +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'; + +jest.mock('react-modal', () => { + return jest.fn(({ children }) =>
{children}
); +}); + +describe('elements/content-explorer/PreviewDialog', () => { + const defaultProps = { + appElement: document.body, + 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.createElement('div'), + previewLibraryVersion: '1.0.0', + staticHost: 'https://static.box.com', + staticPath: '/static', + token: 'token', + }; + + const renderComponent = (props?: Partial) => + render(); + + test('renders', async () => { + renderComponent(); + expect(await screen.findByLabelText('Preview')).toBeInTheDocument(); + expect(await screen.findByRole('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)); + }); + }); + + test('does not call onPreview with cloned data on load', () => { + const data = { id: '1', type: 'file' }; + renderComponent({ onPreview: null }); + waitFor(() => { + expect(defaultProps.onPreview).not.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..fcbb0fe705 --- /dev/null +++ b/src/elements/content-explorer/stories/PreviewDialog.stories.tsx @@ -0,0 +1,83 @@ +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'; + +// need to import this into the story because it's usually in ContentExplorer +import '../../common/modal.scss'; + +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 { appElement, rootElement } = addRootElement(); + + return ( +
+ + + +
+ ); + }, +}; + +export default { + title: 'Elements/ContentExplorer', + component: PreviewDialog, + args: { + isLoading: false, + isOpen: false, + token: global.TOKEN, + }, +}; diff --git a/src/elements/content-explorer/stories/tests/PreviewDialog-visual.stories.tsx b/src/elements/content-explorer/stories/tests/PreviewDialog-visual.stories.tsx new file mode 100644 index 0000000000..6c96a6c41e --- /dev/null +++ b/src/elements/content-explorer/stories/tests/PreviewDialog-visual.stories.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import { useArgs } from '@storybook/preview-api'; +import { Button } from '@box/blueprint-web'; +import { userEvent, within } from '@storybook/test'; + +import { addRootElement } from '../../../../utils/storybook'; + +import PreviewDialog from '../../PreviewDialog'; + +// need to import this into the story because it's usually in ContentExplorer +import '../../../common/modal.scss'; + +export const basic = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const button = canvas.getByRole('button', { name: 'Launch PreviewDialog' }); + await userEvent.click(button); + expect(canvas.getByText('Book Sample.pdf')).toBeInTheDocument(); + }, + // 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 { appElement, rootElement } = addRootElement(); + + return ( +
+ + + +
+ ); + }, +}; + +export default { + title: 'Elements/ContentExplorer/tests/PreviewDialog/visual', + component: PreviewDialog, + args: { + token: global.TOKEN, + }, +};