Skip to content

Commit 4e0712b

Browse files
chore(content-explorer): Migrate Create New Folder (#3896)
1 parent ec98688 commit 4e0712b

File tree

8 files changed

+242
-11
lines changed

8 files changed

+242
-11
lines changed

src/elements/common/create-folder-dialog/CreateFolderDialog.js renamed to src/elements/common/create-folder-dialog/CreateFolderDialog.js.flow

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
/**
2-
* @flow
3-
* @file Content Explorer Create Folder Dialog
4-
* @author Box
5-
*/
6-
71
import * as React from 'react';
82
import Modal from 'react-modal';
93
import { injectIntl, FormattedMessage } from 'react-intl';
@@ -25,9 +19,9 @@ type Props = {
2519
intl: IntlShape,
2620
isLoading: boolean,
2721
isOpen: boolean,
28-
onCancel: Function,
29-
onCreate: Function,
30-
parentElement: HTMLElement,
22+
onCancel: any,
23+
onCreate: any,
24+
parentElement: HTMLElement
3125
};
3226

3327
/* eslint-disable jsx-a11y/label-has-for */
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import React, { useState } from 'react';
2+
import Modal from 'react-modal';
3+
import { useIntl } from 'react-intl';
4+
import { Modal as BlueprintModal, TextInput } from '@box/blueprint-web';
5+
import {
6+
CLASS_MODAL_CONTENT,
7+
CLASS_MODAL_OVERLAY,
8+
CLASS_MODAL,
9+
ERROR_CODE_ITEM_NAME_TOO_LONG,
10+
ERROR_CODE_ITEM_NAME_IN_USE,
11+
} from '../../../constants';
12+
13+
import messages from '../messages';
14+
15+
export interface CreateFolderDialogProps {
16+
appElement: HTMLElement;
17+
errorCode: string;
18+
isLoading: boolean;
19+
isOpen: boolean;
20+
onCancel: () => void;
21+
onCreate: (value: string) => void;
22+
parentElement: HTMLElement;
23+
}
24+
25+
const CreateFolderDialog = ({
26+
appElement,
27+
errorCode,
28+
isOpen,
29+
isLoading,
30+
onCancel,
31+
onCreate,
32+
parentElement,
33+
}: CreateFolderDialogProps) => {
34+
const { formatMessage } = useIntl();
35+
const [value, setValue] = useState('');
36+
let error;
37+
38+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
39+
setValue(e.target.value);
40+
};
41+
42+
const handleCreate = () => {
43+
if (value) {
44+
onCreate(value);
45+
}
46+
};
47+
48+
const handleKeyDown = ({ key }) => {
49+
switch (key) {
50+
case 'Enter':
51+
handleCreate();
52+
break;
53+
default:
54+
break;
55+
}
56+
};
57+
58+
switch (errorCode) {
59+
case ERROR_CODE_ITEM_NAME_IN_USE:
60+
error = formatMessage(messages.createDialogErrorInUse);
61+
break;
62+
case ERROR_CODE_ITEM_NAME_TOO_LONG:
63+
error = formatMessage(messages.createDialogErrorTooLong);
64+
break;
65+
default:
66+
error = errorCode ? formatMessage(messages.createDialogErrorInvalid) : null;
67+
break;
68+
}
69+
70+
return (
71+
<Modal
72+
appElement={appElement}
73+
className={CLASS_MODAL_CONTENT}
74+
contentLabel={formatMessage(messages.createDialogLabel)}
75+
isOpen={isOpen}
76+
onRequestClose={onCancel}
77+
overlayClassName={CLASS_MODAL_OVERLAY}
78+
parentSelector={() => parentElement}
79+
portalClassName={`${CLASS_MODAL} be-modal-create-folder`}
80+
>
81+
<BlueprintModal.Body>
82+
<TextInput
83+
autoFocus
84+
error={error}
85+
label={formatMessage(messages.createDialogText)}
86+
onChange={handleChange}
87+
onKeyDown={handleKeyDown}
88+
required
89+
value={value}
90+
/>
91+
</BlueprintModal.Body>
92+
<BlueprintModal.Footer>
93+
<BlueprintModal.Footer.SecondaryButton disabled={isLoading} onClick={onCancel} size="large">
94+
{formatMessage(messages.cancel)}
95+
</BlueprintModal.Footer.SecondaryButton>
96+
<BlueprintModal.Footer.PrimaryButton
97+
loading={isLoading}
98+
loadingAriaLabel={formatMessage(messages.loading)}
99+
onClick={handleCreate}
100+
size="large"
101+
>
102+
{formatMessage(messages.create)}
103+
</BlueprintModal.Footer.PrimaryButton>
104+
</BlueprintModal.Footer>
105+
</Modal>
106+
);
107+
};
108+
109+
export default CreateFolderDialog;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import * as React from 'react';
2+
import userEvent from '@testing-library/user-event';
3+
4+
import { render, screen } from '../../../../test-utils/testing-library';
5+
import CreateFolderDialog, { CreateFolderDialogProps } from '../CreateFolderDialog';
6+
import { ERROR_CODE_ITEM_NAME_TOO_LONG, ERROR_CODE_ITEM_NAME_IN_USE } from '../../../../constants';
7+
8+
jest.mock('react-modal', () => {
9+
return jest.fn(({ children }) => <div>{children}</div>);
10+
});
11+
12+
const defaultProps = {
13+
appElement: document.createElement('div'),
14+
errorCode: '',
15+
isLoading: false,
16+
isOpen: true,
17+
onCancel: jest.fn(),
18+
onCreate: jest.fn(),
19+
parentElement: document.createElement('div'),
20+
};
21+
22+
describe('elements/common/create-folder-dialog/CreateFolderDialog', () => {
23+
const renderComponent = (props: Partial<CreateFolderDialogProps> = {}) =>
24+
render(<CreateFolderDialog {...defaultProps} {...props} />);
25+
26+
test('renders the dialog with the correct initial state', () => {
27+
renderComponent();
28+
29+
expect(screen.getByText('Please enter a name.')).toBeInTheDocument();
30+
expect(screen.getByRole('textbox')).toBeEmptyDOMElement();
31+
expect(screen.getByRole('button', { name: 'Create' })).toBeInTheDocument();
32+
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
33+
});
34+
35+
test('calls onCancel when cancel button is clicked', async () => {
36+
const onCancel = jest.fn();
37+
38+
renderComponent({ onCancel });
39+
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
40+
expect(onCancel).toHaveBeenCalled();
41+
});
42+
43+
test('calls onCreate with the correct values when create button is clicked', async () => {
44+
const onCreate = jest.fn();
45+
46+
renderComponent({ onCreate });
47+
const input = screen.getByRole('textbox');
48+
await userEvent.clear(input);
49+
await userEvent.type(input, 'newname');
50+
await userEvent.click(screen.getByRole('button', { name: 'Create' }));
51+
expect(onCreate).toHaveBeenCalledWith('newname');
52+
});
53+
54+
test('displays an error message when errorCode is neither ERROR_CODE_ITEM_NAME_IN_USE nor ERROR_CODE_ITEM_NAME_TOO_LONG', () => {
55+
renderComponent({ errorCode: 'bad' });
56+
expect(screen.getByText('This is an invalid folder name.')).toBeInTheDocument();
57+
});
58+
59+
test('displays an error message when errorCode is ERROR_CODE_ITEM_NAME_IN_USE', () => {
60+
renderComponent({ errorCode: ERROR_CODE_ITEM_NAME_IN_USE });
61+
expect(screen.getByText('A folder with the same name already exists.')).toBeInTheDocument();
62+
});
63+
64+
test('displays an error message when errorCode is ERROR_CODE_ITEM_NAME_TOO_LONG', () => {
65+
renderComponent({ errorCode: ERROR_CODE_ITEM_NAME_TOO_LONG });
66+
expect(screen.getByText('This folder name is too long.')).toBeInTheDocument();
67+
});
68+
69+
test('does not call onCreate if the name has not changed', async () => {
70+
const onCancel = jest.fn();
71+
const onCreate = jest.fn();
72+
73+
renderComponent({ onCancel, onCreate });
74+
await userEvent.click(screen.getByText('Create'));
75+
expect(onCreate).not.toHaveBeenCalled();
76+
});
77+
78+
test('calls handleOnCreate on Enter key press', async () => {
79+
const onCreate = jest.fn();
80+
81+
renderComponent({ onCreate });
82+
const input = screen.getByRole('textbox');
83+
await userEvent.clear(input);
84+
await userEvent.type(input, 'newname');
85+
await userEvent.type(input, '{enter}');
86+
expect(onCreate).toHaveBeenCalledWith('newname');
87+
});
88+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {default} from './CreateFolderDialog';
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
// @flow
21
export { default } from './CreateFolderDialog';

src/elements/common/modal.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
background-color: $darker-black;
4545
}
4646

47+
.be-modal-create-folder .be-modal-dialog-content,
4748
.be-modal-rename .be-modal-dialog-content,
4849
.be-modal-share .be-modal-dialog-content,
4950
.be-modal-delete .be-modal-dialog-content {

src/elements/content-explorer/stories/__mocks__/mockRootFolder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const mockRootFolder = {
7373
parent: null,
7474
permissions: {
7575
can_download: true,
76-
can_upload: false,
76+
can_upload: true,
7777
can_rename: false,
7878
can_delete: false,
7979
can_share: false,

src/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,45 @@ export const openExistingFolder = {
3838
},
3939
};
4040

41+
export const openCreateFolderDialog = {
42+
play: async ({ canvasElement }) => {
43+
const canvas = within(canvasElement);
44+
45+
const addButton = await canvas.findByRole('button', { name: 'Add' });
46+
await userEvent.click(addButton);
47+
48+
const dropdown = await screen.findByRole('menu');
49+
const newFolderButton = within(dropdown).getByText('New Folder');
50+
expect(newFolderButton).toBeInTheDocument();
51+
await userEvent.click(newFolderButton);
52+
53+
expect(await screen.findByText('Please enter a name.')).toBeInTheDocument();
54+
},
55+
};
56+
57+
export const closeCreateFolderDialog = {
58+
play: async ({ canvasElement }) => {
59+
const canvas = within(canvasElement);
60+
61+
const addButton = await canvas.findByRole('button', { name: 'Add' });
62+
await userEvent.click(addButton);
63+
64+
const dropdown = await screen.findByRole('menu');
65+
const newFolderButton = within(dropdown).getByText('New Folder');
66+
expect(newFolderButton).toBeInTheDocument();
67+
await userEvent.click(newFolderButton);
68+
69+
expect(await screen.findByText('Please enter a name.')).toBeInTheDocument();
70+
71+
const cancelButton = screen.getByText('Cancel');
72+
await userEvent.click(cancelButton);
73+
74+
await waitFor(() => {
75+
expect(screen.queryByText('Please enter a name.')).not.toBeInTheDocument();
76+
});
77+
},
78+
};
79+
4180
export const openDeleteConfirmationDialog = {
4281
play: async ({ canvasElement }) => {
4382
const canvas = within(canvasElement);

0 commit comments

Comments
 (0)