Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(content-uploader): migrate upload state #3624

Merged
merged 5 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions i18n/en-US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -778,12 +778,16 @@ be.upload = Upload
be.uploadEmptyFileInput = Browse your device
# Message shown for upload link for uploading more folders when there are no items to upload
be.uploadEmptyFolderInput = Select Folders
# Label for upload empty state.
be.uploadEmptyState = Empty state
# Message shown when there are no items to upload and folder upload is disabled
be.uploadEmptyWithFolderUploadDisabled = Drag and drop files
# Message shown when there are no items to upload and folder upload is enabled
be.uploadEmptyWithFolderUploadEnabled = Drag and drop files and folders
# Message shown when there is a network error when uploading
be.uploadError = A network error has occurred while trying to upload.
# Label for upload error state.
be.uploadErrorState = Error state
# Message shown when too many files are uploaded at once
be.uploadErrorTooManyFiles = You can only upload up to {fileLimit} file(s) at a time.
# Message shown when user drag and drops files onto uploads in progress
Expand All @@ -798,6 +802,8 @@ be.uploadSuccess = Success! Your files have been uploaded.
be.uploadSuccessFileInput = Select More Files
# Message shown for upload link for uploading more folders after a successful upload
be.uploadSuccessFolderInput = Select More Folders
# Label for upload success state.
be.uploadSuccessState = Success state
# Cancel upload button tooltip
be.uploadsCancelButtonTooltip = Cancel this upload
# Default error message shown when upload fails
Expand Down
15 changes: 15 additions & 0 deletions src/elements/common/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,21 @@ const messages = defineMessages({
description: 'Label for upload action.',
defaultMessage: 'Upload',
},
uploadEmptyState: {
id: 'be.uploadEmptyState',
description: 'Label for upload empty state.',
defaultMessage: 'Empty state',
},
uploadErrorState: {
id: 'be.uploadErrorState',
description: 'Label for upload error state.',
defaultMessage: 'Error state',
},
uploadSuccessState: {
id: 'be.uploadSuccessState',
description: 'Label for upload success state.',
defaultMessage: 'Success state',
},
add: {
id: 'be.add',
description: 'Label for add action',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
/**
* @flow
* @file Upload state component
*/

import * as React from 'react';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import ErrorEmptyState from '../../icons/states/ErrorEmptyState';
import { useIntl, FormattedMessage } from 'react-intl';
import { HatWand } from '@box/blueprint-web-assets/illustrations/Medium';

import UploadEmptyState from '../../icons/states/UploadEmptyState';
import UploadSuccessState from '../../icons/states/UploadSuccessState';
import messages from '../common/messages';
import UploadStateContent from './UploadStateContent';
import { VIEW_ERROR, VIEW_UPLOAD_EMPTY, VIEW_UPLOAD_IN_PROGRESS, VIEW_UPLOAD_SUCCESS } from '../../constants';
import type { View } from '../../common/types/core';

import { VIEW_ERROR, VIEW_UPLOAD_EMPTY, VIEW_UPLOAD_IN_PROGRESS, VIEW_UPLOAD_SUCCESS } from '../../constants';

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

import './UploadState.scss';

type Props = {
Expand All @@ -22,20 +20,29 @@ type Props = {
isFolderUploadEnabled: boolean,
isOver: boolean,
isTouch: boolean,
onSelect: Function,
view: View,
onSelect: () => void,
view: View
};

const UploadState = ({ canDrop, hasItems, isOver, isTouch, view, onSelect, isFolderUploadEnabled }: Props) => {
const UploadState = ({
canDrop,
hasItems,
isOver,
isTouch,
view,
onSelect,
isFolderUploadEnabled,
}: Props) => {
const intl = useIntl();
let icon;
let content;
switch (view) {
case VIEW_ERROR:
icon = <ErrorEmptyState />;
icon = <HatWand aria-label={intl.formatMessage(messages.uploadErrorState)} height={126} width={130} />;
content = <UploadStateContent message={<FormattedMessage {...messages.uploadError} />} />;
break;
case VIEW_UPLOAD_EMPTY:
icon = <UploadEmptyState />;
icon = <UploadEmptyState title={<FormattedMessage {...messages.uploadEmptyState} />} />;
/* eslint-disable no-nested-ternary */
content =
canDrop && hasItems ? (
Expand Down Expand Up @@ -65,11 +72,11 @@ const UploadState = ({ canDrop, hasItems, isOver, isTouch, view, onSelect, isFol
/* eslint-enable no-nested-ternary */
break;
case VIEW_UPLOAD_IN_PROGRESS:
icon = <UploadEmptyState />;
icon = <UploadEmptyState title={<FormattedMessage {...messages.uploadEmptyState} />} />;
content = <UploadStateContent message={<FormattedMessage {...messages.uploadInProgress} />} />;
break;
case VIEW_UPLOAD_SUCCESS:
icon = <UploadSuccessState />;
icon = <UploadSuccessState title={<FormattedMessage {...messages.uploadSuccessState} />} />;
content = (
<UploadStateContent
fileInputLabel={<FormattedMessage {...messages.uploadSuccessFileInput} />}
Expand Down
114 changes: 114 additions & 0 deletions src/elements/content-uploader/UploadState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import * as React from 'react';
import classNames from 'classnames';
import { useIntl, FormattedMessage } from 'react-intl';
import { HatWand } from '@box/blueprint-web-assets/illustrations/Medium';

import UploadEmptyState from '../../icons/states/UploadEmptyState';
import UploadSuccessState from '../../icons/states/UploadSuccessState';
import UploadStateContent from './UploadStateContent';
import type { View } from '../../common/types/core';

import { VIEW_ERROR, VIEW_UPLOAD_EMPTY, VIEW_UPLOAD_IN_PROGRESS, VIEW_UPLOAD_SUCCESS } from '../../constants';

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

import './UploadState.scss';

export interface UploadStateProps {
canDrop: boolean;
hasItems: boolean;
isFolderUploadEnabled: boolean;
isOver: boolean;
isTouch: boolean;
onSelect: () => void;
view: View;
}

const UploadState = ({
canDrop,
hasItems,
isOver,
isTouch,
view,
onSelect,
isFolderUploadEnabled,
}: UploadStateProps) => {
const intl = useIntl();
let icon;
let content;
switch (view) {
case VIEW_ERROR:
icon = <HatWand aria-label={intl.formatMessage(messages.uploadErrorState)} height={126} width={130} />;
content = <UploadStateContent message={<FormattedMessage {...messages.uploadError} />} />;
break;
case VIEW_UPLOAD_EMPTY:
icon = <UploadEmptyState title={<FormattedMessage {...messages.uploadEmptyState} />} />;
/* eslint-disable no-nested-ternary */
content =
canDrop && hasItems ? (
<UploadStateContent message={<FormattedMessage {...messages.uploadInProgress} />} />
) : isTouch ? (
<UploadStateContent
fileInputLabel={<FormattedMessage {...messages.uploadNoDragDrop} />}
onChange={onSelect}
useButton
/>
) : (
<UploadStateContent
fileInputLabel={<FormattedMessage {...messages.uploadEmptyFileInput} />}
folderInputLabel={
isFolderUploadEnabled && <FormattedMessage {...messages.uploadEmptyFolderInput} />
}
message={
isFolderUploadEnabled ? (
<FormattedMessage {...messages.uploadEmptyWithFolderUploadEnabled} />
) : (
<FormattedMessage {...messages.uploadEmptyWithFolderUploadDisabled} />
)
}
onChange={onSelect}
/>
);
/* eslint-enable no-nested-ternary */
break;
case VIEW_UPLOAD_IN_PROGRESS:
icon = <UploadEmptyState title={<FormattedMessage {...messages.uploadEmptyState} />} />;
content = <UploadStateContent message={<FormattedMessage {...messages.uploadInProgress} />} />;
break;
case VIEW_UPLOAD_SUCCESS:
icon = <UploadSuccessState title={<FormattedMessage {...messages.uploadSuccessState} />} />;
content = (
<UploadStateContent
fileInputLabel={<FormattedMessage {...messages.uploadSuccessFileInput} />}
folderInputLabel={
isFolderUploadEnabled && <FormattedMessage {...messages.uploadSuccessFolderInput} />
}
message={<FormattedMessage {...messages.uploadSuccess} />}
onChange={onSelect}
useButton={isTouch}
/>
);
break;
default:
break;
/* eslint-enable jsx-a11y/label-has-for */
}

const className = classNames('bcu-upload-state', {
'bcu-is-droppable': isOver && canDrop,
'bcu-is-not-droppable': isOver && !canDrop,
'bcu-has-items': hasItems,
});

return (
<div className={className}>
<div>
{icon}
{content}
</div>
<div className="bcu-drag-drop-overlay" />
</div>
);
};

export default UploadState;
75 changes: 0 additions & 75 deletions src/elements/content-uploader/__tests__/UploadState.test.js

This file was deleted.

72 changes: 72 additions & 0 deletions src/elements/content-uploader/__tests__/UploadState.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as React from 'react';
import { render, screen } from '../../../test-utils/testing-library';

import UploadState, { UploadStateProps } from '../UploadState';

import { VIEW_ERROR, VIEW_UPLOAD_EMPTY, VIEW_UPLOAD_IN_PROGRESS, VIEW_UPLOAD_SUCCESS } from '../../../constants';

describe('elements/content-uploader/UploadState', () => {
const getWrapper = (props: Partial<UploadStateProps>) =>
render(
<UploadState
canDrop={false}
hasItems={false}
isFolderUploadEnabled={false}
isOver={false}
isTouch={false}
onSelect={jest.fn()}
view={VIEW_ERROR}
{...props}
/>,
);

test.each`
view | iconName
${VIEW_ERROR} | ${'Error state'}
${VIEW_UPLOAD_EMPTY} | ${'Empty state'}
${VIEW_UPLOAD_IN_PROGRESS} | ${'Empty state'}
${VIEW_UPLOAD_SUCCESS} | ${'Success state'}
`('should render icon correctly based on the view', ({ view, iconName }) => {
getWrapper({ view });
expect(screen.getByRole('img', { name: iconName })).toBeInTheDocument();
});

test('should render upload_empty view correctly when canDrop and hasItems set to true', () => {
getWrapper({ view: VIEW_UPLOAD_EMPTY, canDrop: true, hasItems: true });
expect(screen.getByText('Drag and drop to add additional files')).toBeInTheDocument();
});

test('should render upload_empty view correctly when isTouch set to true', () => {
getWrapper({ view: VIEW_UPLOAD_EMPTY, isTouch: true });
expect(screen.getByText('Select files from your device')).toBeInTheDocument();
});

test('should render content for upload_empty view correctly when isFolderUploadEnabled set to false', () => {
getWrapper({ view: VIEW_UPLOAD_EMPTY });
expect(screen.getByText('Browse your device')).toBeInTheDocument();
expect(screen.getByText('Drag and drop files')).toBeInTheDocument();
});

test('should render content for upload_empty view correctly when isFolderUploadEnabled set to true', () => {
getWrapper({ view: VIEW_UPLOAD_EMPTY, isFolderUploadEnabled: true });
expect(screen.getByText('Select Folders')).toBeInTheDocument();
expect(screen.getByText('Drag and drop files and folders')).toBeInTheDocument();
});

test('should render content for upload_in_progress view correctly when isFolderUploadEnabled set to true', () => {
getWrapper({ view: VIEW_UPLOAD_IN_PROGRESS });
expect(screen.getByText('Drag and drop to add additional files')).toBeInTheDocument();
});

test('should render content for upload_success view correctly when isFolderUploadEnabled set to false', () => {
getWrapper({ view: VIEW_UPLOAD_SUCCESS });
expect(screen.getByText('Select More Files')).toBeInTheDocument();
expect(screen.getByText('Success! Your files have been uploaded.')).toBeInTheDocument();
});

test('should render content for upload_success view correctly when isFolderUploadEnabled set to true', () => {
getWrapper({ view: VIEW_UPLOAD_SUCCESS, isFolderUploadEnabled: true });
expect(screen.getByText('Select More Folders')).toBeInTheDocument();
expect(screen.getByText('Success! Your files have been uploaded.')).toBeInTheDocument();
});
});
Loading
Loading