@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.

Provide an `<input type="file">` control in Remarkup for mobile and users with esoteric windowing systems

Summary:
Ref T5187. This definitely feels a bit flimsy and I'm going to hold it until I cut the release since it changes a couple of things about Workflow in general, but it seems to work OK and most of it is fine.

The intent is described in T5187#176236.

In practice, most of that works like I describe, then the `phui-file-upload` behavior gets some weird glue to figure out if the input is part of the form. Not the most elegant system, but I think it'll hold until we come up with many reasons to write a lot more Javascript.

Test Plan:
Used both drag-and-drop and the upload dialog to upload files in Safari, Firefox and Chrome.

{F1653716}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T5187

Differential Revision: https://secure.phabricator.com/D15953

+287 -38
+2
src/__phutil_library_map__.php
··· 1594 1594 'PHUIFeedStoryExample' => 'applications/uiexample/examples/PHUIFeedStoryExample.php', 1595 1595 'PHUIFeedStoryView' => 'view/phui/PHUIFeedStoryView.php', 1596 1596 'PHUIFormDividerControl' => 'view/form/control/PHUIFormDividerControl.php', 1597 + 'PHUIFormFileControl' => 'view/form/control/PHUIFormFileControl.php', 1597 1598 'PHUIFormFreeformDateControl' => 'view/form/control/PHUIFormFreeformDateControl.php', 1598 1599 'PHUIFormIconSetControl' => 'view/form/control/PHUIFormIconSetControl.php', 1599 1600 'PHUIFormInsetView' => 'view/form/PHUIFormInsetView.php', ··· 5996 5997 'PHUIFeedStoryExample' => 'PhabricatorUIExample', 5997 5998 'PHUIFeedStoryView' => 'AphrontView', 5998 5999 'PHUIFormDividerControl' => 'AphrontFormControl', 6000 + 'PHUIFormFileControl' => 'AphrontFormControl', 5999 6001 'PHUIFormFreeformDateControl' => 'AphrontFormControl', 6000 6002 'PHUIFormIconSetControl' => 'AphrontFormControl', 6001 6003 'PHUIFormInsetView' => 'AphrontView',
+3 -11
src/applications/files/controller/PhabricatorFileDropUploadController.php
··· 56 56 $file_phid = $result['filePHID']; 57 57 if ($file_phid) { 58 58 $file = $this->loadFile($file_phid); 59 - $result += $this->getFileDictionary($file); 59 + $result += $file->getDragAndDropDictionary(); 60 60 } 61 61 62 62 return id(new AphrontAjaxResponse())->setContent($result); ··· 84 84 } else { 85 85 $result = array( 86 86 'complete' => true, 87 - ) + $this->getFileDictionary($file); 87 + ) + $file->getDragAndDropDictionary(); 88 88 } 89 89 90 90 return id(new AphrontAjaxResponse())->setContent($result); ··· 99 99 'isExplicitUpload' => true, 100 100 )); 101 101 102 - $result = $this->getFileDictionary($file); 102 + $result = $file->getDragAndDropDictionary(); 103 103 return id(new AphrontAjaxResponse())->setContent($result); 104 - } 105 - 106 - private function getFileDictionary(PhabricatorFile $file) { 107 - return array( 108 - 'id' => $file->getID(), 109 - 'phid' => $file->getPHID(), 110 - 'uri' => $file->getBestURI(), 111 - ); 112 104 } 113 105 114 106 private function loadFile($file_phid) {
+44 -5
src/applications/files/controller/PhabricatorFileUploadDialogController.php
··· 6 6 public function handleRequest(AphrontRequest $request) { 7 7 $viewer = $request->getViewer(); 8 8 9 - return $this->newDialog() 10 - ->setTitle(pht('Upload File')) 11 - ->appendChild(pht( 12 - 'To add files, drag and drop them into the comment text area.')) 13 - ->addCancelButton('/', pht('Close')); 9 + $e_file = true; 10 + $errors = array(); 11 + if ($request->isDialogFormPost()) { 12 + $file_phids = $request->getStrList('filePHIDs'); 13 + if ($file_phids) { 14 + $files = id(new PhabricatorFileQuery()) 15 + ->setViewer($viewer) 16 + ->withPHIDs($file_phids) 17 + ->setRaisePolicyExceptions(true) 18 + ->execute(); 19 + } else { 20 + $files = array(); 21 + } 22 + 23 + if ($files) { 24 + $results = array(); 25 + foreach ($files as $file) { 26 + $results[] = $file->getDragAndDropDictionary(); 27 + } 28 + 29 + $content = array( 30 + 'files' => $results, 31 + ); 32 + 33 + return id(new AphrontAjaxResponse())->setContent($content); 34 + } else { 35 + $e_file = pht('Required'); 36 + $errors[] = pht('You must choose a file to upload.'); 37 + } 38 + } 14 39 40 + $form = id(new AphrontFormView()) 41 + ->appendChild( 42 + id(new PHUIFormFileControl()) 43 + ->setName('filePHIDs') 44 + ->setLabel(pht('Upload File')) 45 + ->setAllowMultiple(true) 46 + ->setError($e_file)); 47 + 48 + return $this->newDialog() 49 + ->setTitle(pht('File')) 50 + ->setErrors($errors) 51 + ->appendForm($form) 52 + ->addSubmitButton(pht('Upload')) 53 + ->addCancelButton('/'); 15 54 } 16 55 17 56 }
+8
src/applications/files/storage/PhabricatorFile.php
··· 851 851 return $supported; 852 852 } 853 853 854 + public function getDragAndDropDictionary() { 855 + return array( 856 + 'id' => $this->getID(), 857 + 'phid' => $this->getPHID(), 858 + 'uri' => $this->getBestURI(), 859 + ); 860 + } 861 + 854 862 public function instantiateStorageEngine() { 855 863 return self::buildEngine($this->getStorageEngine()); 856 864 }
+44
src/view/form/control/PHUIFormFileControl.php
··· 1 + <?php 2 + 3 + final class PHUIFormFileControl 4 + extends AphrontFormControl { 5 + 6 + private $allowMultiple; 7 + 8 + protected function getCustomControlClass() { 9 + return 'phui-form-file-upload'; 10 + } 11 + 12 + public function setAllowMultiple($allow_multiple) { 13 + $this->allowMultiple = $allow_multiple; 14 + return $this; 15 + } 16 + 17 + public function getAllowMultiple() { 18 + return $this->allowMultiple; 19 + } 20 + 21 + protected function renderInput() { 22 + $file_id = $this->getID(); 23 + 24 + Javelin::initBehavior( 25 + 'phui-file-upload', 26 + array( 27 + 'fileInputID' => $file_id, 28 + 'inputName' => $this->getName(), 29 + 'uploadURI' => '/file/dropupload/', 30 + 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), 31 + )); 32 + 33 + return phutil_tag( 34 + 'input', 35 + array( 36 + 'type' => 'file', 37 + 'multiple' => $this->getAllowMultiple() ? 'multiple' : null, 38 + 'name' => $this->getName().'.raw', 39 + 'id' => $file_id, 40 + 'disabled' => $this->getDisabled() ? 'disabled' : null, 41 + )); 42 + } 43 + 44 + }
+61 -2
webroot/rsrc/externals/javelin/lib/Workflow.js
··· 25 25 this.setData(data || {}); 26 26 }, 27 27 28 - events : ['error', 'finally', 'submit'], 28 + events : ['error', 'finally', 'submit', 'start'], 29 29 30 30 statics : { 31 31 _stack : [], ··· 54 54 } 55 55 56 56 var workflow = new JX.Workflow(form.getAttribute('action'), {}); 57 + 58 + workflow._form = form; 59 + 57 60 workflow.setDataWithListOfPairs(pairs); 58 61 workflow.setMethod(form.getAttribute('method')); 59 62 workflow.listen('finally', function() { ··· 137 140 data.push([button.name, button.value || true]); 138 141 139 142 var active = JX.Workflow._getActiveWorkflow(); 143 + 144 + active._form = form; 145 + 140 146 var e = active.invoke('submit', {form: form, data: data}); 141 147 if (!e.getStopped()) { 142 - active._destroy(); 148 + // NOTE: Don't remove the current dialog yet because additional 149 + // handlers may still want to access the nodes. 150 + 143 151 active 144 152 .setURI(form.getAttribute('action') || active.getURI()) 145 153 .setDataWithListOfPairs(data) ··· 156 164 _root : null, 157 165 _pushed : false, 158 166 _data : null, 167 + 168 + _form: null, 169 + _paused: 0, 170 + _nextCallback: null, 171 + 172 + getSourceForm: function() { 173 + return this._form; 174 + }, 175 + 176 + pause: function() { 177 + this._paused++; 178 + return this; 179 + }, 180 + 181 + resume: function() { 182 + if (!this._paused) { 183 + JX.$E('Resuming a workflow which is not paused!'); 184 + } 185 + 186 + this._paused--; 187 + 188 + if (!this._paused) { 189 + var next = this._nextCallback; 190 + this._nextCallback = null; 191 + if (next) { 192 + next(); 193 + } 194 + } 195 + 196 + return this; 197 + }, 198 + 159 199 _onload : function(r) { 200 + this._destroy(); 201 + 160 202 // It is permissible to send back a falsey redirect to force a page 161 203 // reload, so we need to take this branch if the key is present. 162 204 if (r && (typeof r.redirect != 'undefined')) { ··· 247 289 this._root = null; 248 290 } 249 291 }, 292 + 250 293 start : function() { 294 + var next = JX.bind(this, this._send); 295 + 296 + this.pause(); 297 + this._nextCallback = next; 298 + 299 + this.invoke('start', this); 300 + 301 + this.resume(); 302 + }, 303 + 304 + _send: function() { 251 305 var uri = this.getURI(); 252 306 var method = this.getMethod(); 253 307 var r = new JX.Request(uri, JX.bind(this, this._onload)); ··· 288 342 for (var k in dictionary) { 289 343 this._data.push([k, dictionary[k]]); 290 344 } 345 + return this; 346 + }, 347 + 348 + addData: function(key, value) { 349 + this._data.push([key, value]); 291 350 return this; 292 351 }, 293 352
+3 -3
webroot/rsrc/js/core/DragAndDropFileUpload.js
··· 155 155 156 156 var files = e.getRawEvent().dataTransfer.files; 157 157 for (var ii = 0; ii < files.length; ii++) { 158 - this._sendRequest(files[ii]); 158 + this.sendRequest(files[ii]); 159 159 } 160 160 161 161 // Force depth to 0. ··· 216 216 if (!spec.name) { 217 217 spec.name = 'pasted_file'; 218 218 } 219 - this._sendRequest(spec); 219 + this.sendRequest(spec); 220 220 } 221 221 })); 222 222 } ··· 224 224 this.setIsEnabled(true); 225 225 }, 226 226 227 - _sendRequest : function(spec) { 227 + sendRequest : function(spec) { 228 228 var file = new JX.PhabricatorFileUpload() 229 229 .setRawFileObject(spec) 230 230 .setName(spec.name)
+20
webroot/rsrc/js/core/TextAreaUtils.js
··· 62 62 JX.TextAreaUtils.setSelectionRange(area, start, end); 63 63 }, 64 64 65 + 66 + /** 67 + * Insert a reference to a given uploaded file into a textarea. 68 + */ 69 + insertFileReference: function(area, file) { 70 + var ref = '{F' + file.getID() + '}'; 71 + 72 + // If we're inserting immediately after a "}" (usually, another file 73 + // reference), put some newlines before our token so that multiple file 74 + // uploads get laid out more nicely. 75 + var range = JX.TextAreaUtils.getSelectionRange(area); 76 + var before = area.value.substring(0, range.start); 77 + if (before.match(/\}$/)) { 78 + ref = '\n\n' + ref; 79 + } 80 + 81 + JX.TextAreaUtils.setSelectionText(area, ref, false); 82 + }, 83 + 84 + 65 85 /** 66 86 * Get the document pixel positions of the beginning and end of a character 67 87 * range in a textarea.
+7 -16
webroot/rsrc/js/core/behavior-drag-and-drop-textarea.js
··· 10 10 11 11 var target = JX.$(config.target); 12 12 13 - function onupload(f) { 14 - var ref = '{F' + f.getID() + '}'; 15 - 16 - // If we're inserting immediately after a "}" (usually, another file 17 - // reference), put some newlines before our token so that multiple file 18 - // uploads get laid out more nicely. 19 - var range = JX.TextAreaUtils.getSelectionRange(target); 20 - var before = target.value.substring(0, range.start); 21 - if (before.match(/\}$/)) { 22 - ref = '\n\n' + ref; 23 - } 24 - 25 - JX.TextAreaUtils.setSelectionText(target, ref, false); 26 - } 27 - 28 13 if (JX.PhabricatorDragAndDropFileUpload.isSupported()) { 29 14 var drop = new JX.PhabricatorDragAndDropFileUpload(target) 30 15 .setURI(config.uri) 31 16 .setChunkThreshold(config.chunkThreshold); 17 + 32 18 drop.listen('didBeginDrag', function() { 33 19 JX.DOM.alterClass(target, config.activatedClass, true); 34 20 }); 21 + 35 22 drop.listen('didEndDrag', function() { 36 23 JX.DOM.alterClass(target, config.activatedClass, false); 37 24 }); 38 - drop.listen('didUpload', onupload); 25 + 26 + drop.listen('didUpload', function(file) { 27 + JX.TextAreaUtils.insertFileReference(target, file); 28 + }); 29 + 39 30 drop.start(); 40 31 } 41 32
+15 -1
webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js
··· 194 194 .start(); 195 195 break; 196 196 case 'fa-cloud-upload': 197 - new JX.Workflow('/file/uploaddialog/').start(); 197 + new JX.Workflow('/file/uploaddialog/') 198 + .setHandler(function(response) { 199 + var files = response.files; 200 + for (var ii = 0; ii < files.length; ii++) { 201 + var file = files[ii]; 202 + 203 + var upload = new JX.PhabricatorFileUpload() 204 + .setID(file.id) 205 + .setPHID(file.phid) 206 + .setURI(file.uri); 207 + 208 + JX.TextAreaUtils.insertFileReference(area, upload); 209 + } 210 + }) 211 + .start(); 198 212 break; 199 213 case 'fa-arrows-alt': 200 214 if (edit_mode == 'fa-arrows-alt') {
+80
webroot/rsrc/js/phui/behavior-phui-file-upload.js
··· 1 + /** 2 + * @provides javelin-behavior-phui-file-upload 3 + * @requires javelin-behavior 4 + * javelin-stratcom 5 + * javelin-dom 6 + * phuix-dropdown-menu 7 + */ 8 + 9 + JX.behavior('phui-file-upload', function(config) { 10 + 11 + function startUpload(workflow, input) { 12 + var files = input.files; 13 + 14 + if (!files || !files.length) { 15 + return; 16 + } 17 + 18 + var state = { 19 + workflow: workflow, 20 + input: input, 21 + waiting: 0, 22 + phids: [] 23 + }; 24 + 25 + var callback = JX.bind(null, didUpload, state); 26 + 27 + var dummy = input; 28 + var uploader = new JX.PhabricatorDragAndDropFileUpload(dummy) 29 + .setURI(config.uploadURI) 30 + .setChunkThreshold(config.chunkThreshold); 31 + 32 + uploader.listen('didUpload', callback); 33 + uploader.start(); 34 + 35 + workflow.pause(); 36 + for (var ii = 0; ii < files.length; ii++) { 37 + state.waiting++; 38 + uploader.sendRequest(files[ii]); 39 + } 40 + } 41 + 42 + function didUpload(state, file) { 43 + state.phids.push(file.getPHID()); 44 + state.waiting--; 45 + 46 + if (state.waiting) { 47 + return; 48 + } 49 + 50 + state.workflow 51 + .addData(config.inputName, state.phids.join(', ')) 52 + .resume(); 53 + } 54 + 55 + JX.Workflow.listen('start', function(workflow) { 56 + var form = workflow.getSourceForm(); 57 + if (!form) { 58 + return; 59 + } 60 + 61 + var input; 62 + try { 63 + input = JX.$(config.fileInputID); 64 + } catch (ex) { 65 + return; 66 + } 67 + 68 + var local_form = JX.DOM.findAbove(input, 'form'); 69 + if (!local_form) { 70 + return; 71 + } 72 + 73 + if (local_form !== form) { 74 + return; 75 + } 76 + 77 + startUpload(workflow, input); 78 + }); 79 + 80 + });