Show upload status, success and failures for drag-and-drop files in notifications
Summary: Currently, we do a poor job of communicating drag-and-drop upload errors. Show progress and success/failure in notifications.
Test Plan:
{F20671}
{F20672}
Uploaded files to maniphest attachments, remarkup text areas, Files tool.
Reviewers: btrahan, vrana
Reviewed By: vrana
CC: aran
Differential Revision: https://secure.phabricator.com/D3655
This commit is contained in:
@@ -38,3 +38,13 @@
|
||||
background: #ffa0ff;
|
||||
border: 1px solid #aa60aa;
|
||||
}
|
||||
|
||||
.jx-notification-done {
|
||||
background: #d0ffd0;
|
||||
border: 1px solid #60aa60;
|
||||
}
|
||||
|
||||
.jx-notification-error {
|
||||
background: #ffd0d0;
|
||||
border: 1px solid #aa6060;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* javelin-request
|
||||
* javelin-dom
|
||||
* javelin-uri
|
||||
* phabricator-file-upload
|
||||
* @provides phabricator-drag-and-drop-file-upload
|
||||
* @javelin
|
||||
*/
|
||||
@@ -14,7 +15,7 @@ JX.install('PhabricatorDragAndDropFileUpload', {
|
||||
this._node = node;
|
||||
},
|
||||
|
||||
events : ['willUpload', 'didUpload'],
|
||||
events : ['willUpload', 'progress', 'didUpload', 'didError'],
|
||||
|
||||
statics : {
|
||||
isSupported : function() {
|
||||
@@ -88,24 +89,80 @@ JX.install('PhabricatorDragAndDropFileUpload', {
|
||||
|
||||
var files = e.getRawEvent().dataTransfer.files;
|
||||
for (var ii = 0; ii < files.length; ii++) {
|
||||
var file = files[ii];
|
||||
|
||||
this.invoke('willUpload', file);
|
||||
|
||||
var up_uri = JX.$U(this.getURI())
|
||||
.setQueryParam('name', file.name)
|
||||
.toString();
|
||||
|
||||
new JX.Request(up_uri, JX.bind(this, function(r) {
|
||||
this.invoke('didUpload', r);
|
||||
}))
|
||||
.setFile(file)
|
||||
.send();
|
||||
this._sendRequest(files[ii]);
|
||||
}
|
||||
|
||||
// Force depth to 0.
|
||||
this._updateDepth(-this._depth);
|
||||
}));
|
||||
},
|
||||
_sendRequest : function(spec) {
|
||||
var file = new JX.PhabricatorFileUpload()
|
||||
.setName(spec.name)
|
||||
.setTotalBytes(spec.size)
|
||||
.setStatus('uploading')
|
||||
.update();
|
||||
|
||||
this.invoke('willUpload', file);
|
||||
|
||||
var up_uri = JX.$U(this.getURI())
|
||||
.setQueryParam('name', file.getName())
|
||||
.toString();
|
||||
|
||||
var onupload = JX.bind(this, function(r) {
|
||||
if (r.error) {
|
||||
file
|
||||
.setStatus('error')
|
||||
.setError(r.error)
|
||||
.update();
|
||||
|
||||
this.invoke('didError', file);
|
||||
} else {
|
||||
file
|
||||
.setID(r.id)
|
||||
.setPHID(r.phid)
|
||||
.setURI(r.uri)
|
||||
.setMarkup(r.html)
|
||||
.setStatus('done')
|
||||
.update();
|
||||
|
||||
this.invoke('didUpload', file);
|
||||
}
|
||||
});
|
||||
|
||||
var req = new JX.Request(up_uri, onupload);
|
||||
|
||||
var onerror = JX.bind(this, function(error) {
|
||||
file.setStatus('error');
|
||||
|
||||
if (error) {
|
||||
file.setError(error.code + ': ' + error.info);
|
||||
} else {
|
||||
var xhr = req.getTransport();
|
||||
if (xhr.responseText) {
|
||||
file.setError('Server responded: ' + xhr.responseText);
|
||||
}
|
||||
}
|
||||
|
||||
file.update();
|
||||
this.invoke('didError', file);
|
||||
});
|
||||
|
||||
var onprogress = JX.bind(this, function(progress) {
|
||||
file
|
||||
.setTotalBytes(progress.total)
|
||||
.setUploadedBytes(progress.loaded)
|
||||
.update();
|
||||
|
||||
this.invoke('progress', file);
|
||||
});
|
||||
|
||||
req.listen('error', onerror);
|
||||
req.listen('uploadprogress', onprogress);
|
||||
|
||||
req
|
||||
.setFile(spec)
|
||||
.send();
|
||||
}
|
||||
},
|
||||
properties: {
|
||||
|
||||
111
webroot/rsrc/js/application/core/FileUpload.js
Normal file
111
webroot/rsrc/js/application/core/FileUpload.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* @requires javelin-install
|
||||
* @provides phabricator-file-upload
|
||||
* @javelin
|
||||
*/
|
||||
|
||||
JX.install('PhabricatorFileUpload', {
|
||||
|
||||
construct : function() {
|
||||
this._notification = new JX.Notification();
|
||||
},
|
||||
|
||||
properties : {
|
||||
name : null,
|
||||
totalBytes : null,
|
||||
uploadedBytes : null,
|
||||
ID : null,
|
||||
PHID : null,
|
||||
URI : null,
|
||||
status : null,
|
||||
markup : null,
|
||||
error : null
|
||||
},
|
||||
|
||||
members : {
|
||||
_notification : null,
|
||||
|
||||
update : function() {
|
||||
if (!this._notification) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._notification
|
||||
.setDuration(0)
|
||||
.show();
|
||||
|
||||
switch (this.getStatus()) {
|
||||
case 'done':
|
||||
var link = JX.$N('a', {href: this.getURI()}, 'F' + this.getID());
|
||||
|
||||
var content = [
|
||||
JX.$N('strong', {}, ['Upload Complete (', link, ')']),
|
||||
JX.$N('br'),
|
||||
this.getName()
|
||||
];
|
||||
|
||||
this._notification
|
||||
.setContent(content)
|
||||
.alterClassName('jx-notification-done', true)
|
||||
.setDuration(12000);
|
||||
this._notification = null;
|
||||
break;
|
||||
case 'error':
|
||||
var content = [
|
||||
JX.$N('strong', {}, 'Upload Failure'),
|
||||
JX.$N('br'),
|
||||
this.getName(),
|
||||
JX.$N('br'),
|
||||
JX.$N('br'),
|
||||
this.getError()
|
||||
];
|
||||
|
||||
this._notification
|
||||
.setContent(content)
|
||||
.alterClassName('jx-notification-error', true);
|
||||
this._notification = null;
|
||||
break;
|
||||
default:
|
||||
var info = '';
|
||||
if (this.getTotalBytes()) {
|
||||
var p = this._renderPercentComplete();
|
||||
var f = this._renderFileSize();
|
||||
info = ' (' + p + ' of ' + f + ')';
|
||||
}
|
||||
|
||||
info = 'Uploading "' + this.getName() + '"' + info + '...';
|
||||
|
||||
this._notification
|
||||
.setContent(info);
|
||||
break;
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
_renderPercentComplete : function() {
|
||||
if (!this.getTotalBytes()) {
|
||||
return null;
|
||||
}
|
||||
var ratio = this.getUploadedBytes() / this.getTotalBytes();
|
||||
return parseInt(100 * ratio) + '%';
|
||||
},
|
||||
_renderFileSize : function() {
|
||||
if (!this.getTotalBytes()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var s = 3;
|
||||
var n = this.getTotalBytes();
|
||||
while (s && n >= 1000) {
|
||||
n = Math.round(n / 100);
|
||||
n = n / 10;
|
||||
s--;
|
||||
}
|
||||
|
||||
s = ['GB', 'MB', 'KB', 'bytes'][s];
|
||||
return n + ' ' + s;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ JX.behavior('aphront-drag-and-drop-textarea', function(config) {
|
||||
var target = JX.$(config.target);
|
||||
|
||||
function onupload(f) {
|
||||
JX.TextAreaUtils.setSelectionText(target, '{F' + f.id + '}');
|
||||
JX.TextAreaUtils.setSelectionText(target, '{F' + f.getID() + '}');
|
||||
}
|
||||
|
||||
if (JX.PhabricatorDragAndDropFileUpload.isSupported()) {
|
||||
|
||||
@@ -32,7 +32,7 @@ JX.behavior('aphront-drag-and-drop', function(config) {
|
||||
});
|
||||
|
||||
drop.listen('didUpload', function(f) {
|
||||
files[f.phid] = f;
|
||||
files[f.getPHID()] = f;
|
||||
|
||||
// This redraws "Upload complete!"
|
||||
pending--;
|
||||
@@ -59,13 +59,13 @@ JX.behavior('aphront-drag-and-drop', function(config) {
|
||||
var items = [];
|
||||
for (var k in files) {
|
||||
var file = files[k];
|
||||
items.push(JX.$N('div', {}, JX.$H(file.html)));
|
||||
items.push(JX.$N('div', {}, JX.$H(file.getMarkup())));
|
||||
items.push(JX.$N(
|
||||
'input',
|
||||
{
|
||||
type: "hidden",
|
||||
name: config.name + "[" + file.phid + "]",
|
||||
value: file.phid
|
||||
name: config.name + "[" + file.getPHID() + "]",
|
||||
value: file.getPHID()
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ JX.behavior('files-drag-and-drop', function(config) {
|
||||
|
||||
var pending = 0;
|
||||
var files = [];
|
||||
var errors = false;
|
||||
|
||||
var control = JX.$(config.control);
|
||||
// Show the control, since we have browser support.
|
||||
@@ -34,14 +35,14 @@ JX.behavior('files-drag-and-drop', function(config) {
|
||||
files.push(f);
|
||||
|
||||
pending--;
|
||||
if (pending == 0) {
|
||||
if (pending == 0 && !errors) {
|
||||
// If whatever the user dropped in has finished uploading, send them to
|
||||
// their uploads.
|
||||
var uri;
|
||||
uri = JX.$U(config.browseURI);
|
||||
var ids = [];
|
||||
for (var ii = 0; ii < files.length; ii++) {
|
||||
ids.push(files[ii].id);
|
||||
ids.push(files[ii].getID());
|
||||
}
|
||||
uri.setQueryParam('h', ids.join('-'));
|
||||
|
||||
@@ -56,6 +57,12 @@ JX.behavior('files-drag-and-drop', function(config) {
|
||||
}
|
||||
});
|
||||
|
||||
drop.listen('didError', function(f) {
|
||||
pending--;
|
||||
errors = true;
|
||||
redraw();
|
||||
});
|
||||
|
||||
drop.start();
|
||||
redraw();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user