forked from box/box-ui-elements
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(content-explorer): migrate renameDialog (box#3666)
* chore(content-explorer): migrate renameDialog * chore(content-explorer): migrate rename dialog * chore(content-explorer): update based on comments * chore(content-explorer): update storybook and messages
- Loading branch information
1 parent
ef79e44
commit f5f1bac
Showing
11 changed files
with
347 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.bce-RenameDialog { | ||
// override the textinput width inside _forms.scss | ||
input[type='text'] { | ||
width: 100%; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import * as React from 'react'; | ||
import { useIntl } from 'react-intl'; | ||
import { Modal, TextInput } from '@box/blueprint-web'; | ||
import type { BoxItem } from '../../common/types/core'; | ||
|
||
import { | ||
ERROR_CODE_ITEM_NAME_IN_USE, | ||
ERROR_CODE_ITEM_NAME_TOO_LONG, | ||
TYPE_FILE, | ||
TYPE_FOLDER, | ||
TYPE_WEBLINK, | ||
} from '../../constants'; | ||
|
||
import messages from '../common/messages'; | ||
|
||
import './RenameDialog.scss'; | ||
|
||
export interface RenameDialogProps { | ||
errorCode: string; | ||
isLoading: boolean; | ||
isOpen: boolean; | ||
item: BoxItem; | ||
onCancel: () => void; | ||
onRename: (nameWithoutExt: string, extension: string) => void; | ||
parentElement: HTMLElement; | ||
} | ||
|
||
const RenameDialog = ({ errorCode, isLoading, isOpen, item, onCancel, onRename, parentElement }: RenameDialogProps) => { | ||
const { formatMessage } = useIntl(); | ||
|
||
let textInput = null; | ||
let error; | ||
|
||
const { name = '', extension, type } = item; | ||
const ext = extension ? `.${extension}` : ''; | ||
const nameWithoutExt = extension ? name.replace(ext, '') : name; | ||
|
||
const headerMessages = { | ||
[TYPE_FILE]: messages.renameDialogFileHeader, | ||
[TYPE_FOLDER]: messages.renameDialogFolderHeader, | ||
[TYPE_WEBLINK]: messages.renameDialogWebLinkHeader, | ||
}; | ||
|
||
/** | ||
* Appends the extension and calls rename function | ||
*/ | ||
const rename = () => { | ||
if (textInput && textInput.value) { | ||
if (textInput.value === nameWithoutExt) { | ||
onCancel(); | ||
} else { | ||
onRename(textInput.value, ext); | ||
} | ||
} | ||
}; | ||
|
||
/** | ||
* Grabs reference to the input element | ||
*/ | ||
const ref = input => { | ||
textInput = input; | ||
if (textInput instanceof HTMLInputElement) { | ||
textInput.focus(); | ||
textInput.select(); | ||
} | ||
}; | ||
|
||
/** | ||
* Handles enter key down | ||
*/ | ||
const onKeyDown = ({ key }) => { | ||
switch (key) { | ||
case 'Enter': | ||
rename(); | ||
break; | ||
default: | ||
break; | ||
} | ||
}; | ||
|
||
switch (errorCode) { | ||
case ERROR_CODE_ITEM_NAME_IN_USE: | ||
error = messages.renameDialogErrorInUse; | ||
break; | ||
case ERROR_CODE_ITEM_NAME_TOO_LONG: | ||
error = messages.renameDialogErrorTooLong; | ||
break; | ||
default: | ||
error = errorCode ? messages.renameDialogErrorInvalid : null; | ||
break; | ||
} | ||
|
||
return ( | ||
<Modal onOpenChange={onCancel} open={isOpen}> | ||
<Modal.Content | ||
aria-label={formatMessage(messages.renameDialogLabel)} | ||
className="bce-RenameDialog" | ||
container={parentElement} | ||
size="small" | ||
> | ||
<Modal.Header>{formatMessage(headerMessages[type])}</Modal.Header> | ||
<Modal.Body> | ||
<TextInput | ||
defaultValue={nameWithoutExt} | ||
error={error && formatMessage(error)} | ||
label={formatMessage(messages.name)} | ||
onKeyDown={onKeyDown} | ||
ref={ref} | ||
required | ||
/> | ||
</Modal.Body> | ||
<Modal.Footer> | ||
<Modal.Footer.SecondaryButton disabled={isLoading} onClick={onCancel}> | ||
{formatMessage(messages.cancel)} | ||
</Modal.Footer.SecondaryButton> | ||
<Modal.Footer.PrimaryButton | ||
loading={isLoading} | ||
loadingAriaLabel={formatMessage(messages.loading)} | ||
onClick={rename} | ||
> | ||
{formatMessage(messages.rename)} | ||
</Modal.Footer.PrimaryButton> | ||
</Modal.Footer> | ||
<Modal.Close aria-label={formatMessage(messages.close)} /> | ||
</Modal.Content> | ||
</Modal> | ||
); | ||
}; | ||
|
||
export default RenameDialog; |
115 changes: 115 additions & 0 deletions
115
src/elements/content-explorer/__tests__/RenameDialog.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import * as React from 'react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { render, screen } from '../../../test-utils/testing-library'; | ||
|
||
import RenameDialog, { RenameDialogProps } from '../RenameDialog'; | ||
|
||
describe('elements/content-explorer/RenameDialog', () => { | ||
const mockItem = { | ||
id: '123456', | ||
name: 'mockFile', | ||
extension: 'txt', | ||
type: 'file', | ||
}; | ||
|
||
const defaultProps = { | ||
errorCode: '', | ||
isLoading: false, | ||
isOpen: false, | ||
item: mockItem, | ||
onCancel: jest.fn(), | ||
onRename: jest.fn(), | ||
parentElement: document.body, | ||
}; | ||
|
||
const renderComponent = (props: Partial<RenameDialogProps>) => | ||
render(<RenameDialog {...defaultProps} {...props} />); | ||
|
||
test('render rename dialog correctly when it is open and not loading', () => { | ||
renderComponent({ isOpen: true, item: mockItem }); | ||
|
||
expect(screen.getByText('Rename File')).toBeInTheDocument(); | ||
expect(screen.getByRole('button', { name: 'Rename' })).toBeInTheDocument(); | ||
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); | ||
expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument(); | ||
|
||
const textInput = screen.getByLabelText('Name'); | ||
expect(textInput).toBeInTheDocument(); | ||
expect(textInput).toHaveValue('mockFile'); | ||
}); | ||
|
||
test('render dialog footer correctly when it is open and loading', () => { | ||
renderComponent({ isOpen: true, isLoading: true }); | ||
|
||
const cancelButton = screen.getByRole('button', { name: 'Cancel' }); | ||
expect(cancelButton).toBeInTheDocument(); | ||
expect(cancelButton).toBeDisabled(); | ||
|
||
const loadingIndicator = screen.getByRole('status', { name: 'Loading' }); | ||
expect(loadingIndicator).toBeInTheDocument(); | ||
|
||
const renameButton = screen.queryByRole('button', { name: 'Rename' }); | ||
expect(renameButton).not.toBeInTheDocument(); | ||
}); | ||
|
||
test('call onRename with input value and extension when rename button is clicked', async () => { | ||
const mockRenameFunction = jest.fn(); | ||
renderComponent({ isOpen: true, item: mockItem, onRename: mockRenameFunction }); | ||
|
||
const textInput = screen.getByLabelText('Name'); | ||
await userEvent.clear(textInput); | ||
await userEvent.type(textInput, 'newFileName'); | ||
await userEvent.click(screen.getByRole('button', { name: 'Rename' })); | ||
|
||
expect(mockRenameFunction).toHaveBeenCalledTimes(1); | ||
expect(mockRenameFunction).toHaveBeenCalledWith('newFileName', '.txt'); | ||
}); | ||
|
||
test('call onRename with input value and extension when enter key is pressed', async () => { | ||
const mockRenameFunction = jest.fn(); | ||
renderComponent({ isOpen: true, item: mockItem, onRename: mockRenameFunction }); | ||
|
||
const textInput = screen.getByLabelText('Name'); | ||
await userEvent.clear(textInput); | ||
await userEvent.type(textInput, 'newFileName'); | ||
await userEvent.keyboard('{Enter}'); | ||
|
||
expect(mockRenameFunction).toHaveBeenCalledTimes(1); | ||
expect(mockRenameFunction).toHaveBeenCalledWith('newFileName', '.txt'); | ||
}); | ||
|
||
test('call onCancel when cancel button is clicked', async () => { | ||
const mockCancelFunction = jest.fn(); | ||
renderComponent({ isOpen: true, onCancel: mockCancelFunction }); | ||
|
||
await userEvent.click(screen.getByRole('button', { name: 'Cancel' })); | ||
expect(mockCancelFunction).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
test('call onCancel when close icon is clicked', async () => { | ||
const mockCancelFunction = jest.fn(); | ||
renderComponent({ isOpen: true, onCancel: mockCancelFunction }); | ||
|
||
await userEvent.click(screen.getByRole('button', { name: 'Close' })); | ||
expect(mockCancelFunction).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
test('call onCancel when text value is not changed and rename button is clicked', async () => { | ||
const mockCancelFunction = jest.fn(); | ||
renderComponent({ isOpen: true, onCancel: mockCancelFunction }); | ||
|
||
await userEvent.click(screen.getByRole('button', { name: 'Rename' })); | ||
expect(mockCancelFunction).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
test.each` | ||
errorCode | expectedError | ||
${'item_name_in_use'} | ${'An item with the same name already exists.'} | ||
${'item_name_too_long'} | ${'This name is too long.'} | ||
${'default'} | ${'This name is invalid.'} | ||
`('render correct error message based on errorCode', ({ errorCode, expectedError }) => { | ||
renderComponent({ isOpen: true, errorCode }); | ||
|
||
expect(screen.getByText(expectedError)).toBeInTheDocument(); | ||
}); | ||
}); |
51 changes: 51 additions & 0 deletions
51
src/elements/content-explorer/stories/RenameDialog.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import * as React from 'react'; | ||
import { useArgs } from '@storybook/preview-api'; | ||
|
||
import { Button } from '@box/blueprint-web'; | ||
import { addRootElement } from '../../../utils/storybook'; | ||
|
||
import RenameDialog from '../RenameDialog'; | ||
|
||
export const renameDialog = { | ||
// 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 ( | ||
<div> | ||
<RenameDialog | ||
item={{ | ||
id: '123456', | ||
name: 'mockItem', | ||
type: 'file', | ||
}} | ||
onCancel={handleCloseModal} | ||
parentElement={rootElement} | ||
{...args} | ||
/> | ||
|
||
<Button onClick={handleOpenModal} variant="primary"> | ||
Launch RenameDialog | ||
</Button> | ||
</div> | ||
); | ||
}, | ||
}; | ||
|
||
export default { | ||
title: 'Elements/ContentExplorer', | ||
component: RenameDialog, | ||
args: { | ||
isLoading: false, | ||
isOpen: false, | ||
}, | ||
}; |
Oops, something went wrong.