Merge pull request #378 from rgarmsen2295/mm-316c

MM-316 Allows users to drag and drop files from their computer to the center pane or RHS to upload them
Этот коммит содержится в:
Christopher Speller
2015-08-18 08:46:14 -04:00
родитель ab197e9835 2c098d7711
Коммит 96d1eb1c80
16 изменённых файлов: 327 добавлений и 33 удалений

Просмотреть файл

@@ -122,16 +122,20 @@ module.exports = React.createClass({
this.setState({uploadsInProgress: draft['uploadsInProgress'], previews: draft['previews']});
},
handleUploadError: function(err, clientId) {
var draft = PostStore.getCommentDraft(this.props.rootId);
if (clientId !== -1) {
var draft = PostStore.getCommentDraft(this.props.rootId);
var index = draft['uploadsInProgress'].indexOf(clientId);
if (index !== -1) {
draft['uploadsInProgress'].splice(index, 1);
var index = draft['uploadsInProgress'].indexOf(clientId);
if (index !== -1) {
draft['uploadsInProgress'].splice(index, 1);
}
PostStore.storeCommentDraft(this.props.rootId, draft);
this.setState({uploadsInProgress: draft['uploadsInProgress'], serverError: err});
} else {
this.setState({serverError: err});
}
PostStore.storeCommentDraft(this.props.rootId, draft);
this.setState({uploadsInProgress: draft['uploadsInProgress'], serverError: err});
},
clearPreviews: function() {
this.setState({previews: []});
@@ -222,7 +226,9 @@ module.exports = React.createClass({
getFileCount={this.getFileCount}
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError} />
onUploadError={this.handleUploadError}
postType='comment'
channelId={this.props.channelId} />
</div>
<MsgTyping channelId={this.props.channelId} parentId={this.props.rootId} />
<div className={postFooterClassName}>

Просмотреть файл

@@ -145,16 +145,20 @@ module.exports = React.createClass({
this.setState({uploadsInProgress: draft['uploadsInProgress'], previews: draft['previews']});
},
handleUploadError: function(err, clientId) {
var draft = PostStore.getDraft(this.state.channelId);
if (clientId !== -1) {
var draft = PostStore.getDraft(this.state.channelId);
var index = draft['uploadsInProgress'].indexOf(clientId);
if (index !== -1) {
draft['uploadsInProgress'].splice(index, 1);
var index = draft['uploadsInProgress'].indexOf(clientId);
if (index !== -1) {
draft['uploadsInProgress'].splice(index, 1);
}
PostStore.storeDraft(this.state.channelId, draft);
this.setState({uploadsInProgress: draft['uploadsInProgress'], serverError: err});
} else {
this.setState({serverError: err});
}
PostStore.storeDraft(this.state.channelId, draft);
this.setState({uploadsInProgress: draft['uploadsInProgress'], serverError: err});
},
removePreview: function(id) {
var previews = this.state.previews;
@@ -262,7 +266,9 @@ module.exports = React.createClass({
getFileCount={this.getFileCount}
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError} />
onUploadError={this.handleUploadError}
postType='post'
channelId='' />
</div>
<div className={postFooterClassName}>
{postError}

Просмотреть файл

@@ -12,7 +12,9 @@ module.exports = React.createClass({
onUploadError: React.PropTypes.func,
getFileCount: React.PropTypes.func,
onFileUpload: React.PropTypes.func,
onUploadStart: React.PropTypes.func
onUploadStart: React.PropTypes.func,
channelId: React.PropTypes.string,
postType: React.PropTypes.string
},
getInitialState: function() {
return {requests: {}};
@@ -21,7 +23,7 @@ module.exports = React.createClass({
var element = $(this.refs.fileInput.getDOMNode());
var files = element.prop('files');
var channelId = ChannelStore.getCurrentId();
var channelId = this.props.channelId || ChannelStore.getCurrentId();
this.props.onUploadError(null);
@@ -61,8 +63,8 @@ module.exports = React.createClass({
this.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId);
var requests = this.state.requests;
for (var i = 0; i < parsedData.client_ids.length; i++) {
delete requests[parsedData.client_ids[i]];
for (var j = 0; j < parsedData.client_ids.length; j++) {
delete requests[parsedData.client_ids[j]];
}
this.setState({requests: requests});
}.bind(this),
@@ -87,10 +89,94 @@ module.exports = React.createClass({
}
} catch(e) {}
},
handleDrop: function(e) {
this.props.onUploadError(null);
var files = e.originalEvent.dataTransfer.files;
var channelId = this.props.channelId || ChannelStore.getCurrentId();
if (typeof files !== 'string' && files.length) {
var numFiles = files.length;
var numToUpload = Math.min(Constants.MAX_UPLOAD_FILES - this.props.getFileCount(channelId), numFiles);
if (numFiles > numToUpload) {
this.props.onUploadError('Uploads limited to ' + Constants.MAX_UPLOAD_FILES + ' files maximum. Please use additional posts for more files.');
}
for (var i = 0; i < files.length && i < numToUpload; i++) {
if (files[i].size > Constants.MAX_FILE_SIZE) {
this.props.onUploadError('Files must be no more than ' + Constants.MAX_FILE_SIZE / 1000000 + ' MB');
continue;
}
// generate a unique id that can be used by other components to refer back to this file upload
var clientId = utils.generateId();
// Prepare data to be uploaded.
var formData = new FormData();
formData.append('channel_id', channelId);
formData.append('files', files[i], files[i].name);
formData.append('client_ids', clientId);
var request = client.uploadFile(formData,
function(data) {
var parsedData = $.parseJSON(data);
this.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId);
var requests = this.state.requests;
for (var j = 0; j < parsedData.client_ids.length; j++) {
delete requests[parsedData.client_ids[j]];
}
this.setState({requests: requests});
}.bind(this),
function(err) {
this.props.onUploadError(err, clientId);
}.bind(this)
);
var requests = this.state.requests;
requests[clientId] = request;
this.setState({requests: requests});
this.props.onUploadStart([clientId], channelId);
}
} else {
this.props.onUploadError('Invalid file upload', -1);
}
},
componentDidMount: function() {
var inputDiv = this.refs.input.getDOMNode();
var self = this;
if (this.props.postType === 'post') {
$('.row.main').dragster({
enter: function() {
$('.center-file-overlay').removeClass('hidden');
},
leave: function() {
$('.center-file-overlay').addClass('hidden');
},
drop: function(dragsterEvent, e) {
$('.center-file-overlay').addClass('hidden');
self.handleDrop(e);
}
});
} else if (this.props.postType === 'comment') {
$('.post-right__container').dragster({
enter: function() {
$('.right-file-overlay').removeClass('hidden');
},
leave: function() {
$('.right-file-overlay').addClass('hidden');
},
drop: function(dragsterEvent, e) {
$('.right-file-overlay').addClass('hidden');
self.handleDrop(e);
}
});
}
document.addEventListener('paste', function(e) {
var textarea = $(inputDiv.parentNode.parentNode).find('.custom-textarea')[0];
@@ -133,14 +219,13 @@ module.exports = React.createClass({
continue;
}
var channelId = ChannelStore.getCurrentId();
var channelId = this.props.channelId || ChannelStore.getCurrentId();
// generate a unique id that can be used by other components to refer back to this file upload
var clientId = utils.generateId();
var formData = new FormData();
formData.append('channel_id', channelId);
var d = new Date();
var hour;
if (d.getHours() < 10) {
@@ -165,8 +250,8 @@ module.exports = React.createClass({
self.props.onFileUpload(parsedData.filenames, parsedData.client_ids, channelId);
var requests = self.state.requests;
for (var i = 0; i < parsedData.client_ids.length; i++) {
delete requests[parsedData.client_ids[i]];
for (var j = 0; j < parsedData.client_ids.length; j++) {
delete requests[parsedData.client_ids[j]];
}
self.setState({requests: requests});
},

Просмотреть файл

@@ -0,0 +1,26 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
module.exports = React.createClass({
displayName: 'FileUploadOverlay',
propTypes: {
overlayType: React.PropTypes.string
},
render: function() {
var overlayClass = 'file-overlay hidden';
if (this.props.overlayType === 'right') {
overlayClass += ' right-file-overlay';
} else if (this.props.overlayType === 'center') {
overlayClass += ' center-file-overlay';
}
return (
<div className={overlayClass}>
<div>
<i className='fa fa-upload'></i>
<span>Drop a file to upload it.</span>
</div>
</div>
);
}
});

Просмотреть файл

@@ -11,6 +11,7 @@ var SearchBox =require('./search_bar.jsx');
var CreateComment = require( './create_comment.jsx' );
var Constants = require('../utils/constants.jsx');
var FileAttachmentList = require('./file_attachment_list.jsx');
var FileUploadOverlay = require('./file_upload_overlay.jsx');
var ActionTypes = Constants.ActionTypes;
RhsHeaderPost = React.createClass({
@@ -296,6 +297,8 @@ module.exports = React.createClass({
return (
<div className="post-right__container">
<FileUploadOverlay
overlayType='right' />
<div className="search-bar__container sidebar--right__search-header">{searchForm}</div>
<div className="sidebar-right__body">
<RhsHeaderPost fromSearch={this.props.fromSearch} isMentionSearch={this.props.isMentionSearch} />

Просмотреть файл

@@ -34,6 +34,7 @@ var ChannelInfoModal = require('../components/channel_info_modal.jsx');
var AccessHistoryModal = require('../components/access_history_modal.jsx');
var ActivityLogModal = require('../components/activity_log_modal.jsx');
var RemovedFromChannelModal = require('../components/removed_from_channel_modal.jsx')
var FileUploadOverlay = require('../components/file_upload_overlay.jsx');
var AsyncClient = require('../utils/async_client.jsx');
@@ -224,4 +225,10 @@ global.window.setup_channel_page = function(team_name, team_type, team_id, chann
document.getElementById('removed_from_channel_modal')
);
React.render(
<FileUploadOverlay
overlayType='center' />,
document.getElementById('file_upload_overlay')
);
};

Просмотреть файл

@@ -24,6 +24,7 @@ body {
height: 100%;
> .row.main {
height: 100%;
position: relative;
}
}
> .container-fluid {

Просмотреть файл

@@ -193,11 +193,12 @@
border-right: 1px solid #ddd;
vertical-align: center;
// helper to center the image icon in the preview window
.file-details__preview-helper {
height: 100%;
display: inline-block;
vertical-align: middle;
}
}
// helper to center the image icon in the preview window
.file-details__preview-helper {
height: 100%;
display: inline-block;
vertical-align: middle;
}
}
}

Просмотреть файл

@@ -106,6 +106,32 @@ body.ios {
}
}
.file-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
text-align: center;
color: #FFF;
display: table;
font-size: 1.7em;
font-weight: 600;
z-index: 6;
> div {
display: table-cell;
vertical-align: middle;
}
.fa {
display: block;
font-size: 2em;
margin: 0 0 0.3em;
}
}
#post-list {
.post-list-holder-by-time {
background: #fff;

Просмотреть файл

@@ -189,6 +189,9 @@
}
@media screen and (max-width: 960px) {
.center-file-overlay {
font-size: 1.5em;
}
.post {
.post-header .post-header-col.post-header__reply {
.comment-icon__container__hide {
@@ -240,6 +243,9 @@
}
}
@media screen and (max-width: 768px) {
.center-file-overlay {
font-size: 1.3em;
}
.date-separator, .new-separator {
&.hovered--after {
&:before {

Просмотреть файл

@@ -6,6 +6,7 @@
border-right: $border-gray;
padding: 0 0 2em 0;
background: #fafafa;
z-index: 5;
&.sidebar--padded {
padding-top: 44px;
}

21
web/static/js/jquery-dragster/LICENSE Обычный файл
Просмотреть файл

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Jan Martin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

17
web/static/js/jquery-dragster/README.md Обычный файл
Просмотреть файл

@@ -0,0 +1,17 @@
Include [jquery.dragster.js](https://rawgithub.com/catmanjan/jquery-dragster/master/jquery.dragster.js) in page.
Works in IE.
```javascript
$('.element').dragster({
enter: function (dragsterEvent, event) {
$(this).addClass('hover');
},
leave: function (dragsterEvent, event) {
$(this).removeClass('hover');
},
drop: function (dragsterEvent, event) {
$(this).removeClass('hover');
}
});
```

Просмотреть файл

@@ -0,0 +1,85 @@
// 1.0.3
/*
The MIT License (MIT)
Copyright (c) 2015 Jan Martin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
(function ($) {
$.fn.dragster = function (options) {
var settings = $.extend({
enter: $.noop,
leave: $.noop,
over: $.noop,
drop: $.noop
}, options);
return this.each(function () {
var first = false,
second = false,
$this = $(this);
$this.on({
dragenter: function (event) {
if (first) {
second = true;
return;
} else {
first = true;
$this.trigger('dragster:enter', event);
}
event.preventDefault();
},
dragleave: function (event) {
if (second) {
second = false;
} else if (first) {
first = false;
}
if (!first && !second) {
$this.trigger('dragster:leave', event);
}
event.preventDefault();
},
dragover: function (event) {
$this.trigger('dragster:over', event);
event.preventDefault();
},
drop: function (event) {
if (second) {
second = false;
} else if (first) {
first = false;
}
if (!first && !second) {
$this.trigger('dragster:drop', event);
}
event.preventDefault();
},
'dragster:enter': settings.enter,
'dragster:leave': settings.leave,
'dragster:over': settings.over,
'dragster:drop': settings.drop
});
});
};
}(jQuery));

Просмотреть файл

@@ -14,6 +14,7 @@
<div id="navbar"></div>
</div>
<div class="row main">
<div id="file_upload_overlay"></div>
<div id="app-content" class="app__content">
<div id="channel-header"></div>
<div id="post-list"></div>

Просмотреть файл

@@ -32,6 +32,8 @@
<script src="/static/js/perfect-scrollbar-0.6.3.jquery.js"></script>
<script src="/static/js/jquery-dragster/jquery.dragster.js"></script>
<script type="text/javascript" src="https://www.google.com/jsapi?autoload={'modules':[{'name':'visualization','version':'1','packages':['annotationchart']}]}"></script>
<script type="text/javascript" src="https://cloudfront.loggly.com/js/loggly.tracker.js" async></script>