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/DeleteConfirmationDialog.js b/src/elements/content-explorer/DeleteConfirmationDialog.js deleted file mode 100644 index 2586d09c10..0000000000 --- a/src/elements/content-explorer/DeleteConfirmationDialog.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * @flow - * @file Content Explorer Delete Confirmation Dialog - * @author Box - */ - -import * as React from 'react'; -import Modal from 'react-modal'; -import { injectIntl, FormattedMessage } from 'react-intl'; -import type { IntlShape } from 'react-intl'; -import PrimaryButton from '../../components/primary-button/PrimaryButton'; -import Button from '../../components/button/Button'; -import messages from '../common/messages'; -import { CLASS_MODAL_CONTENT, CLASS_MODAL_OVERLAY, CLASS_MODAL, TYPE_FOLDER } from '../../constants'; -import type { BoxItem } from '../../common/types/core'; - -import './DeleteConfirmationDialog.scss'; - -type Props = { - appElement: HTMLElement, - intl: IntlShape, - isLoading: boolean, - isOpen: boolean, - item: BoxItem, - onCancel: Function, - onDelete: Function, - parentElement: HTMLElement, -}; - -const DeleteConfirmationDialog = ({ - isOpen, - onDelete, - onCancel, - item, - isLoading, - parentElement, - appElement, - intl, -}: Props) => { - const message = item.type === TYPE_FOLDER ? messages.deleteDialogFolderText : messages.deleteDialogFileText; - return ( - parentElement} - portalClassName={`${CLASS_MODAL} be-modal-delete`} - > - -
- - - - -
-
- ); -}; - -export default injectIntl(DeleteConfirmationDialog); diff --git a/src/elements/content-explorer/DeleteConfirmationDialog.js.flow b/src/elements/content-explorer/DeleteConfirmationDialog.js.flow new file mode 100644 index 0000000000..1d2357fafd --- /dev/null +++ b/src/elements/content-explorer/DeleteConfirmationDialog.js.flow @@ -0,0 +1,51 @@ +// @flow +import * as React from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { Modal } from '@box/blueprint-web'; +import type { BoxItem } from '../../common/types/core'; + +import { TYPE_FOLDER } from '../../constants'; + +import messages from '../common/messages'; + +type Props = { + isLoading: boolean, + isOpen: boolean, + item: BoxItem, + onCancel: any, + onDelete: any, + parentElement: HTMLElement, +}; + +const DeleteConfirmationDialog = ({ isOpen, onDelete, onCancel, item, isLoading, parentElement }: Props) => { + const { formatMessage } = useIntl(); + const message = item.type === TYPE_FOLDER ? messages.deleteDialogFolderText : messages.deleteDialogFileText; + return ( + + + + + + + + {formatMessage(messages.cancel)} + + + {formatMessage(messages.delete)} + + + + + ); +}; + +export default DeleteConfirmationDialog; diff --git a/src/elements/content-explorer/DeleteConfirmationDialog.scss b/src/elements/content-explorer/DeleteConfirmationDialog.scss deleted file mode 100644 index 68317c27d5..0000000000 --- a/src/elements/content-explorer/DeleteConfirmationDialog.scss +++ /dev/null @@ -1,5 +0,0 @@ -.be-modal.be-modal-delete { - .be-modal-dialog-content { - overflow-wrap: break-word; - } -} diff --git a/src/elements/content-explorer/DeleteConfirmationDialog.tsx b/src/elements/content-explorer/DeleteConfirmationDialog.tsx new file mode 100644 index 0000000000..a2793cdea6 --- /dev/null +++ b/src/elements/content-explorer/DeleteConfirmationDialog.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { Modal } from '@box/blueprint-web'; +import type { BoxItem } from '../../common/types/core'; + +import { TYPE_FOLDER } from '../../constants'; + +import messages from '../common/messages'; + +export interface DeleteConfirmationDialogProps { + isLoading: boolean; + isOpen: boolean; + item: BoxItem; + onCancel: () => void; + onDelete: () => void; + parentElement: HTMLElement; +} + +const DeleteConfirmationDialog = ({ + isOpen, + onDelete, + onCancel, + item, + isLoading, + parentElement, +}: DeleteConfirmationDialogProps) => { + const { formatMessage } = useIntl(); + const message = item.type === TYPE_FOLDER ? messages.deleteDialogFolderText : messages.deleteDialogFileText; + return ( + + + + + + + + {formatMessage(messages.cancel)} + + + {formatMessage(messages.delete)} + + + + + ); +}; + +export default DeleteConfirmationDialog; diff --git a/src/elements/content-explorer/__tests__/DeleteConfirmation.test.tsx b/src/elements/content-explorer/__tests__/DeleteConfirmation.test.tsx new file mode 100644 index 0000000000..21a1be260c --- /dev/null +++ b/src/elements/content-explorer/__tests__/DeleteConfirmation.test.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import userEvent from '@testing-library/user-event'; +import { render, screen } from '../../../test-utils/testing-library'; +import DeleteConfirmationDialog, { DeleteConfirmationDialogProps } from '../DeleteConfirmationDialog'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { TYPE_FOLDER } from '../../../constants'; + +const mockItem = { type: 'pdf', name: 'Test File' }; +const mockFolderItem = { type: TYPE_FOLDER, name: 'Test Folder' }; +const mockOnCancel = jest.fn(); +const mockOnDelete = jest.fn(); + +const defaultProps = { + errorCode: '', + isLoading: false, + isOpen: false, + item: mockItem, + onCancel: jest.fn(), + onDelete: jest.fn(), + parentElement: document.body, +}; + +const renderComponent = (props: Partial) => + render(); + +describe('elements/content-explorer/DeleteConfirmationDialog', () => { + test('should render the dialog with file message', async () => { + renderComponent({ isOpen: true }); + + expect(await screen.findByText('Are you sure you want to delete Test File?')).toBeInTheDocument(); + }); + + test('should render the dialog with folder message', async () => { + renderComponent({ isOpen: true, item: mockFolderItem }); + + expect( + await screen.findByText('Are you sure you want to delete Test Folder and all its contents?'), + ).toBeInTheDocument(); + }); + + test('should call onCancel when cancel button is clicked', async () => { + renderComponent({ isOpen: true, onCancel: mockOnCancel }); + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + await userEvent.click(cancelButton); + expect(mockOnCancel).toBeCalledTimes(1); + }); + + test('should call onDelete when delete button is clicked', async () => { + renderComponent({ isOpen: true, onDelete: mockOnDelete }); + + const deleteButton = screen.getByRole('button', { name: 'Delete' }); + await userEvent.click(deleteButton); + expect(mockOnDelete).toBeCalledTimes(1); + }); + + test('should disable buttons when isLoading is true', () => { + renderComponent({ isOpen: true, isLoading: true }); + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + expect(cancelButton).toBeDisabled(); + const loadingIndicator = screen.getByRole('status', { name: 'Loading' }); + expect(loadingIndicator).toBeInTheDocument(); + }); +}); diff --git a/src/elements/content-explorer/stories/DeleteConfirmationDialog.stories.js b/src/elements/content-explorer/stories/DeleteConfirmationDialog.stories.tsx similarity index 51% rename from src/elements/content-explorer/stories/DeleteConfirmationDialog.stories.js rename to src/elements/content-explorer/stories/DeleteConfirmationDialog.stories.tsx index 5d5bb24c68..eb0c2086de 100644 --- a/src/elements/content-explorer/stories/DeleteConfirmationDialog.stories.js +++ b/src/elements/content-explorer/stories/DeleteConfirmationDialog.stories.tsx @@ -1,17 +1,13 @@ -// @flow import * as React from 'react'; import { useArgs } from '@storybook/preview-api'; -import PrimaryButton from '../../../components/primary-button/PrimaryButton'; +import { Button } from '@box/blueprint-web'; import { addRootElement } from '../../../utils/storybook'; import DeleteConfirmationDialog from '../DeleteConfirmationDialog'; -// need to import this into the story because it's usually in ContentExplorer -import '../../common/modal.scss'; - export const deleteDialog = { - // eslint-disable-next-line react/prop-types + // eslint-disable-next-line @typescript-eslint/no-explicit-any render: (args: any) => { // eslint-disable-next-line react-hooks/rules-of-hooks const [, setArgs] = useArgs(); @@ -22,27 +18,26 @@ export const deleteDialog = { setArgs({ isOpen: false }); }; - const { appElement, rootElement } = addRootElement(); + const { rootElement } = addRootElement(); return (
- - Launch DeleteConfirmationDialog +
); }, -}; +} as const; export default { title: 'Elements/ContentExplorer', 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 1cdd99410f..bdbd8692a3 100644 --- a/src/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js +++ b/src/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js @@ -39,7 +39,7 @@ export const openDeleteConfirmationDialog = { await userEvent.click(moreOptionsButton); const dropdown = await screen.findByRole('menu'); - const deleteButton = within(dropdown).getByText('Delete'); + const deleteButton = within(dropdown).findByText('Delete'); expect(deleteButton).toBeInTheDocument(); await userEvent.click(deleteButton); diff --git a/src/elements/content-explorer/stories/tests/DeleteConfirmationDialog-visual.stories.js b/src/elements/content-explorer/stories/tests/DeleteConfirmationDialog-visual.stories.js index 62361634f4..aa06d800c9 100644 --- a/src/elements/content-explorer/stories/tests/DeleteConfirmationDialog-visual.stories.js +++ b/src/elements/content-explorer/stories/tests/DeleteConfirmationDialog-visual.stories.js @@ -4,44 +4,24 @@ import { addRootElement, defaultVisualConfig } from '../../../../utils/storybook import DeleteConfirmationDialog from '../../DeleteConfirmationDialog'; -// need to import this into the story because it's usually in ContentExplorer -import '../../../common/modal.scss'; - const item = { id: '123456', - name: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Eget nulla facilisi etiam dignissim diam quis enim lobortis scelerisque. Aliquam faucibus purus in massa tempor nec. Ut consequat semper viverra nam libero justo laoreet sit amet. Purus gravida quis blandit turpis cursus in hac. Dui ut ornare lectus sit amet est. Nisl condimentum id venenatis a condimentum vitae sapien ', + name: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Eget nulla facilisi etiam dignissim diam quis enim lobortis scelerisque. Aliquam faucibus purus in massa tempor nec. Ut consequat semper viverra nam libero justo laoreet sit amet. Purus gravida quis blandit turpis cursus in hac. Dui ut ornare lectus sit amet est. Nisl condimentum id venenatis a condimentum vitae sapien ', }; export const deleteDialogNotLoading = { render: () => { - const { appElement, rootElement } = addRootElement(); - - return ( - - ); + const { rootElement } = addRootElement(); + + return ; }, }; export const deleteDialogIsLoading = { render: () => { - const { appElement, rootElement } = addRootElement(); - - return ( - - ); + const { rootElement } = addRootElement(); + + return ; }, };