Unverified Commit 1d5d3eac authored by Sudheer's avatar Sudheer Committed by GitHub
Browse files

MM-4712 Add progress bar to uploads (#2010)

* MM-4712 Add progressbar to uploads

* Fix % spacing

* * Add a new component for file_progress

* * Change extentions key to type key
parent 5540a318
......@@ -134,6 +134,7 @@ exports[`components/CreateComment should match snapshot, comment with message 1`
onFileUpload={[Function]}
onFileUploadChange={[Function]}
onUploadError={[Function]}
onUploadProgress={[Function]}
onUploadStart={[Function]}
postType="comment"
rootId=""
......@@ -261,6 +262,7 @@ exports[`components/CreateComment should match snapshot, emoji picker disabled 1
onFileUpload={[Function]}
onFileUploadChange={[Function]}
onUploadError={[Function]}
onUploadProgress={[Function]}
onUploadStart={[Function]}
postType="comment"
rootId=""
......@@ -291,6 +293,7 @@ exports[`components/CreateComment should match snapshot, emoji picker disabled 1
}
onRemove={[Function]}
uploadsInProgress={Array []}
uploadsProgressPercent={Object {}}
/>
</div>
</div>
......@@ -375,6 +378,7 @@ exports[`components/CreateComment should match snapshot, empty comment 1`] = `
onFileUpload={[Function]}
onFileUploadChange={[Function]}
onUploadError={[Function]}
onUploadProgress={[Function]}
onUploadStart={[Function]}
postType="comment"
rootId=""
......@@ -502,6 +506,7 @@ exports[`components/CreateComment should match snapshot, non-empty message and u
onFileUpload={[Function]}
onFileUploadChange={[Function]}
onUploadError={[Function]}
onUploadProgress={[Function]}
onUploadStart={[Function]}
postType="comment"
rootId=""
......@@ -569,6 +574,7 @@ exports[`components/CreateComment should match snapshot, non-empty message and u
Object {},
]
}
uploadsProgressPercent={Object {}}
/>
</div>
</div>
......
......@@ -17,7 +17,7 @@ import {containsAtChannel, postMessageOnKeyPress, shouldFocusMainTextbox} from '
import ConfirmModal from 'components/confirm_modal.jsx';
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
import FilePreview from 'components/file_preview.jsx';
import FilePreview from 'components/file_preview/file_preview.jsx';
import FileUpload from 'components/file_upload';
import MsgTyping from 'components/msg_typing';
import PostDeletedModal from 'components/post_deleted_modal.jsx';
......@@ -193,6 +193,7 @@ export default class CreateComment extends React.PureComponent {
fileInfos: [],
},
channelMembersCount: 0,
uploadsProgressPercent: {},
};
this.lastBlurAt = 0;
......@@ -536,6 +537,11 @@ export default class CreateComment extends React.PureComponent {
this.focusTextbox();
}
handleUploadProgress = ({clientId, name, percent, type}) => {
const uploadsProgressPercent = {...this.state.uploadsProgressPercent, [clientId]: {percent, name, type}};
this.setState({uploadsProgressPercent});
}
handleFileUploadComplete = (fileInfos, clientIds, channelId, rootId) => {
const draft = this.draftsForPost[rootId];
const uploadsInProgress = [...draft.uploadsInProgress];
......@@ -735,6 +741,7 @@ export default class CreateComment extends React.PureComponent {
fileInfos={draft.fileInfos}
onRemove={this.removePreview}
uploadsInProgress={draft.uploadsInProgress}
uploadsProgressPercent={this.state.uploadsProgressPercent}
ref='preview'
/>
);
......@@ -775,6 +782,7 @@ export default class CreateComment extends React.PureComponent {
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
onUploadProgress={this.handleUploadProgress}
rootId={this.props.rootId}
postType='comment'
/>
......
......@@ -7,6 +7,8 @@ import {shallow} from 'enzyme';
import Constants from 'utils/constants.jsx';
import CreateComment from 'components/create_comment/create_comment.jsx';
import FileUpload from 'components/file_upload';
import FilePreview from 'components/file_preview/file_preview.jsx';
jest.mock('stores/post_store.jsx', () => ({
clearCommentDraftUploads: jest.fn(),
......@@ -315,6 +317,17 @@ describe('components/CreateComment', () => {
expect(wrapper.state().draft.fileInfos).toEqual(expectedNewFileInfos);
});
it('check for uploadsProgressPercent state on handleUploadProgress callback', () => {
const wrapper = shallow(
<CreateComment {...baseProps}/>
);
wrapper.find(FileUpload).prop('onUploadProgress')({clientId: 'clientId', name: 'name', percent: 10, type: 'type'});
expect(wrapper.find(FilePreview).prop('uploadsProgressPercent')).toEqual({clientId: {percent: 10, name: 'name', type: 'type'}});
expect(wrapper.state('uploadsProgressPercent')).toEqual({clientId: {percent: 10, name: 'name', type: 'type'}});
});
test('calls showPostDeletedModal when createPostErrorId === api.post.create_post.root_id.app_error', () => {
const onUpdateCommentDraft = jest.fn();
const draft = {
......
......@@ -42,6 +42,7 @@ exports[`components/create_post Show tutorial 1`] = `
onFileUpload={[Function]}
onFileUploadChange={[Function]}
onUploadError={[Function]}
onUploadProgress={[Function]}
onUploadStart={[Function]}
postType="post"
/>
......@@ -206,6 +207,7 @@ exports[`components/create_post should match snapshot for center textbox 1`] = `
onFileUpload={[Function]}
onFileUploadChange={[Function]}
onUploadError={[Function]}
onUploadProgress={[Function]}
onUploadStart={[Function]}
postType="post"
/>
......@@ -439,6 +441,7 @@ exports[`components/create_post should match snapshot when file upload disabled
onFileUpload={[Function]}
onFileUploadChange={[Function]}
onUploadError={[Function]}
onUploadProgress={[Function]}
onUploadStart={[Function]}
postType="post"
/>
......@@ -572,6 +575,7 @@ exports[`components/create_post should match snapshot, init 1`] = `
onFileUpload={[Function]}
onFileUploadChange={[Function]}
onUploadError={[Function]}
onUploadProgress={[Function]}
onUploadStart={[Function]}
postType="post"
/>
......
......@@ -17,7 +17,7 @@ import * as Utils from 'utils/utils.jsx';
import ConfirmModal from 'components/confirm_modal.jsx';
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
import FilePreview from 'components/file_preview.jsx';
import FilePreview from 'components/file_preview/file_preview.jsx';
import FileUpload from 'components/file_upload';
import MsgTyping from 'components/msg_typing';
import PostDeletedModal from 'components/post_deleted_modal.jsx';
......@@ -240,6 +240,7 @@ export default class CreatePost extends React.Component {
showEmojiPicker: false,
showConfirmModal: false,
channelMembersCount: 0,
uploadsProgressPercent: {},
};
this.lastBlurAt = 0;
......@@ -597,6 +598,11 @@ export default class CreatePost extends React.Component {
this.focusTextbox();
}
handleUploadProgress = ({clientId, name, percent, type}) => {
const uploadsProgressPercent = {...this.state.uploadsProgressPercent, [clientId]: {percent, name, type}};
this.setState({uploadsProgressPercent});
}
handleFileUploadComplete = (fileInfos, clientIds, channelId) => {
const draft = {...this.draftsForChannel[channelId]};
......@@ -967,6 +973,7 @@ export default class CreatePost extends React.Component {
fileInfos={draft.fileInfos}
onRemove={this.removePreview}
uploadsInProgress={draft.uploadsInProgress}
uploadsProgressPercent={this.state.uploadsProgressPercent}
/>
);
}
......@@ -1007,6 +1014,7 @@ export default class CreatePost extends React.Component {
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
onUploadProgress={this.handleUploadProgress}
postType='post'
/>
);
......
......@@ -12,6 +12,7 @@ import Constants, {StoragePrefixes, ModalIdentifiers} from 'utils/constants.jsx'
import * as Utils from 'utils/utils.jsx';
import CreatePost from 'components/create_post/create_post.jsx';
import FileUpload from 'components/file_upload';
jest.mock('actions/global_actions.jsx', () => ({
emitLocalUserTypingEvent: jest.fn(),
......@@ -556,6 +557,13 @@ describe('components/create_post', () => {
expect(setDraft).toHaveBeenCalledWith(StoragePrefixes.DRAFT + currentChannelProp.id, draftProp);
});
it('check for uploadsProgressPercent state on handleUploadProgress callback', () => {
const wrapper = shallow(createPost({}));
wrapper.find(FileUpload).prop('onUploadProgress')({clientId: 'clientId', name: 'name', percent: 10, type: 'type'});
expect(wrapper.state('uploadsProgressPercent')).toEqual({clientId: {percent: 10, name: 'name', type: 'type'}});
});
it('Remove preview from fileInfos', () => {
const setDraft = jest.fn();
const fileInfos = {
......
......@@ -70,25 +70,18 @@ exports[`component/FilePreview should match snapshot 1`] = `
</div>
</div>
</div>
<div
className="file-preview"
data-client-id="clientID_1"
<FileProgressPreview
clientId="clientID_1"
fileInfo={
Object {
"extension": "image/png",
"name": "file",
"percent": 50,
}
}
handleRemove={[Function]}
key="clientID_1"
>
<img
className="spinner"
src={null}
/>
<a
className="file-preview__remove"
onClick={[Function]}
>
<i
className="fa fa-remove"
title="Remove Icon"
/>
</a>
</div>
/>
</div>
`;
......@@ -162,25 +155,18 @@ exports[`component/FilePreview should match snapshot when props are changed 1`]
</div>
</div>
</div>
<div
className="file-preview"
data-client-id="clientID_1"
<FileProgressPreview
clientId="clientID_1"
fileInfo={
Object {
"extension": "image/png",
"name": "file",
"percent": 50,
}
}
handleRemove={[Function]}
key="clientID_1"
>
<img
className="spinner"
src={null}
/>
<a
className="file-preview__remove"
onClick={[Function]}
>
<i
className="fa fa-remove"
title="Remove Icon"
/>
</a>
</div>
/>
</div>
`;
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`component/file_preview/file_progress_preview should match snapshot 1`] = `
<div
className="file-preview post-image__column"
data-client-id="clientId"
key="clientId"
>
<div
className="post-image__thumbnail"
>
<div
className="file-icon image"
/>
</div>
<div
className="post-image__details"
>
<div
className="post-image__detail_wrapper"
>
<div
className="post-image__detail"
>
<FilenameOverlay
canDownload={false}
compactDisplay={false}
fileInfo={
Object {
"name": "file",
"percent": 50,
"type": "image/png",
}
}
handleImageClick={null}
index="clientId"
/>
<span
className="post-image__uploadingTxt"
>
<FormattedMessage
defaultMessage="Uploading..."
id="admin.plugin.uploading"
values={Object {}}
/>
<span>
(50%)
</span>
</span>
<ProgressBar
active={false}
bsClass="progress-bar"
className="post-image__progressBar"
isChild={false}
max={100}
min={0}
now={50}
srOnly={false}
striped={false}
/>
</div>
</div>
<div>
<a
className="file-preview__remove"
onClick={[Function]}
>
<i
className="fa fa-remove"
title="Remove Icon"
/>
</a>
</div>
</div>
</div>
`;
......@@ -9,26 +9,22 @@ import FilenameOverlay from 'components/file_attachment/filename_overlay.jsx';
import Constants, {FileTypes} from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import loadingGif from 'images/load.gif';
import FileProgressPreview from './file_progress_preview.jsx';
export default class FilePreview extends React.PureComponent {
static propTypes = {
onRemove: PropTypes.func.isRequired,
fileInfos: PropTypes.arrayOf(PropTypes.object).isRequired,
uploadsInProgress: PropTypes.array,
uploadsProgressPercent: PropTypes.object,
};
static defaultProps = {
fileInfos: [],
uploadsInProgress: [],
uploadsProgressPercent: {},
};
componentDidUpdate() {
if (this.props.uploadsInProgress.length > 0) {
this.refs[this.props.uploadsInProgress[0]].scrollIntoView();
}
}
handleRemove = (id) => {
this.props.onRemove(id);
}
......@@ -115,26 +111,12 @@ export default class FilePreview extends React.PureComponent {
this.props.uploadsInProgress.forEach((clientId) => {
previews.push(
<div
ref={clientId}
<FileProgressPreview
key={clientId}
className='file-preview'
data-client-id={clientId}
>
<img
className='spinner'
src={loadingGif}
/>
<a
className='file-preview__remove'
onClick={this.handleRemove.bind(this, clientId)}
>
<i
className='fa fa-remove'
title={Utils.localizeMessage('generic_icons.remove', 'Remove Icon')}
/>
</a>
</div>
clientId={clientId}
fileInfo={this.props.uploadsProgressPercent[clientId]}
handleRemove={this.handleRemove}
/>
);
});
......
......@@ -4,7 +4,7 @@
import React from 'react';
import {shallow} from 'enzyme';
import FilePreview from 'components/file_preview.jsx';
import FilePreview from './file_preview.jsx';
describe('component/FilePreview', () => {
const onRemove = jest.fn();
......@@ -18,11 +18,19 @@ describe('component/FilePreview', () => {
},
];
const uploadsInProgress = ['clientID_1'];
const uploadsProgressPercent = {
clientID_1: {
name: 'file',
percent: 50,
extension: 'image/png',
},
};
const baseProps = {
fileInfos,
uploadsInProgress,
onRemove,
uploadsProgressPercent,
};
test('should match snapshot', () => {
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {ProgressBar} from 'react-bootstrap';
import FilenameOverlay from 'components/file_attachment/filename_overlay.jsx';
import {getFileTypeFromMime} from 'utils/file_utils';
import * as Utils from 'utils/utils.jsx';
export default class FileProgressPreview extends React.PureComponent {
static propTypes = {
handleRemove: PropTypes.func.isRequired,
clientId: PropTypes.string.isRequired,
fileInfo: PropTypes.object,
};
handleRemove = () => {
this.props.handleRemove(this.props.clientId);
}
render() {
let percent = 0;
let fileNameComponent;
let previewImage;
const {fileInfo, clientId} = this.props;
if (fileInfo) {
percent = fileInfo.percent;
const percentTxt = ` (${percent.toFixed(0)}%)`;
const fileType = getFileTypeFromMime(fileInfo.type);
previewImage = <div className={'file-icon ' + Utils.getIconClassName(fileType)}/>;
fileNameComponent = (
<React.Fragment>
<FilenameOverlay
fileInfo={fileInfo}
index={clientId}
handleImageClick={null}
compactDisplay={false}
canDownload={false}
/>
<span className='post-image__uploadingTxt'>
{percent === 100 ? (
<FormattedMessage
id='create_post.fileProcessing'
defaultMessage='Processing...'
/>
) : (
<React.Fragment>
<FormattedMessage
id='admin.plugin.uploading'
defaultMessage='Uploading...'
/>
<span>{percentTxt}</span>
</React.Fragment>
)}
</span>
<ProgressBar
className='post-image__progressBar'
now={percent}
active={percent === 100}
/>
</React.Fragment>
);
}
return (
<div
ref={clientId}
key={clientId}
className='file-preview post-image__column'
data-client-id={clientId}
>
<div className='post-image__thumbnail'>
{previewImage}
</div>
<div className='post-image__details'>
<div className='post-image__detail_wrapper'>
<div className='post-image__detail'>
{fileNameComponent}
</div>
</div>
<div>
<a
className='file-preview__remove'
onClick={this.handleRemove}
>
<i
className='fa fa-remove'
title={Utils.localizeMessage('generic_icons.remove', 'Remove Icon')}
/>
</a>
</div>
</div>
</div>
);
}
}
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import FileProgressPreview from './file_progress_preview.jsx';
describe('component/file_preview/file_progress_preview', () => {
const handleRemove = jest.fn();
const fileInfo = {
name: 'file',
percent: 50,
type: 'image/png',
};
const baseProps = {
clientId: 'clientId',
fileInfo,
handleRemove,
};
test('should match snapshot', () => {
const wrapper = shallow(
<FileProgressPreview {...baseProps}/>
);
expect(wrapper).toMatchSnapshot();
});
});
......@@ -131,6 +131,10 @@ export default class FileUpload extends PureComponent {
pluginFileUploadMethods: PropTypes.arrayOf(PropTypes.object),
pluginFilesWillUploadHooks: PropTypes.arrayOf(PropTypes.object),
/**
* Function called when superAgent fires progress event.
*/
onUploadProgress: PropTypes.func.isRequired,
actions: PropTypes.shape({
/**
......@@ -264,6 +268,15 @@ export default class FileUpload extends PureComponent {
clientId,
);
request.on('progress', (progressEvent) => {
this.props.onUploadProgress({
clientId,