diff --git a/i18n/en-US.properties b/i18n/en-US.properties index a7fad33ac9..368753a74f 100644 --- a/i18n/en-US.properties +++ b/i18n/en-US.properties @@ -92,6 +92,8 @@ be.close = Close be.collaboratedFolder = Collaborated Folder # Message to the user to collapse the Transcript entries be.collapse = Collapse +# Label for complete state. +be.complete = Complete # Text shown to users when opening the content insights flyout and there is an error be.contentInsights.contentAnalyticsErrorText = There was a problem loading content insights. Please try again. # Message shown when the user does not have access to view content insights anymore diff --git a/src/elements/common/messages.js b/src/elements/common/messages.js index 0a3821b351..1da503ab4b 100644 --- a/src/elements/common/messages.js +++ b/src/elements/common/messages.js @@ -52,6 +52,11 @@ const messages = defineMessages({ description: 'Message when new preview is available.', defaultMessage: 'A new version of this file is available.', }, + complete: { + id: 'be.complete', + description: 'Label for complete state.', + defaultMessage: 'Complete', + }, loading: { id: 'be.loading', description: 'Label for loading state.', diff --git a/src/elements/content-uploader/IconInProgress.js b/src/elements/content-uploader/IconInProgress.js deleted file mode 100644 index a10a69a0e6..0000000000 --- a/src/elements/content-uploader/IconInProgress.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @flow - * @file Icon - * @author Box - */ - -import * as React from 'react'; -import LoadingIndicator from '../../components/loading-indicator/LoadingIndicator'; -import IconClose from '../../icons/general/IconClose'; - -const IconInProgress = () => ( -
- - -
-); - -export default IconInProgress; diff --git a/src/elements/content-uploader/IconInProgress.tsx b/src/elements/content-uploader/IconInProgress.tsx new file mode 100644 index 0000000000..82df749b8e --- /dev/null +++ b/src/elements/content-uploader/IconInProgress.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { useIntl } from 'react-intl'; +import { LoadingIndicator } from '@box/blueprint-web'; +import { IconCtaIconHover, Size5 } from '@box/blueprint-web-assets/tokens/tokens'; +import { XMark } from '@box/blueprint-web-assets/icons/Fill'; +import messages from '../common/messages'; + +const IconInProgress = () => { + const { formatMessage } = useIntl(); + + return ( +
+ + +
+ ); +}; + +export default IconInProgress; diff --git a/src/elements/content-uploader/ItemAction.scss b/src/elements/content-uploader/ItemAction.scss index c8a9ffc9aa..d538776327 100644 --- a/src/elements/content-uploader/ItemAction.scss +++ b/src/elements/content-uploader/ItemAction.scss @@ -1,23 +1,20 @@ .bcu-item-action { - .crawler { - display: flex; - align-items: center; - justify-content: center; - height: 100%; + display: flex; + align-items: center; + justify-content: center; + + .bcu-ItemAction-loading, + .bcu-IconInProgress-loading { + position: relative; // Override default Blueprint position: `absolute` } button { - display: flex; - // Vertically center icon svg { display: block; } - .be-icon-in-progress { - width: 24px; - height: 24px; - + .bcu-IconInProgress { // Hide cross icon without hover svg { display: none; @@ -25,12 +22,12 @@ } // Display cross icon on hover and hide loading indicator - &:hover .be-icon-in-progress { + &:hover .bcu-IconInProgress { svg { display: block; } - .crawler { + .bcu-IconInProgress-loading { display: none; } } diff --git a/src/elements/content-uploader/ItemAction.tsx b/src/elements/content-uploader/ItemAction.tsx index 1479867984..8e61e14ce7 100644 --- a/src/elements/content-uploader/ItemAction.tsx +++ b/src/elements/content-uploader/ItemAction.tsx @@ -2,17 +2,17 @@ import * as React from 'react'; import { useIntl } from 'react-intl'; import { AxiosError } from 'axios'; import { Button, IconButton, LoadingIndicator, Tooltip } from '@box/blueprint-web'; -import { ArrowCurveForward, Checkmark } from '@box/blueprint-web-assets/icons/Line'; -import { EllipsisBadge, XMark } from '@box/blueprint-web-assets/icons/Fill'; +import { ArrowCurveForward, Checkmark, XMark } from '@box/blueprint-web-assets/icons/Fill'; import { Size5, SurfaceStatusSurfaceSuccess } from '@box/blueprint-web-assets/tokens/tokens'; +import IconInProgress from './IconInProgress'; import { ERROR_CODE_UPLOAD_FILE_SIZE_LIMIT_EXCEEDED, - STATUS_PENDING, - STATUS_IN_PROGRESS, - STATUS_STAGED, STATUS_COMPLETE, STATUS_ERROR, + STATUS_IN_PROGRESS, + STATUS_PENDING, + STATUS_STAGED, } from '../../constants'; import messages from '../common/messages'; @@ -22,7 +22,7 @@ import type { UploadStatus } from '../../common/types/upload'; import './ItemAction.scss'; export interface ItemActionProps { - error?: AxiosError; + error?: Partial; isFolder?: boolean; isResumableUploadsEnabled: boolean; onClick: React.MouseEventHandler; @@ -30,29 +30,6 @@ export interface ItemActionProps { status: UploadStatus; } -const getIconWithTooltip = ( - icon: React.ReactNode, - isDisabled: boolean, - isLoading: boolean, - onClick: React.MouseEventHandler, - tooltip: boolean, - tooltipText: string, -) => { - if (isLoading) { - return ; - } - - if (tooltip) { - return ( - - icon} /> - - ); - } - - return <>{icon}; -}; - const ItemAction = ({ error, isFolder = false, @@ -61,12 +38,15 @@ const ItemAction = ({ onUpgradeCTAClick, status, }: ItemActionProps) => { - const intl = useIntl(); - let icon: React.ReactNode = ; - let tooltip; - let isLoading = false; + const { formatMessage } = useIntl(); const { code } = error || {}; - const { formatMessage } = intl; + + const LoadingIndicatorIcon = () => ( + + ); + + let Icon = XMark; + let tooltip; if (isFolder && status !== STATUS_PENDING) { return null; @@ -74,28 +54,35 @@ const ItemAction = ({ switch (status) { case STATUS_COMPLETE: - icon = ; + Icon = () => ( + + ); if (!isResumableUploadsEnabled) { tooltip = messages.remove; } break; case STATUS_ERROR: - icon = ; + Icon = ArrowCurveForward; tooltip = isResumableUploadsEnabled ? messages.resume : messages.retry; break; case STATUS_IN_PROGRESS: case STATUS_STAGED: if (isResumableUploadsEnabled) { - isLoading = true; + Icon = LoadingIndicatorIcon; } else { - icon = ; + Icon = IconInProgress; tooltip = messages.uploadsCancelButtonTooltip; } break; case STATUS_PENDING: default: if (isResumableUploadsEnabled) { - isLoading = true; + Icon = LoadingIndicatorIcon; } else { tooltip = messages.uploadsCancelButtonTooltip; } @@ -109,16 +96,23 @@ const ItemAction = ({ data-resin-target="large_version_error_inline_upgrade_cta" variant="primary" > - {intl.formatMessage(messages.uploadsFileSizeLimitExceededUpgradeMessageForUpgradeCta)} + {formatMessage(messages.uploadsFileSizeLimitExceededUpgradeMessageForUpgradeCta)} ); } + const isDisabled = status === STATUS_STAGED; const tooltipText = tooltip && formatMessage(tooltip); return (
- {getIconWithTooltip(icon, isDisabled, isLoading, onClick, tooltip, tooltipText)} + {tooltip ? ( + + + + ) : ( + + )}
); }; diff --git a/src/elements/content-uploader/ItemList.tsx b/src/elements/content-uploader/ItemList.tsx index b81c0a0a36..f6d9eb7191 100644 --- a/src/elements/content-uploader/ItemList.tsx +++ b/src/elements/content-uploader/ItemList.tsx @@ -13,6 +13,8 @@ import type { UploadItem } from '../../common/types/upload'; import '@box/react-virtualized/styles.css'; import './ItemList.scss'; +const ACTION_CELL_WIDTH = 32; + export interface ItemListProps { isResumableUploadsEnabled?: boolean; items: UploadItem[]; @@ -38,7 +40,6 @@ const ItemList = ({ const progressCell = progressCellRenderer(!!onUpgradeCTAClick); const actionCell = actionCellRenderer(isResumableUploadsEnabled, onClick, onUpgradeCTAClick); const removeCell = removeCellRenderer(onRemoveClick); - const baseIconWidth = 32; return ( {isResumableUploadsEnabled && ( )}
diff --git a/src/elements/content-uploader/ItemProgress.js b/src/elements/content-uploader/ItemProgress.js deleted file mode 100644 index e070def5c5..0000000000 --- a/src/elements/content-uploader/ItemProgress.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @flow - * @file Upload item progress component - */ - -import * as React from 'react'; -import ProgressBar from './ProgressBar'; -import './ItemProgress.scss'; - -type Props = { - progress: number, -}; - -const ItemProgress = ({ progress }: Props) => ( -
- -
{progress}%
-
-); - -export default ItemProgress; diff --git a/src/elements/content-uploader/ItemProgress.scss b/src/elements/content-uploader/ItemProgress.scss index 6b22b04bc4..da3f4cff5f 100644 --- a/src/elements/content-uploader/ItemProgress.scss +++ b/src/elements/content-uploader/ItemProgress.scss @@ -1,8 +1,8 @@ -.bcu-item-progress { +.bcu-ItemProgress { display: flex; align-items: center; } -.bcu-progress-label { +.bcu-ItemProgress-label { min-width: 35px; // 100% takes up 34.27px } diff --git a/src/elements/content-uploader/ItemProgress.tsx b/src/elements/content-uploader/ItemProgress.tsx new file mode 100644 index 0000000000..d22f6ebaba --- /dev/null +++ b/src/elements/content-uploader/ItemProgress.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import ProgressBar from './ProgressBar'; +import './ItemProgress.scss'; + +export interface ItemProgressProps { + progress: number; +} + +const ItemProgress = ({ progress }: ItemProgressProps) => ( +
+ +
{progress}%
+
+); + +export default ItemProgress; diff --git a/src/elements/content-uploader/ItemRemove.tsx b/src/elements/content-uploader/ItemRemove.tsx index 42264aa75f..65e235f68f 100644 --- a/src/elements/content-uploader/ItemRemove.tsx +++ b/src/elements/content-uploader/ItemRemove.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { useIntl } from 'react-intl'; import { IconButton, Tooltip } from '@box/blueprint-web'; -import { GrayBlack, Size5 } from '@box/blueprint-web-assets/tokens/tokens'; import { XMark } from '@box/blueprint-web-assets/icons/Fill'; import type { UploadItem, UploadStatus } from '../../common/types/upload'; @@ -16,6 +15,8 @@ export interface ItemRemoveProps { } const ItemRemove = ({ onClick, status }: ItemRemoveProps) => { + const { formatMessage } = useIntl(); + const resin: Record = {}; let target = null; @@ -29,20 +30,13 @@ const ItemRemove = ({ onClick, status }: ItemRemoveProps) => { resin['data-resin-target'] = target; } - const intl = useIntl(); const isDisabled = status === STATUS_STAGED; - const tooltipText = intl.formatMessage(messages.remove); + const tooltipText = formatMessage(messages.remove); return (
- - } - {...resin} - /> + +
); diff --git a/src/elements/content-uploader/ProgressBar.tsx b/src/elements/content-uploader/ProgressBar.tsx index 58b20aa049..ac3d333009 100644 --- a/src/elements/content-uploader/ProgressBar.tsx +++ b/src/elements/content-uploader/ProgressBar.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; import './ProgressBar.scss'; -type Props = { +export interface ProgressBarProps { percent?: number; -}; +} -const ProgressBar = ({ percent }: Props) => { - const clampedPercentage = Math.max(0, Math.min(100, percent || 0)); +const ProgressBar = ({ percent = 0 }: ProgressBarProps) => { + const clampedPercentage = Math.max(0, Math.min(100, percent)); const containerStyle = { transitionDelay: clampedPercentage > 0 && clampedPercentage < 100 ? '0' : '0.4s', diff --git a/src/elements/content-uploader/UploadsManager.scss b/src/elements/content-uploader/UploadsManager.scss index e02b3c0d98..962a82e055 100644 --- a/src/elements/content-uploader/UploadsManager.scss +++ b/src/elements/content-uploader/UploadsManager.scss @@ -11,12 +11,12 @@ margin-right: 0; } - .bcu-item-progress { + .bcu-ItemProgress { .bcu-progress-container { display: none; } - .bcu-progress-label { + .bcu-ItemProgress-label { margin-left: auto; } } diff --git a/src/elements/content-uploader/__tests__/ItemAction.test.tsx b/src/elements/content-uploader/__tests__/ItemAction.test.tsx index 92a5812355..1661f1a145 100644 --- a/src/elements/content-uploader/__tests__/ItemAction.test.tsx +++ b/src/elements/content-uploader/__tests__/ItemAction.test.tsx @@ -1,106 +1,70 @@ import * as React from 'react'; -import { AxiosError } from 'axios'; import { render, screen } from '../../../test-utils/testing-library'; -import ItemAction, { ItemActionProps } from '../ItemAction'; -import { - ERROR_CODE_UPLOAD_FILE_SIZE_LIMIT_EXCEEDED, - STATUS_PENDING, - STATUS_IN_PROGRESS, - STATUS_COMPLETE, - STATUS_STAGED, - STATUS_ERROR, -} from '../../../constants'; +import ItemAction from '../ItemAction'; describe('elements/content-uploader/ItemAction', () => { - const defaultError: AxiosError = { - code: '', - config: undefined, - isAxiosError: false, - toJSON: jest.fn(), - name: '', - message: '', + const renderComponent = (props = {}) => { + const defaultProps = { + isResumableUploadsEnabled: false, + onClick: jest.fn(), + status: 'pending', + }; + return render(); }; - const defaultProps: ItemActionProps = { - isResumableUploadsEnabled: false, - onClick: jest.fn(), - status: STATUS_PENDING, - error: defaultError, - isFolder: false, - onUpgradeCTAClick: jest.fn(), - }; - - const renderComponent = (props: Partial) => render(); - test.each` - status - ${STATUS_COMPLETE} - ${STATUS_IN_PROGRESS} - ${STATUS_STAGED} - ${STATUS_ERROR} - ${STATUS_PENDING} - `('should render correctly with $status', ({ status }: Pick) => { - renderComponent({ status }); - expect(screen.getByRole('button')).toBeInTheDocument(); + status | label + ${'complete'} | ${'Remove'} + ${'error'} | ${'Retry'} + ${'inprogress'} | ${'Cancel this upload'} + ${'pending'} | ${'Cancel this upload'} + ${'staged'} | ${'Cancel this upload'} + ${'unknown'} | ${'Cancel this upload'} + `('renders icon button when status is `$status` and resumable uploads is disabled', ({ label, status }) => { + renderComponent({ isResumableUploadsEnabled: false, status }); + expect(screen.getByRole('button', { name: label })).toBeInTheDocument(); }); - test.each` - status | label - ${STATUS_COMPLETE} | ${'complete'} - ${STATUS_ERROR} | ${'error'} - `( - 'should render correctly with $status and resumable uploads enabled', - ({ status, label }: Pick & { label: string }) => { - renderComponent({ status, isResumableUploadsEnabled: true }); - expect(screen.getByRole('img', { name: label })).toBeInTheDocument(); + test.each(['inprogress', 'pending', 'staged', 'unknown'])( + 'renders loading indicator when status is `%s` and resumable uploads is enabled', + status => { + renderComponent({ isResumableUploadsEnabled: true, status }); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + expect(screen.getByRole('status', { name: 'Loading' })).toBeInTheDocument(); }, ); - test.each` - status - ${STATUS_IN_PROGRESS} - ${STATUS_PENDING} - ${STATUS_STAGED} - `( - 'should render correctly with $status and resumable uploads enabled', - ({ status }: Pick) => { - renderComponent({ status, isResumableUploadsEnabled: true }); - expect(screen.getByRole('status', { name: 'loading' })).toBeInTheDocument(); - }, - ); + test('renders correctly when status is `complete` and resumable uploads is enabled', () => { + renderComponent({ isResumableUploadsEnabled: true, status: 'complete' }); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + expect(screen.getByRole('img', { name: 'Complete' })).toBeInTheDocument(); + }); - test.each` - status | label - ${STATUS_IN_PROGRESS} | ${'staged'} - ${STATUS_STAGED} | ${'staged'} - `( - 'should render correctly with $status and resumable uploads disabled', - ({ status, label }: Pick & { label: string }) => { - renderComponent({ status, isResumableUploadsEnabled: false }); - expect(screen.getByRole('img', { name: label })).toBeInTheDocument(); + test('renders correctly when status is `error` and resumable uploads is enabled', () => { + renderComponent({ isResumableUploadsEnabled: true, status: 'error' }); + expect(screen.getByRole('button', { name: 'Resume' })).toBeInTheDocument(); + }); + + test.each(['complete', 'error', 'inprogress', 'staged', 'unknown'])( + 'renders an empty component when item is folder and status is `%s`', + status => { + const { container } = renderComponent({ isFolder: true, status }); + expect(container).toBeEmptyDOMElement(); }, ); - test('should render correctly with STATUS_PENDING and resumable uploads disabled', () => { - renderComponent({ status: STATUS_PENDING, isResumableUploadsEnabled: false }); - expect(screen.getByRole('button', { name: 'Cancel this upload' })).toBeInTheDocument(); + test('does not render an empty component when item is folder and status is `pending`', () => { + const { container } = renderComponent({ isFolder: true, status: 'pending' }); + expect(container).not.toBeEmptyDOMElement(); }); - test('should render correctly with STATUS_ERROR and item is folder', () => { - renderComponent({ status: STATUS_ERROR, isFolder: true }); - expect(screen.queryByRole('button')).not.toBeInTheDocument(); + test('renders CTA button when status is `error` and code is `file_size_limit_exceeded`', () => { + renderComponent({ error: { code: 'file_size_limit_exceeded' }, onUpgradeCTAClick: jest.fn(), status: 'error' }); + expect(screen.getByRole('button', { name: 'Upgrade' })).toBeInTheDocument(); }); - test('should render CTA button to upgrade when upload file size exceeded error is received', () => { - renderComponent({ - status: STATUS_ERROR, - error: { ...defaultError, code: ERROR_CODE_UPLOAD_FILE_SIZE_LIMIT_EXCEEDED }, - onUpgradeCTAClick: jest.fn(), - }); - expect( - screen.getByRole('button', { - name: 'Upgrade', - }), - ).toBeInTheDocument(); + test('does not render CTA button when status is `error` and code is not `file_size_limit_exceeded`', () => { + renderComponent({ error: { code: 'unknown' }, onUpgradeCTAClick: jest.fn(), status: 'error' }); + expect(screen.queryByRole('button', { name: 'Upgrade' })).not.toBeInTheDocument(); }); }); diff --git a/src/elements/content-uploader/__tests__/ItemList.test.js b/src/elements/content-uploader/__tests__/ItemList.test.js deleted file mode 100644 index 67a39b8ebf..0000000000 --- a/src/elements/content-uploader/__tests__/ItemList.test.js +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react'; -import { mount } from 'enzyme'; -// TODO Providers can be removed when converted to RTL - providers are included in the testing utility library -import { IntlProvider } from 'react-intl'; -import { TooltipProvider } from '@box/blueprint-web'; - -import ItemList from '../ItemList'; -import { ERROR_CODE_UPLOAD_FILE_SIZE_LIMIT_EXCEEDED, STATUS_COMPLETE, STATUS_ERROR } from '../../../constants'; - -jest.unmock('react-intl'); // TODO can be removed when converted to RTL -jest.mock( - '@box/react-virtualized/dist/es/AutoSizer', - () => - ({ children }) => - children({ height: 600, width: 600 }), -); - -describe('elements/content-uploader/ItemList', () => { - const renderComponent = props => - mount( - - - {}} {...props} /> - - , - ); - - describe('render()', () => { - test('should render default component', () => { - const wrapper = renderComponent(); - - expect(wrapper.find('Table').length).toBe(1); - expect(wrapper.find('Table.bcu-item-list').length).toBe(1); - }); - - 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 }, - ]; - const wrapper = renderComponent({ items }); - expect(wrapper.find('div.bcu-item-row').length).toBe(3); - const actionColumnStyle = wrapper.find('.bcu-item-list-action-column').first().prop('style'); - expect(actionColumnStyle.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 }, - ]; - const wrapper = renderComponent({ items, onUpgradeCTAClick: () => {} }); - expect(wrapper.find('div.bcu-item-row').length).toBe(1); - const actionColumnStyle = wrapper.find('.bcu-item-list-action-column').prop('style'); - expect(actionColumnStyle.flex).toEqual('0 0 100px'); - }); - }); -});