From 4e0712b62291f97f5f0c2cbf81a89405c819495e Mon Sep 17 00:00:00 2001 From: greg-in-a-box <103291617+greg-in-a-box@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:38:12 -0500 Subject: [PATCH] chore(content-explorer): Migrate Create New Folder (#3896) --- ...erDialog.js => CreateFolderDialog.js.flow} | 12 +- .../CreateFolderDialog.tsx | 109 ++++++++++++++++++ .../__tests__/CreateFolderDialog.test.tsx | 88 ++++++++++++++ .../common/create-folder-dialog/index.js.flow | 1 + .../{index.js => index.ts} | 1 - src/elements/common/modal.scss | 1 + .../stories/__mocks__/mockRootFolder.ts | 2 +- .../tests/ContentExplorer-visual.stories.js | 39 +++++++ 8 files changed, 242 insertions(+), 11 deletions(-) rename src/elements/common/create-folder-dialog/{CreateFolderDialog.js => CreateFolderDialog.js.flow} (95%) create mode 100644 src/elements/common/create-folder-dialog/CreateFolderDialog.tsx create mode 100644 src/elements/common/create-folder-dialog/__tests__/CreateFolderDialog.test.tsx create mode 100644 src/elements/common/create-folder-dialog/index.js.flow rename src/elements/common/create-folder-dialog/{index.js => index.ts} (84%) diff --git a/src/elements/common/create-folder-dialog/CreateFolderDialog.js b/src/elements/common/create-folder-dialog/CreateFolderDialog.js.flow similarity index 95% rename from src/elements/common/create-folder-dialog/CreateFolderDialog.js rename to src/elements/common/create-folder-dialog/CreateFolderDialog.js.flow index d8859888db..adea19b719 100644 --- a/src/elements/common/create-folder-dialog/CreateFolderDialog.js +++ b/src/elements/common/create-folder-dialog/CreateFolderDialog.js.flow @@ -1,9 +1,3 @@ -/** - * @flow - * @file Content Explorer Create Folder Dialog - * @author Box - */ - import * as React from 'react'; import Modal from 'react-modal'; import { injectIntl, FormattedMessage } from 'react-intl'; @@ -25,9 +19,9 @@ type Props = { intl: IntlShape, isLoading: boolean, isOpen: boolean, - onCancel: Function, - onCreate: Function, - parentElement: HTMLElement, + onCancel: any, + onCreate: any, + parentElement: HTMLElement }; /* eslint-disable jsx-a11y/label-has-for */ diff --git a/src/elements/common/create-folder-dialog/CreateFolderDialog.tsx b/src/elements/common/create-folder-dialog/CreateFolderDialog.tsx new file mode 100644 index 0000000000..244aa6add1 --- /dev/null +++ b/src/elements/common/create-folder-dialog/CreateFolderDialog.tsx @@ -0,0 +1,109 @@ +import React, { useState } from 'react'; +import Modal from 'react-modal'; +import { useIntl } from 'react-intl'; +import { Modal as BlueprintModal, TextInput } from '@box/blueprint-web'; +import { + CLASS_MODAL_CONTENT, + CLASS_MODAL_OVERLAY, + CLASS_MODAL, + ERROR_CODE_ITEM_NAME_TOO_LONG, + ERROR_CODE_ITEM_NAME_IN_USE, +} from '../../../constants'; + +import messages from '../messages'; + +export interface CreateFolderDialogProps { + appElement: HTMLElement; + errorCode: string; + isLoading: boolean; + isOpen: boolean; + onCancel: () => void; + onCreate: (value: string) => void; + parentElement: HTMLElement; +} + +const CreateFolderDialog = ({ + appElement, + errorCode, + isOpen, + isLoading, + onCancel, + onCreate, + parentElement, +}: CreateFolderDialogProps) => { + const { formatMessage } = useIntl(); + const [value, setValue] = useState(''); + let error; + + const handleChange = (e: React.ChangeEvent) => { + setValue(e.target.value); + }; + + const handleCreate = () => { + if (value) { + onCreate(value); + } + }; + + const handleKeyDown = ({ key }) => { + switch (key) { + case 'Enter': + handleCreate(); + break; + default: + break; + } + }; + + switch (errorCode) { + case ERROR_CODE_ITEM_NAME_IN_USE: + error = formatMessage(messages.createDialogErrorInUse); + break; + case ERROR_CODE_ITEM_NAME_TOO_LONG: + error = formatMessage(messages.createDialogErrorTooLong); + break; + default: + error = errorCode ? formatMessage(messages.createDialogErrorInvalid) : null; + break; + } + + return ( + parentElement} + portalClassName={`${CLASS_MODAL} be-modal-create-folder`} + > + + + + + + {formatMessage(messages.cancel)} + + + {formatMessage(messages.create)} + + + + ); +}; + +export default CreateFolderDialog; diff --git a/src/elements/common/create-folder-dialog/__tests__/CreateFolderDialog.test.tsx b/src/elements/common/create-folder-dialog/__tests__/CreateFolderDialog.test.tsx new file mode 100644 index 0000000000..f0da895455 --- /dev/null +++ b/src/elements/common/create-folder-dialog/__tests__/CreateFolderDialog.test.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import userEvent from '@testing-library/user-event'; + +import { render, screen } from '../../../../test-utils/testing-library'; +import CreateFolderDialog, { CreateFolderDialogProps } from '../CreateFolderDialog'; +import { ERROR_CODE_ITEM_NAME_TOO_LONG, ERROR_CODE_ITEM_NAME_IN_USE } from '../../../../constants'; + +jest.mock('react-modal', () => { + return jest.fn(({ children }) =>
{children}
); +}); + +const defaultProps = { + appElement: document.createElement('div'), + errorCode: '', + isLoading: false, + isOpen: true, + onCancel: jest.fn(), + onCreate: jest.fn(), + parentElement: document.createElement('div'), +}; + +describe('elements/common/create-folder-dialog/CreateFolderDialog', () => { + const renderComponent = (props: Partial = {}) => + render(); + + test('renders the dialog with the correct initial state', () => { + renderComponent(); + + expect(screen.getByText('Please enter a name.')).toBeInTheDocument(); + expect(screen.getByRole('textbox')).toBeEmptyDOMElement(); + expect(screen.getByRole('button', { name: 'Create' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + }); + + test('calls onCancel when cancel button is clicked', async () => { + const onCancel = jest.fn(); + + renderComponent({ onCancel }); + await userEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(onCancel).toHaveBeenCalled(); + }); + + test('calls onCreate with the correct values when create button is clicked', async () => { + const onCreate = jest.fn(); + + renderComponent({ onCreate }); + const input = screen.getByRole('textbox'); + await userEvent.clear(input); + await userEvent.type(input, 'newname'); + await userEvent.click(screen.getByRole('button', { name: 'Create' })); + expect(onCreate).toHaveBeenCalledWith('newname'); + }); + + test('displays an error message when errorCode is neither ERROR_CODE_ITEM_NAME_IN_USE nor ERROR_CODE_ITEM_NAME_TOO_LONG', () => { + renderComponent({ errorCode: 'bad' }); + expect(screen.getByText('This is an invalid folder name.')).toBeInTheDocument(); + }); + + test('displays an error message when errorCode is ERROR_CODE_ITEM_NAME_IN_USE', () => { + renderComponent({ errorCode: ERROR_CODE_ITEM_NAME_IN_USE }); + expect(screen.getByText('A folder with the same name already exists.')).toBeInTheDocument(); + }); + + test('displays an error message when errorCode is ERROR_CODE_ITEM_NAME_TOO_LONG', () => { + renderComponent({ errorCode: ERROR_CODE_ITEM_NAME_TOO_LONG }); + expect(screen.getByText('This folder name is too long.')).toBeInTheDocument(); + }); + + test('does not call onCreate if the name has not changed', async () => { + const onCancel = jest.fn(); + const onCreate = jest.fn(); + + renderComponent({ onCancel, onCreate }); + await userEvent.click(screen.getByText('Create')); + expect(onCreate).not.toHaveBeenCalled(); + }); + + test('calls handleOnCreate on Enter key press', async () => { + const onCreate = jest.fn(); + + renderComponent({ onCreate }); + const input = screen.getByRole('textbox'); + await userEvent.clear(input); + await userEvent.type(input, 'newname'); + await userEvent.type(input, '{enter}'); + expect(onCreate).toHaveBeenCalledWith('newname'); + }); +}); diff --git a/src/elements/common/create-folder-dialog/index.js.flow b/src/elements/common/create-folder-dialog/index.js.flow new file mode 100644 index 0000000000..8236706b79 --- /dev/null +++ b/src/elements/common/create-folder-dialog/index.js.flow @@ -0,0 +1 @@ +export {default} from './CreateFolderDialog'; diff --git a/src/elements/common/create-folder-dialog/index.js b/src/elements/common/create-folder-dialog/index.ts similarity index 84% rename from src/elements/common/create-folder-dialog/index.js rename to src/elements/common/create-folder-dialog/index.ts index 2e5b2fef32..4dfc9eef95 100644 --- a/src/elements/common/create-folder-dialog/index.js +++ b/src/elements/common/create-folder-dialog/index.ts @@ -1,2 +1 @@ -// @flow export { default } from './CreateFolderDialog'; diff --git a/src/elements/common/modal.scss b/src/elements/common/modal.scss index 5a376a5b31..1cb86c4375 100644 --- a/src/elements/common/modal.scss +++ b/src/elements/common/modal.scss @@ -44,6 +44,7 @@ background-color: $darker-black; } +.be-modal-create-folder .be-modal-dialog-content, .be-modal-rename .be-modal-dialog-content, .be-modal-share .be-modal-dialog-content, .be-modal-delete .be-modal-dialog-content { diff --git a/src/elements/content-explorer/stories/__mocks__/mockRootFolder.ts b/src/elements/content-explorer/stories/__mocks__/mockRootFolder.ts index 1ff6d9b26d..bf6d2ef26a 100644 --- a/src/elements/content-explorer/stories/__mocks__/mockRootFolder.ts +++ b/src/elements/content-explorer/stories/__mocks__/mockRootFolder.ts @@ -73,7 +73,7 @@ const mockRootFolder = { parent: null, permissions: { can_download: true, - can_upload: false, + can_upload: true, can_rename: false, can_delete: false, can_share: false, 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..c036fa21cb 100644 --- a/src/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js +++ b/src/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js @@ -38,6 +38,45 @@ export const openExistingFolder = { }, }; +export const openCreateFolderDialog = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const addButton = await canvas.findByRole('button', { name: 'Add' }); + await userEvent.click(addButton); + + const dropdown = await screen.findByRole('menu'); + const newFolderButton = within(dropdown).getByText('New Folder'); + expect(newFolderButton).toBeInTheDocument(); + await userEvent.click(newFolderButton); + + expect(await screen.findByText('Please enter a name.')).toBeInTheDocument(); + }, +}; + +export const closeCreateFolderDialog = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const addButton = await canvas.findByRole('button', { name: 'Add' }); + await userEvent.click(addButton); + + const dropdown = await screen.findByRole('menu'); + const newFolderButton = within(dropdown).getByText('New Folder'); + expect(newFolderButton).toBeInTheDocument(); + await userEvent.click(newFolderButton); + + expect(await screen.findByText('Please enter a name.')).toBeInTheDocument(); + + const cancelButton = screen.getByText('Cancel'); + await userEvent.click(cancelButton); + + await waitFor(() => { + expect(screen.queryByText('Please enter a name.')).not.toBeInTheDocument(); + }); + }, +}; + export const openDeleteConfirmationDialog = { play: async ({ canvasElement }) => { const canvas = within(canvasElement);