Skip to content

Commit

Permalink
chore(content-uploader): Migrate ItemList (#3602)
Browse files Browse the repository at this point in the history
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
greg-in-a-box and mergify[bot] authored Aug 14, 2024
1 parent 1746e3c commit b2d0c7f
Show file tree
Hide file tree
Showing 12 changed files with 446 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* @flow
* @file Item list component
*/

import * as React from 'react';
import noop from 'lodash/noop';
import { Table, Column } from '@box/react-virtualized/dist/es/Table';
Expand All @@ -18,9 +13,9 @@ import './ItemList.scss';
type Props = {
isResumableUploadsEnabled?: boolean,
items: UploadItem[],
onClick: Function,
onClick: any,
onRemoveClick?: (item: UploadItem) => void,
onUpgradeCTAClick?: Function,
onUpgradeCTAClick?: any
};

const ItemList = ({
Expand Down
86 changes: 86 additions & 0 deletions src/elements/content-uploader/ItemList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as React from 'react';
import noop from 'lodash/noop';
import { Table, Column } from '@box/react-virtualized/dist/es/Table';
import AutoSizer from '@box/react-virtualized/dist/es/AutoSizer';

import nameCellRenderer from './nameCellRenderer';
import progressCellRenderer from './progressCellRenderer';
import actionCellRenderer from './actionCellRenderer';
import removeCellRenderer from './removeCellRenderer';

import type { UploadItem } from '../../common/types/upload';

import '@box/react-virtualized/styles.css';
import './ItemList.scss';

export interface ItemListProps {
isResumableUploadsEnabled?: boolean;
items: UploadItem[];
onClick: React.MouseEventHandler<HTMLButtonElement>;
onRemoveClick?: (item: UploadItem) => void;
onUpgradeCTAClick?: () => void;
}

export interface cellRendererProps {
rowData: UploadItem;
}

const ItemList = ({
isResumableUploadsEnabled = false,
items,
onClick,
onRemoveClick = noop,
onUpgradeCTAClick,
}: ItemListProps) => (
<AutoSizer>
{({ width, height }) => {
const nameCell = nameCellRenderer(isResumableUploadsEnabled);
const progressCell = progressCellRenderer(!!onUpgradeCTAClick);
const actionCell = actionCellRenderer(isResumableUploadsEnabled, onClick, onUpgradeCTAClick);
const removeCell = removeCellRenderer(onRemoveClick);
const baseIconWidth = 32;

return (
<Table
className="bcu-item-list"
disableHeader
headerHeight={0}
height={height}
rowClassName="bcu-item-row"
rowCount={items.length}
rowGetter={({ index }) => items[index]}
rowHeight={50}
width={width}
>
<Column cellRenderer={nameCell} dataKey="name" flexGrow={1} flexShrink={1} width={300} />
<Column
cellRenderer={progressCell}
dataKey="progress"
flexGrow={1}
flexShrink={1}
style={{ textAlign: 'right' }}
width={300}
/>
<Column
className={isResumableUploadsEnabled ? '' : 'bcu-item-list-action-column'}
cellRenderer={actionCell}
dataKey="status"
flexShrink={0}
width={onUpgradeCTAClick ? 100 : baseIconWidth}
/>
{isResumableUploadsEnabled && (
<Column
className="bcu-item-list-action-column"
cellRenderer={removeCell}
dataKey="remove"
flexShrink={0}
width={baseIconWidth}
/>
)}
</Table>
);
}}
</AutoSizer>
);

export default ItemList;
170 changes: 170 additions & 0 deletions src/elements/content-uploader/__tests__/CellRenderer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import * as React from 'react';
import { fireEvent, render, screen } from '../../../test-utils/testing-library';
import actionCellRenderer from '../actionCellRenderer';
import progressCellRenderer from '../progressCellRenderer';
import removeCellRenderer from '../removeCellRenderer';

import Browser from '../../../utils/Browser';
import {
STATUS_COMPLETE,
STATUS_ERROR,
STATUS_IN_PROGRESS,
STATUS_STAGED,
ERROR_CODE_ITEM_NAME_INVALID,
ERROR_CODE_ITEM_NAME_IN_USE,
ERROR_CODE_UPLOAD_BAD_DIGEST,
ERROR_CODE_UPLOAD_CHILD_FOLDER_FAILED,
ERROR_CODE_UPLOAD_FAILED_PACKAGE,
ERROR_CODE_UPLOAD_FILE_SIZE_LIMIT_EXCEEDED,
ERROR_CODE_UPLOAD_PENDING_APP_FOLDER_SIZE_LIMIT,
ERROR_CODE_UPLOAD_STORAGE_LIMIT_EXCEEDED,
} from '../../../constants';

import type { UploadItem } from '../../../common/types/upload';

describe('elements/content-uploader/CellRenderer', () => {
describe('actionCellRenderer', () => {
const renderComponent = (rowData: UploadItem, onClick: jest.Mock) => {
const Component = actionCellRenderer(false, onClick);
return render(<Component rowData={rowData} />);
};

test('calls onClick with rowData when ItemAction is clicked', () => {
const rowData = { id: '3', status: STATUS_ERROR, isFolder: false };
const onClick = jest.fn();
renderComponent(rowData, onClick);
fireEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledWith(rowData);
});
});
describe('progressCellRenderer', () => {
const renderComponent = (rowData: UploadItem, shouldShowUpgradeCTAMessage?: boolean) => {
const Component = progressCellRenderer(shouldShowUpgradeCTAMessage);
return render(<Component rowData={rowData} />);
};

test('renders ItemProgress for in-progress status', () => {
const rowData = { status: STATUS_IN_PROGRESS };
renderComponent(rowData);
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});

test('renders ItemProgress for staged status', () => {
const rowData = { status: STATUS_STAGED };
renderComponent(rowData);
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});

test('renders error message for failing to upload a child folder', () => {
const rowData = { status: STATUS_ERROR, error: { code: ERROR_CODE_UPLOAD_CHILD_FOLDER_FAILED } };
renderComponent(rowData);
expect(screen.getByText('One or more child folders failed to upload.')).toBeInTheDocument();
});

test('renders error message for file size limit exceeded', () => {
const rowData = { status: STATUS_ERROR, error: { code: ERROR_CODE_UPLOAD_FILE_SIZE_LIMIT_EXCEEDED } };
renderComponent(rowData);
expect(screen.getByText('File size exceeds the folder owner’s file size limit')).toBeInTheDocument();
});

test('renders upgrade CTA message for file size limit exceeded when shouldShowUpgradeCTAMessage is true', () => {
const rowData = { status: STATUS_ERROR, error: { code: ERROR_CODE_UPLOAD_FILE_SIZE_LIMIT_EXCEEDED } };
renderComponent(rowData, true);
expect(
screen.getByText('This file exceeds your plan’s upload limit. Upgrade now to store larger files.'),
).toBeInTheDocument();
});

test('renders error message for item name in use', () => {
const rowData = { status: STATUS_ERROR, error: { code: ERROR_CODE_ITEM_NAME_IN_USE } };
renderComponent(rowData);
expect(screen.getByText('A file with this name already exists.')).toBeInTheDocument();
});

test('renders error message for invalid item name', () => {
const rowData = {
status: STATUS_ERROR,
error: { code: ERROR_CODE_ITEM_NAME_INVALID },
name: 'invalidName',
};
renderComponent(rowData);
expect(
screen.getByText('Provided folder name, invalidName, could not be used to create a folder.'),
).toBeInTheDocument();
});

test('renders error message for storage limit exceeded', () => {
const rowData = { status: STATUS_ERROR, error: { code: ERROR_CODE_UPLOAD_STORAGE_LIMIT_EXCEEDED } };
renderComponent(rowData);
expect(screen.getByText('Account storage limit reached')).toBeInTheDocument();
});

test('renders error message for pending app folder size limit', () => {
const rowData = { status: STATUS_ERROR, error: { code: ERROR_CODE_UPLOAD_PENDING_APP_FOLDER_SIZE_LIMIT } };
renderComponent(rowData);
expect(screen.getByText('Pending app folder size limit exceeded')).toBeInTheDocument();
});

test('renders error message for failed package upload', () => {
const rowData = { status: STATUS_ERROR, error: { code: ERROR_CODE_UPLOAD_FAILED_PACKAGE } };
renderComponent(rowData);
expect(
screen.getByText('Failed to upload package file. Please retry by saving as a single file.'),
).toBeInTheDocument();
});

test('renders error message for bad digest in Safari with zip file', () => {
Browser.isSafari = jest.fn().mockReturnValue(true);
const rowData = {
status: STATUS_ERROR,
error: { code: ERROR_CODE_UPLOAD_BAD_DIGEST },
file: { name: 'file.zip' },
};
renderComponent(rowData);
expect(
screen.getByText('Failed to upload package file. Please retry by saving as a single file.'),
).toBeInTheDocument();
});

test('renders default error message for unknown error code', () => {
const rowData = { status: STATUS_ERROR, error: { code: 'UNKNOWN_ERROR_CODE' } };
renderComponent(rowData);
expect(screen.getByText('Something went wrong with the upload. Please try again.')).toBeInTheDocument();
});

test('returns null for folder with non-error status', () => {
const rowData = { status: STATUS_IN_PROGRESS, isFolder: true };
const Component = progressCellRenderer();
const { container } = render(<Component rowData={rowData} />);
expect(container.firstChild).toBeNull();
});
});
describe('removeCellRenderer', () => {
const renderComponent = (rowData: UploadItem, onClick: jest.Mock) => {
const Component = removeCellRenderer(onClick);
return render(<Component rowData={rowData} />);
};

test('renders ItemRemove for non-folder item', () => {
const rowData = { isFolder: false };
const onClick = jest.fn();
renderComponent(rowData, onClick);
expect(screen.getByRole('button', { name: 'Remove' })).toBeInTheDocument();
});

test('does not render ItemRemove for folder item', () => {
const rowData = { id: '2', status: STATUS_COMPLETE, isFolder: true };
const onClick = jest.fn();
const { container } = renderComponent(rowData, onClick);
expect(container.firstChild).toBeNull();
});

test('calls onClick with rowData when ItemRemove is clicked', () => {
const rowData = { id: '3', status: STATUS_ERROR, isFolder: false };
const onClick = jest.fn();
renderComponent(rowData, onClick);
fireEvent.click(screen.getByRole('button', { name: 'Remove' }));
expect(onClick).toHaveBeenCalledWith(rowData);
});
});
});
56 changes: 56 additions & 0 deletions src/elements/content-uploader/__tests__/ItemList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as React from 'react';
import { render, screen } from '../../../test-utils/testing-library';
import ItemList, { ItemListProps } from '../ItemList';
import { STATUS_ERROR, STATUS_COMPLETE, ERROR_CODE_UPLOAD_FILE_SIZE_LIMIT_EXCEEDED } from '../../../constants';

jest.mock(
'@box/react-virtualized/dist/es/AutoSizer',
() =>
({ children }) =>
children({ height: 600, width: 600 }),
);

describe('elements/content-uploader/ItemList', () => {
const renderComponent = (props?: Partial<ItemListProps>) =>
render(<ItemList items={[]} onClick={jest.fn()} {...props} />);

test('should render with default props', () => {
renderComponent();
expect(screen.getByRole('grid')).toBeInTheDocument();
});

test('should render component with correct number of items', () => {
const items = [
{ id: '1', name: 'item1', status: STATUS_COMPLETE },
{ id: '2', name: 'item2', status: STATUS_COMPLETE },
{ id: '3', name: 'item3', status: STATUS_COMPLETE },
];
renderComponent({ items });

expect(screen.getAllByRole('row')).toHaveLength(3);
const actionColumn = screen
.getAllByRole('gridcell')
.find(cell => cell.className.includes('bcu-item-list-action-column'));
expect(actionColumn.style.flex).toEqual('0 0 32px');
});

test('should render action column with correct width for upgrade cta', () => {
const items = [
{ id: '1', name: 'item1', status: STATUS_ERROR, code: ERROR_CODE_UPLOAD_FILE_SIZE_LIMIT_EXCEEDED },
];

renderComponent({ items, onUpgradeCTAClick: jest.fn() });
expect(screen.getAllByRole('row')).toHaveLength(1);
const actionColumn = screen
.getAllByRole('gridcell')
.find(cell => cell.className.includes('bcu-item-list-action-column'));
expect(actionColumn.style.flex).toEqual('0 0 100px');
});

test('should render component with resumable uploads enabled', () => {
const items = [{ id: '1', name: 'item1', status: STATUS_COMPLETE }];
renderComponent({ items, isResumableUploadsEnabled: true });
expect(screen.getByRole('grid')).toBeInTheDocument();
expect(screen.getAllByRole('gridcell')).toHaveLength(4);
});
});
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
/**
* @flow
* @file Function to render the action table cell
*/

import * as React from 'react';
import ItemAction from './ItemAction';
import type { UploadItem } from '../../common/types/upload';

type Props = {
rowData: UploadItem,
rowData: UploadItem
};

export default (isResumableUploadsEnabled: boolean, onClick: Function, onUpgradeCTAClick?: Function) => ({
export default (isResumableUploadsEnabled: boolean, onClick: any, onUpgradeCTAClick?: any) => ({
rowData,
}: Props) => (
<ItemAction
Expand Down
17 changes: 17 additions & 0 deletions src/elements/content-uploader/actionCellRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as React from 'react';
import ItemAction from './ItemAction';
import { cellRendererProps } from './ItemList';

export default (
isResumableUploadsEnabled: boolean,
onClick: React.MouseEventHandler<HTMLButtonElement>,
onUpgradeCTAClick?: () => void,
) =>
({ rowData }: cellRendererProps) => (
<ItemAction
{...rowData}
isResumableUploadsEnabled={isResumableUploadsEnabled}
onClick={() => onClick(rowData)}
onUpgradeCTAClick={onUpgradeCTAClick}
/>
);
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
/**
* @flow
* @file Function to render the name table cell
*/

import * as React from 'react';
import IconName from './IconName';
import type { UploadItem } from '../../common/types/upload';

type Props = {
rowData: UploadItem,
rowData: UploadItem
};

export default (isResumableUploadsEnabled: boolean) => ({ rowData }: Props) => (
export default (isResumableUploadsEnabled: boolean) => ({
rowData,
}: Props) => (
<IconName isResumableUploadsEnabled={isResumableUploadsEnabled} {...rowData} />
);
Loading

0 comments on commit b2d0c7f

Please sign in to comment.