Skip to content

Commit

Permalink
Merge branch 'master' into fix-archive-date-preview
Browse files Browse the repository at this point in the history
  • Loading branch information
michalkowalczyk-box authored Nov 21, 2024
2 parents 3617c48 + e967f6d commit da7ff02
Show file tree
Hide file tree
Showing 7 changed files with 345 additions and 56 deletions.
7 changes: 7 additions & 0 deletions src/elements/common/modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,19 @@
background-color: $darker-black;
}

.be-modal-rename .be-modal-dialog-content,
.be-modal-share .be-modal-dialog-content,
.be-modal-delete .be-modal-dialog-content {
padding: 0;
border-radius: tokens.$radius-4;
}

.be-modal-rename .be-modal-dialog-content {
input[type='text'] {
width: 100%;
}
}

.be-modal-dialog-content {
@extend .be-modal-wrapper-content;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
/**
* @flow
* @file Content Explorer Rename 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 { useIntl } from 'react-intl';
import { Modal as BlueprintModal, TextInput } from '@box/blueprint-web';

import {
CLASS_MODAL_CONTENT,
CLASS_MODAL_OVERLAY,
Expand All @@ -20,30 +12,30 @@ import {
} from '../../constants';
import type { BoxItem } from '../../common/types/core';

import messages from '../common/messages';

type Props = {
appElement: HTMLElement,
errorCode: string,
intl: IntlShape,
isLoading: boolean,
isOpen: boolean,
item: BoxItem,
onCancel: Function,
onRename: Function,
parentElement: HTMLElement,
onCancel: any,
onRename: any,
parentElement: HTMLElement
};

/* eslint-disable jsx-a11y/label-has-for */
const RenameDialog = ({
appElement,
errorCode,
isOpen,
onRename,
onCancel,
item,
isLoading,
errorCode,
item,
onCancel,
onRename,
parentElement,
appElement,
intl,
}: Props) => {
const { formatMessage } = useIntl();
let textInput = null;
let error;

Expand All @@ -54,7 +46,7 @@ const RenameDialog = ({
/**
* Appends the extension and calls rename function
*/
const rename = () => {
const handleRename = () => {
if (textInput && textInput.value) {
if (textInput.value === nameWithoutExt) {
onCancel();
Expand All @@ -81,7 +73,7 @@ const RenameDialog = ({
const onKeyDown = ({ key }) => {
switch (key) {
case 'Enter':
rename();
handleRename();
break;
default:
break;
Expand All @@ -104,32 +96,38 @@ const RenameDialog = ({
<Modal
appElement={appElement}
className={CLASS_MODAL_CONTENT}
contentLabel={intl.formatMessage(messages.renameDialogLabel)}
contentLabel={formatMessage(messages.renameDialogLabel)}
isOpen={isOpen}
onRequestClose={onCancel}
overlayClassName={CLASS_MODAL_OVERLAY}
parentSelector={() => parentElement}
portalClassName={`${CLASS_MODAL} be-modal-rename`}
>
<label>
{error ? (
<div className="be-modal-error">
<FormattedMessage {...error} values={{ name: nameWithoutExt }} />
</div>
) : null}
<FormattedMessage tagName="div" {...messages.renameDialogText} values={{ name: nameWithoutExt }} />
<input ref={ref} defaultValue={nameWithoutExt} onKeyDown={onKeyDown} required type="text" />
</label>
<div className="be-modal-btns">
<PrimaryButton isLoading={isLoading} onClick={rename} type="button">
<FormattedMessage {...messages.rename} />
</PrimaryButton>
<Button isDisabled={isLoading} onClick={onCancel} type="button">
<FormattedMessage {...messages.cancel} />
</Button>
</div>
<BlueprintModal.Body>
<TextInput
defaultValue={nameWithoutExt}
error={error && formatMessage(error, { name: nameWithoutExt })}
label={formatMessage(messages.renameDialogText, { name: nameWithoutExt })}
onKeyDown={onKeyDown}
ref={ref}
required
/>
</BlueprintModal.Body>
<BlueprintModal.Footer>
<BlueprintModal.Footer.SecondaryButton disabled={isLoading} onClick={onCancel} size="large">
{formatMessage(messages.cancel)}
</BlueprintModal.Footer.SecondaryButton>
<BlueprintModal.Footer.PrimaryButton
loading={isLoading}
loadingAriaLabel={formatMessage(messages.loading)}
onClick={handleRename}
size="large"
>
{formatMessage(messages.rename)}
</BlueprintModal.Footer.PrimaryButton>
</BlueprintModal.Footer>
</Modal>
);
};

export default injectIntl(RenameDialog);
export default RenameDialog;
133 changes: 133 additions & 0 deletions src/elements/content-explorer/RenameDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import * as React 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 type { BoxItem } from '../../common/types/core';

import messages from '../common/messages';

export interface RenameDialogProps {
appElement: HTMLElement;
errorCode: string;
isLoading: boolean;
isOpen: boolean;
item: BoxItem;
onCancel: () => void;
onRename: (value: string, extension: string) => void;
parentElement: HTMLElement;
}

const RenameDialog = ({
appElement,
errorCode,
isOpen,
isLoading,
item,
onCancel,
onRename,
parentElement,
}: RenameDialogProps) => {
const { formatMessage } = useIntl();
let textInput = null;
let error;

const { name = '', extension } = item;
const ext = extension ? `.${extension}` : '';
const nameWithoutExt = extension ? name.replace(ext, '') : name;

/**
* Appends the extension and calls rename function
*/
const handleRename = () => {
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':
handleRename();
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
appElement={appElement}
className={CLASS_MODAL_CONTENT}
contentLabel={formatMessage(messages.renameDialogLabel)}
isOpen={isOpen}
onRequestClose={onCancel}
overlayClassName={CLASS_MODAL_OVERLAY}
parentSelector={() => parentElement}
portalClassName={`${CLASS_MODAL} be-modal-rename`}
>
<BlueprintModal.Body>
<TextInput
defaultValue={nameWithoutExt}
error={error && formatMessage(error, { name: nameWithoutExt })}
label={formatMessage(messages.renameDialogText, { name: nameWithoutExt })}
onKeyDown={onKeyDown}
ref={ref}
required
/>
</BlueprintModal.Body>
<BlueprintModal.Footer>
<BlueprintModal.Footer.SecondaryButton disabled={isLoading} onClick={onCancel} size="large">
{formatMessage(messages.cancel)}
</BlueprintModal.Footer.SecondaryButton>
<BlueprintModal.Footer.PrimaryButton
loading={isLoading}
loadingAriaLabel={formatMessage(messages.loading)}
onClick={handleRename}
size="large"
>
{formatMessage(messages.rename)}
</BlueprintModal.Footer.PrimaryButton>
</BlueprintModal.Footer>
</Modal>
);
};

export default RenameDialog;
90 changes: 90 additions & 0 deletions src/elements/content-explorer/__tests__/RenameDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import * as React from 'react';
import userEvent from '@testing-library/user-event';

import { render, screen } from '../../../test-utils/testing-library';
import RenameDialog from '../RenameDialog';
import { ERROR_CODE_ITEM_NAME_TOO_LONG, ERROR_CODE_ITEM_NAME_IN_USE } from '../../../constants';

jest.mock('react-modal', () => {
return jest.fn(({ children }) => <div aria-label="Rename">{children}</div>);
});

const defaultProps = {
appElement: document.createElement('div'),
errorCode: '',
isLoading: false,
isOpen: true,
item: { name: 'test.txt', extension: 'txt' },
onCancel: jest.fn(),
onRename: jest.fn(),
parentElement: document.createElement('div'),
};

describe('elements/content-explorer/RenameDialog', () => {
const renderComponent = (props = {}) => render(<RenameDialog {...defaultProps} {...props} />);

test('renders the dialog with the correct initial state', () => {
renderComponent();

expect(screen.getByText('Please enter a new name for test:')).toBeInTheDocument();
expect(screen.getByRole('textbox')).toHaveValue('test');
expect(screen.getByDisplayValue('test')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Rename' })).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 onRename with the correct values when rename button is clicked', async () => {
const onRename = jest.fn();

renderComponent({ onRename });
const input = screen.getByRole('textbox');
await userEvent.clear(input);
await userEvent.type(input, 'newname');
await userEvent.click(screen.getByRole('button', { name: 'Rename' }));
expect(onRename).toHaveBeenCalledWith('newname', '.txt');
});

test('displays an error message is neither ERROR_CODE_ITEM_NAME_IN_USE or ERROR_CODE_ITEM_NAME_TOO_LONG', () => {
renderComponent({ errorCode: 'something else' });
expect(screen.getByText('This name is invalid.')).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('An item 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 name is too long.')).toBeInTheDocument();
});

test('does not call onRename if the name has not changed', async () => {
const onCancel = jest.fn();
const onRename = jest.fn();

renderComponent({ onCancel, onRename });
await userEvent.click(screen.getByText('Rename'));
expect(onRename).not.toHaveBeenCalled();
expect(onCancel).toHaveBeenCalled();
});

test('calls handleRename on Enter key press', async () => {
const onRename = jest.fn();

renderComponent({ onRename });
const input = screen.getByRole('textbox');
await userEvent.clear(input);
await userEvent.type(input, 'newname');
await userEvent.type(input, '{enter}');
expect(onRename).toHaveBeenCalledWith('newname', '.txt');
});
});
Loading

0 comments on commit da7ff02

Please sign in to comment.