-
Notifications
You must be signed in to change notification settings - Fork 314
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(content-uploader): Migrate ItemList (#3602)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
- Loading branch information
1 parent
1746e3c
commit b2d0c7f
Showing
12 changed files
with
446 additions
and
36 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
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
170
src/elements/content-uploader/__tests__/CellRenderer.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,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); | ||
}); | ||
}); | ||
}); |
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,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); | ||
}); | ||
}); |
9 changes: 2 additions & 7 deletions
9
...ts/content-uploader/actionCellRenderer.js → ...ntent-uploader/actionCellRenderer.js.flow
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,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} | ||
/> | ||
); |
11 changes: 4 additions & 7 deletions
11
...ents/content-uploader/nameCellRenderer.js → ...content-uploader/nameCellRenderer.js.flow
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 |
---|---|---|
@@ -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} /> | ||
); |
Oops, something went wrong.