@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+267 -71
+61 -50
src/__celerity_resource_map__.php
··· 916 916 ), 917 917 'javelin-behavior-aphront-drag-and-drop' => 918 918 array( 919 - 'uri' => '/res/724c922b/rsrc/js/application/core/behavior-drag-and-drop.js', 919 + 'uri' => '/res/0910fc0a/rsrc/js/application/core/behavior-drag-and-drop.js', 920 920 'type' => 'js', 921 921 'requires' => 922 922 array( ··· 929 929 ), 930 930 'javelin-behavior-aphront-drag-and-drop-textarea' => 931 931 array( 932 - 'uri' => '/res/99f58821/rsrc/js/application/core/behavior-drag-and-drop-textarea.js', 932 + 'uri' => '/res/ad737ce4/rsrc/js/application/core/behavior-drag-and-drop-textarea.js', 933 933 'type' => 'js', 934 934 'requires' => 935 935 array( ··· 1301 1301 ), 1302 1302 'javelin-behavior-files-drag-and-drop' => 1303 1303 array( 1304 - 'uri' => '/res/3a7a2a8a/rsrc/js/application/core/behavior-files-drag-and-drop.js', 1304 + 'uri' => '/res/4893f577/rsrc/js/application/core/behavior-files-drag-and-drop.js', 1305 1305 'type' => 'js', 1306 1306 'requires' => 1307 1307 array( ··· 2361 2361 ), 2362 2362 'phabricator-drag-and-drop-file-upload' => 2363 2363 array( 2364 - 'uri' => '/res/63a06ad9/rsrc/js/application/core/DragAndDropFileUpload.js', 2364 + 'uri' => '/res/0db5b9d5/rsrc/js/application/core/DragAndDropFileUpload.js', 2365 2365 'type' => 'js', 2366 2366 'requires' => 2367 2367 array( ··· 2370 2370 2 => 'javelin-request', 2371 2371 3 => 'javelin-dom', 2372 2372 4 => 'javelin-uri', 2373 + 5 => 'phabricator-file-upload', 2373 2374 ), 2374 2375 'disk' => '/rsrc/js/application/core/DragAndDropFileUpload.js', 2375 2376 ), ··· 2397 2398 ), 2398 2399 'disk' => '/rsrc/css/application/feed/feed.css', 2399 2400 ), 2401 + 'phabricator-file-upload' => 2402 + array( 2403 + 'uri' => '/res/98503231/rsrc/js/application/core/FileUpload.js', 2404 + 'type' => 'js', 2405 + 'requires' => 2406 + array( 2407 + 0 => 'javelin-install', 2408 + ), 2409 + 'disk' => '/rsrc/js/application/core/FileUpload.js', 2410 + ), 2400 2411 'phabricator-filetree-view-css' => 2401 2412 array( 2402 2413 'uri' => '/res/be0ab498/rsrc/css/layout/phabricator-filetree-view.css', ··· 2512 2523 ), 2513 2524 'phabricator-notification-css' => 2514 2525 array( 2515 - 'uri' => '/res/77e8c821/rsrc/css/aphront/notification.css', 2526 + 'uri' => '/res/91197237/rsrc/css/aphront/notification.css', 2516 2527 'type' => 'css', 2517 2528 'requires' => 2518 2529 array( ··· 2636 2647 ), 2637 2648 'phabricator-remarkup-css' => 2638 2649 array( 2639 - 'uri' => '/res/66b4cd42/rsrc/css/core/remarkup.css', 2650 + 'uri' => '/res/2e0d0042/rsrc/css/core/remarkup.css', 2640 2651 'type' => 'css', 2641 2652 'requires' => 2642 2653 array( ··· 3003 3014 ), array( 3004 3015 'packages' => 3005 3016 array( 3006 - 'c88632b0' => 3017 + '315e8536' => 3007 3018 array( 3008 3019 'name' => 'core.pkg.css', 3009 3020 'symbols' => ··· 3032 3043 21 => 'phabricator-flag-css', 3033 3044 22 => 'aphront-error-view-css', 3034 3045 ), 3035 - 'uri' => '/res/pkg/c88632b0/core.pkg.css', 3046 + 'uri' => '/res/pkg/315e8536/core.pkg.css', 3036 3047 'type' => 'css', 3037 3048 ), 3038 3049 '3a455e4f' => ··· 3084 3095 'uri' => '/res/pkg/2ba14b3d/differential.pkg.css', 3085 3096 'type' => 'css', 3086 3097 ), 3087 - 'a5cb9310' => 3098 + 'a569df32' => 3088 3099 array( 3089 3100 'name' => 'differential.pkg.js', 3090 3101 'symbols' => ··· 3107 3118 15 => 'differential-inline-comment-editor', 3108 3119 16 => 'javelin-behavior-differential-dropdown-menus', 3109 3120 ), 3110 - 'uri' => '/res/pkg/a5cb9310/differential.pkg.js', 3121 + 'uri' => '/res/pkg/a569df32/differential.pkg.js', 3111 3122 'type' => 'js', 3112 3123 ), 3113 3124 'c8ce2d88' => ··· 3199 3210 'reverse' => 3200 3211 array( 3201 3212 'aphront-attached-file-view-css' => '7839ae2d', 3202 - 'aphront-crumbs-view-css' => 'c88632b0', 3203 - 'aphront-dialog-view-css' => 'c88632b0', 3204 - 'aphront-error-view-css' => 'c88632b0', 3205 - 'aphront-form-view-css' => 'c88632b0', 3213 + 'aphront-crumbs-view-css' => '315e8536', 3214 + 'aphront-dialog-view-css' => '315e8536', 3215 + 'aphront-error-view-css' => '315e8536', 3216 + 'aphront-form-view-css' => '315e8536', 3206 3217 'aphront-headsup-action-list-view-css' => '2ba14b3d', 3207 - 'aphront-headsup-view-css' => 'c88632b0', 3208 - 'aphront-list-filter-view-css' => 'c88632b0', 3209 - 'aphront-pager-view-css' => 'c88632b0', 3210 - 'aphront-panel-view-css' => 'c88632b0', 3211 - 'aphront-side-nav-view-css' => 'c88632b0', 3212 - 'aphront-table-view-css' => 'c88632b0', 3213 - 'aphront-tokenizer-control-css' => 'c88632b0', 3214 - 'aphront-tooltip-css' => 'c88632b0', 3215 - 'aphront-typeahead-control-css' => 'c88632b0', 3218 + 'aphront-headsup-view-css' => '315e8536', 3219 + 'aphront-list-filter-view-css' => '315e8536', 3220 + 'aphront-pager-view-css' => '315e8536', 3221 + 'aphront-panel-view-css' => '315e8536', 3222 + 'aphront-side-nav-view-css' => '315e8536', 3223 + 'aphront-table-view-css' => '315e8536', 3224 + 'aphront-tokenizer-control-css' => '315e8536', 3225 + 'aphront-tooltip-css' => '315e8536', 3226 + 'aphront-typeahead-control-css' => '315e8536', 3216 3227 'differential-changeset-view-css' => '2ba14b3d', 3217 3228 'differential-core-view-css' => '2ba14b3d', 3218 - 'differential-inline-comment-editor' => 'a5cb9310', 3229 + 'differential-inline-comment-editor' => 'a569df32', 3219 3230 'differential-local-commits-view-css' => '2ba14b3d', 3220 3231 'differential-results-table-css' => '2ba14b3d', 3221 3232 'differential-revision-add-comment-css' => '2ba14b3d', ··· 3229 3240 'inline-comment-summary-css' => '2ba14b3d', 3230 3241 'javelin-behavior' => '6c45a1d8', 3231 3242 'javelin-behavior-aphront-basic-tokenizer' => '81c9cd69', 3232 - 'javelin-behavior-aphront-drag-and-drop' => 'a5cb9310', 3233 - 'javelin-behavior-aphront-drag-and-drop-textarea' => 'a5cb9310', 3243 + 'javelin-behavior-aphront-drag-and-drop' => 'a569df32', 3244 + 'javelin-behavior-aphront-drag-and-drop-textarea' => 'a569df32', 3234 3245 'javelin-behavior-aphront-form-disable-on-submit' => '3a455e4f', 3235 3246 'javelin-behavior-audit-preview' => '5e68be89', 3236 - 'javelin-behavior-differential-accept-with-errors' => 'a5cb9310', 3237 - 'javelin-behavior-differential-add-reviewers-and-ccs' => 'a5cb9310', 3238 - 'javelin-behavior-differential-comment-jump' => 'a5cb9310', 3239 - 'javelin-behavior-differential-diff-radios' => 'a5cb9310', 3240 - 'javelin-behavior-differential-dropdown-menus' => 'a5cb9310', 3241 - 'javelin-behavior-differential-edit-inline-comments' => 'a5cb9310', 3242 - 'javelin-behavior-differential-feedback-preview' => 'a5cb9310', 3243 - 'javelin-behavior-differential-keyboard-navigation' => 'a5cb9310', 3244 - 'javelin-behavior-differential-populate' => 'a5cb9310', 3245 - 'javelin-behavior-differential-show-more' => 'a5cb9310', 3247 + 'javelin-behavior-differential-accept-with-errors' => 'a569df32', 3248 + 'javelin-behavior-differential-add-reviewers-and-ccs' => 'a569df32', 3249 + 'javelin-behavior-differential-comment-jump' => 'a569df32', 3250 + 'javelin-behavior-differential-diff-radios' => 'a569df32', 3251 + 'javelin-behavior-differential-dropdown-menus' => 'a569df32', 3252 + 'javelin-behavior-differential-edit-inline-comments' => 'a569df32', 3253 + 'javelin-behavior-differential-feedback-preview' => 'a569df32', 3254 + 'javelin-behavior-differential-keyboard-navigation' => 'a569df32', 3255 + 'javelin-behavior-differential-populate' => 'a569df32', 3256 + 'javelin-behavior-differential-show-more' => 'a569df32', 3246 3257 'javelin-behavior-diffusion-commit-graph' => '5e68be89', 3247 3258 'javelin-behavior-diffusion-pull-lastmodified' => '5e68be89', 3248 3259 'javelin-behavior-maniphest-batch-selector' => '7707de41', ··· 3252 3263 'javelin-behavior-maniphest-transaction-preview' => '7707de41', 3253 3264 'javelin-behavior-phabricator-autofocus' => '3a455e4f', 3254 3265 'javelin-behavior-phabricator-keyboard-shortcuts' => '3a455e4f', 3255 - 'javelin-behavior-phabricator-object-selector' => 'a5cb9310', 3266 + 'javelin-behavior-phabricator-object-selector' => 'a569df32', 3256 3267 'javelin-behavior-phabricator-oncopy' => '3a455e4f', 3257 3268 'javelin-behavior-phabricator-tooltips' => '3a455e4f', 3258 3269 'javelin-behavior-phabricator-watch-anchor' => '3a455e4f', 3259 3270 'javelin-behavior-refresh-csrf' => '3a455e4f', 3260 - 'javelin-behavior-repository-crossreference' => 'a5cb9310', 3271 + 'javelin-behavior-repository-crossreference' => 'a569df32', 3261 3272 'javelin-behavior-workflow' => '3a455e4f', 3262 3273 'javelin-dom' => '6c45a1d8', 3263 3274 'javelin-event' => '6c45a1d8', ··· 3278 3289 'javelin-workflow' => '3a455e4f', 3279 3290 'maniphest-task-summary-css' => '7839ae2d', 3280 3291 'maniphest-transaction-detail-css' => '7839ae2d', 3281 - 'phabricator-app-buttons-css' => 'c88632b0', 3292 + 'phabricator-app-buttons-css' => '315e8536', 3282 3293 'phabricator-content-source-view-css' => '2ba14b3d', 3283 - 'phabricator-core-buttons-css' => 'c88632b0', 3284 - 'phabricator-core-css' => 'c88632b0', 3285 - 'phabricator-directory-css' => 'c88632b0', 3286 - 'phabricator-drag-and-drop-file-upload' => 'a5cb9310', 3294 + 'phabricator-core-buttons-css' => '315e8536', 3295 + 'phabricator-core-css' => '315e8536', 3296 + 'phabricator-directory-css' => '315e8536', 3297 + 'phabricator-drag-and-drop-file-upload' => 'a569df32', 3287 3298 'phabricator-dropdown-menu' => '3a455e4f', 3288 - 'phabricator-flag-css' => 'c88632b0', 3289 - 'phabricator-jump-nav' => 'c88632b0', 3299 + 'phabricator-flag-css' => '315e8536', 3300 + 'phabricator-jump-nav' => '315e8536', 3290 3301 'phabricator-keyboard-shortcut' => '3a455e4f', 3291 3302 'phabricator-keyboard-shortcut-manager' => '3a455e4f', 3292 3303 'phabricator-menu-item' => '3a455e4f', ··· 3294 3305 'phabricator-paste-file-upload' => '3a455e4f', 3295 3306 'phabricator-prefab' => '3a455e4f', 3296 3307 'phabricator-project-tag-css' => '7839ae2d', 3297 - 'phabricator-remarkup-css' => 'c88632b0', 3298 - 'phabricator-shaped-request' => 'a5cb9310', 3299 - 'phabricator-standard-page-view' => 'c88632b0', 3308 + 'phabricator-remarkup-css' => '315e8536', 3309 + 'phabricator-shaped-request' => 'a569df32', 3310 + 'phabricator-standard-page-view' => '315e8536', 3300 3311 'phabricator-tooltip' => '3a455e4f', 3301 - 'phabricator-transaction-view-css' => 'c88632b0', 3302 - 'syntax-highlighting-css' => 'c88632b0', 3312 + 'phabricator-transaction-view-css' => '315e8536', 3313 + 'syntax-highlighting-css' => '315e8536', 3303 3314 ), 3304 3315 ));
+10
webroot/rsrc/css/aphront/notification.css
··· 38 38 background: #ffa0ff; 39 39 border: 1px solid #aa60aa; 40 40 } 41 + 42 + .jx-notification-done { 43 + background: #d0ffd0; 44 + border: 1px solid #60aa60; 45 + } 46 + 47 + .jx-notification-error { 48 + background: #ffd0d0; 49 + border: 1px solid #aa6060; 50 + }
+71 -14
webroot/rsrc/js/application/core/DragAndDropFileUpload.js
··· 4 4 * javelin-request 5 5 * javelin-dom 6 6 * javelin-uri 7 + * phabricator-file-upload 7 8 * @provides phabricator-drag-and-drop-file-upload 8 9 * @javelin 9 10 */ ··· 14 15 this._node = node; 15 16 }, 16 17 17 - events : ['willUpload', 'didUpload'], 18 + events : ['willUpload', 'progress', 'didUpload', 'didError'], 18 19 19 20 statics : { 20 21 isSupported : function() { ··· 88 89 89 90 var files = e.getRawEvent().dataTransfer.files; 90 91 for (var ii = 0; ii < files.length; ii++) { 91 - var file = files[ii]; 92 + this._sendRequest(files[ii]); 93 + } 94 + 95 + // Force depth to 0. 96 + this._updateDepth(-this._depth); 97 + })); 98 + }, 99 + _sendRequest : function(spec) { 100 + var file = new JX.PhabricatorFileUpload() 101 + .setName(spec.name) 102 + .setTotalBytes(spec.size) 103 + .setStatus('uploading') 104 + .update(); 105 + 106 + this.invoke('willUpload', file); 107 + 108 + var up_uri = JX.$U(this.getURI()) 109 + .setQueryParam('name', file.getName()) 110 + .toString(); 111 + 112 + var onupload = JX.bind(this, function(r) { 113 + if (r.error) { 114 + file 115 + .setStatus('error') 116 + .setError(r.error) 117 + .update(); 118 + 119 + this.invoke('didError', file); 120 + } else { 121 + file 122 + .setID(r.id) 123 + .setPHID(r.phid) 124 + .setURI(r.uri) 125 + .setMarkup(r.html) 126 + .setStatus('done') 127 + .update(); 128 + 129 + this.invoke('didUpload', file); 130 + } 131 + }); 92 132 93 - this.invoke('willUpload', file); 133 + var req = new JX.Request(up_uri, onupload); 94 134 95 - var up_uri = JX.$U(this.getURI()) 96 - .setQueryParam('name', file.name) 97 - .toString(); 135 + var onerror = JX.bind(this, function(error) { 136 + file.setStatus('error'); 98 137 99 - new JX.Request(up_uri, JX.bind(this, function(r) { 100 - this.invoke('didUpload', r); 101 - })) 102 - .setFile(file) 103 - .send(); 138 + if (error) { 139 + file.setError(error.code + ': ' + error.info); 140 + } else { 141 + var xhr = req.getTransport(); 142 + if (xhr.responseText) { 143 + file.setError('Server responded: ' + xhr.responseText); 104 144 } 145 + } 105 146 106 - // Force depth to 0. 107 - this._updateDepth(-this._depth); 108 - })); 147 + file.update(); 148 + this.invoke('didError', file); 149 + }); 150 + 151 + var onprogress = JX.bind(this, function(progress) { 152 + file 153 + .setTotalBytes(progress.total) 154 + .setUploadedBytes(progress.loaded) 155 + .update(); 156 + 157 + this.invoke('progress', file); 158 + }); 159 + 160 + req.listen('error', onerror); 161 + req.listen('uploadprogress', onprogress); 162 + 163 + req 164 + .setFile(spec) 165 + .send(); 109 166 } 110 167 }, 111 168 properties: {
+111
webroot/rsrc/js/application/core/FileUpload.js
··· 1 + /** 2 + * @requires javelin-install 3 + * @provides phabricator-file-upload 4 + * @javelin 5 + */ 6 + 7 + JX.install('PhabricatorFileUpload', { 8 + 9 + construct : function() { 10 + this._notification = new JX.Notification(); 11 + }, 12 + 13 + properties : { 14 + name : null, 15 + totalBytes : null, 16 + uploadedBytes : null, 17 + ID : null, 18 + PHID : null, 19 + URI : null, 20 + status : null, 21 + markup : null, 22 + error : null 23 + }, 24 + 25 + members : { 26 + _notification : null, 27 + 28 + update : function() { 29 + if (!this._notification) { 30 + return; 31 + } 32 + 33 + this._notification 34 + .setDuration(0) 35 + .show(); 36 + 37 + switch (this.getStatus()) { 38 + case 'done': 39 + var link = JX.$N('a', {href: this.getURI()}, 'F' + this.getID()); 40 + 41 + var content = [ 42 + JX.$N('strong', {}, ['Upload Complete (', link, ')']), 43 + JX.$N('br'), 44 + this.getName() 45 + ]; 46 + 47 + this._notification 48 + .setContent(content) 49 + .alterClassName('jx-notification-done', true) 50 + .setDuration(12000); 51 + this._notification = null; 52 + break; 53 + case 'error': 54 + var content = [ 55 + JX.$N('strong', {}, 'Upload Failure'), 56 + JX.$N('br'), 57 + this.getName(), 58 + JX.$N('br'), 59 + JX.$N('br'), 60 + this.getError() 61 + ]; 62 + 63 + this._notification 64 + .setContent(content) 65 + .alterClassName('jx-notification-error', true); 66 + this._notification = null; 67 + break; 68 + default: 69 + var info = ''; 70 + if (this.getTotalBytes()) { 71 + var p = this._renderPercentComplete(); 72 + var f = this._renderFileSize(); 73 + info = ' (' + p + ' of ' + f + ')'; 74 + } 75 + 76 + info = 'Uploading "' + this.getName() + '"' + info + '...'; 77 + 78 + this._notification 79 + .setContent(info); 80 + break; 81 + } 82 + 83 + return this; 84 + }, 85 + _renderPercentComplete : function() { 86 + if (!this.getTotalBytes()) { 87 + return null; 88 + } 89 + var ratio = this.getUploadedBytes() / this.getTotalBytes(); 90 + return parseInt(100 * ratio) + '%'; 91 + }, 92 + _renderFileSize : function() { 93 + if (!this.getTotalBytes()) { 94 + return null; 95 + } 96 + 97 + var s = 3; 98 + var n = this.getTotalBytes(); 99 + while (s && n >= 1000) { 100 + n = Math.round(n / 100); 101 + n = n / 10; 102 + s--; 103 + } 104 + 105 + s = ['GB', 'MB', 'KB', 'bytes'][s]; 106 + return n + ' ' + s; 107 + } 108 + } 109 + 110 + }); 111 +
+1 -1
webroot/rsrc/js/application/core/behavior-drag-and-drop-textarea.js
··· 12 12 var target = JX.$(config.target); 13 13 14 14 function onupload(f) { 15 - JX.TextAreaUtils.setSelectionText(target, '{F' + f.id + '}'); 15 + JX.TextAreaUtils.setSelectionText(target, '{F' + f.getID() + '}'); 16 16 } 17 17 18 18 if (JX.PhabricatorDragAndDropFileUpload.isSupported()) {
+4 -4
webroot/rsrc/js/application/core/behavior-drag-and-drop.js
··· 32 32 }); 33 33 34 34 drop.listen('didUpload', function(f) { 35 - files[f.phid] = f; 35 + files[f.getPHID()] = f; 36 36 37 37 // This redraws "Upload complete!" 38 38 pending--; ··· 59 59 var items = []; 60 60 for (var k in files) { 61 61 var file = files[k]; 62 - items.push(JX.$N('div', {}, JX.$H(file.html))); 62 + items.push(JX.$N('div', {}, JX.$H(file.getMarkup()))); 63 63 items.push(JX.$N( 64 64 'input', 65 65 { 66 66 type: "hidden", 67 - name: config.name + "[" + file.phid + "]", 68 - value: file.phid 67 + name: config.name + "[" + file.getPHID() + "]", 68 + value: file.getPHID() 69 69 })); 70 70 } 71 71
+9 -2
webroot/rsrc/js/application/core/behavior-files-drag-and-drop.js
··· 16 16 17 17 var pending = 0; 18 18 var files = []; 19 + var errors = false; 19 20 20 21 var control = JX.$(config.control); 21 22 // Show the control, since we have browser support. ··· 34 35 files.push(f); 35 36 36 37 pending--; 37 - if (pending == 0) { 38 + if (pending == 0 && !errors) { 38 39 // If whatever the user dropped in has finished uploading, send them to 39 40 // their uploads. 40 41 var uri; 41 42 uri = JX.$U(config.browseURI); 42 43 var ids = []; 43 44 for (var ii = 0; ii < files.length; ii++) { 44 - ids.push(files[ii].id); 45 + ids.push(files[ii].getID()); 45 46 } 46 47 uri.setQueryParam('h', ids.join('-')); 47 48 ··· 54 55 } else { 55 56 redraw(); 56 57 } 58 + }); 59 + 60 + drop.listen('didError', function(f) { 61 + pending--; 62 + errors = true; 63 + redraw(); 57 64 }); 58 65 59 66 drop.start();