diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ 'names' => array( 'core.pkg.css' => 'a8d8a51c', - 'core.pkg.js' => '07b01d4f', + 'core.pkg.js' => '920e18bd', 'darkconsole.pkg.js' => 'ca8671ce', 'differential.pkg.css' => '4a93db37', 'differential.pkg.js' => 'eca39a2c', @@ -202,7 +202,7 @@ 'rsrc/externals/javelin/lib/Router.js' => '29274e2b', 'rsrc/externals/javelin/lib/URI.js' => 'd9a9b862', 'rsrc/externals/javelin/lib/Vector.js' => 'bd0aedcd', - 'rsrc/externals/javelin/lib/Workflow.js' => '09b15cf1', + 'rsrc/externals/javelin/lib/Workflow.js' => '9a24d9c4', 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8', 'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b', 'rsrc/externals/javelin/lib/__tests__/JSON.js' => '2295d074', @@ -468,7 +468,7 @@ 'rsrc/js/core/behavior-object-selector.js' => 'e6f67523', 'rsrc/js/core/behavior-oncopy.js' => 'c3e218fe', 'rsrc/js/core/behavior-phabricator-nav.js' => 'b5842a5e', - 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => 'ba22863c', + 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '8ddb4ee2', 'rsrc/js/core/behavior-refresh-csrf.js' => '7814b593', 'rsrc/js/core/behavior-remarkup-preview.js' => 'f7379f45', 'rsrc/js/core/behavior-reorder-applications.js' => 'a8e3795d', @@ -612,7 +612,7 @@ 'javelin-behavior-phabricator-notification-example' => 'c51a6616', 'javelin-behavior-phabricator-object-selector' => 'e6f67523', 'javelin-behavior-phabricator-oncopy' => 'c3e218fe', - 'javelin-behavior-phabricator-remarkup-assist' => 'ba22863c', + 'javelin-behavior-phabricator-remarkup-assist' => '8ddb4ee2', 'javelin-behavior-phabricator-reveal-content' => '8f24abfc', 'javelin-behavior-phabricator-search-typeahead' => 'fbeabd1e', 'javelin-behavior-phabricator-show-all-transactions' => '7c273581', @@ -682,7 +682,7 @@ 'javelin-view-interpreter' => '0c33c1a0', 'javelin-view-renderer' => '6c2b09a2', 'javelin-view-visitor' => 'efe49472', - 'javelin-workflow' => '09b15cf1', + 'javelin-workflow' => '9a24d9c4', 'lightbox-attachment-css' => '7acac05d', 'maniphest-batch-editor' => '8f380ebc', 'maniphest-report-css' => '6fc16517', @@ -871,18 +871,6 @@ array( 0 => 'javelin-install', ), - '09b15cf1' => - array( - 0 => 'javelin-stratcom', - 1 => 'javelin-request', - 2 => 'javelin-dom', - 3 => 'javelin-vector', - 4 => 'javelin-install', - 5 => 'javelin-util', - 6 => 'javelin-mask', - 7 => 'javelin-uri', - 8 => 'javelin-routable', - ), '0a3f3021' => array( 0 => 'javelin-behavior', @@ -1478,6 +1466,17 @@ 2 => 'javelin-stratcom', 3 => 'javelin-uri', ), + '8ddb4ee2' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-dom', + 3 => 'phabricator-phtize', + 4 => 'phabricator-textareautils', + 5 => 'javelin-workflow', + 6 => 'javelin-vector', + 7 => 'javelin-util', + ), '8ef9ab58' => array( 0 => 'javelin-behavior', @@ -1537,6 +1536,18 @@ 3 => 'javelin-dom', 4 => 'phabricator-draggable-list', ), + '9a24d9c4' => + array( + 0 => 'javelin-stratcom', + 1 => 'javelin-request', + 2 => 'javelin-dom', + 3 => 'javelin-vector', + 4 => 'javelin-install', + 5 => 'javelin-util', + 6 => 'javelin-mask', + 7 => 'javelin-uri', + 8 => 'javelin-routable', + ), '9b9197be' => array( 0 => 'javelin-behavior', @@ -1712,16 +1723,6 @@ 0 => 'javelin-install', 1 => 'javelin-dom', ), - 'ba22863c' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-stratcom', - 2 => 'javelin-dom', - 3 => 'phabricator-phtize', - 4 => 'phabricator-textareautils', - 5 => 'javelin-workflow', - 6 => 'javelin-vector', - ), 'bd0aedcd' => array( 0 => 'javelin-install', diff --git a/src/applications/files/controller/PhabricatorFileUploadDialogController.php b/src/applications/files/controller/PhabricatorFileUploadDialogController.php --- a/src/applications/files/controller/PhabricatorFileUploadDialogController.php +++ b/src/applications/files/controller/PhabricatorFileUploadDialogController.php @@ -5,14 +5,62 @@ public function processRequest() { $request = $this->getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); - $dialog = id(new AphrontDialogView()) - ->setUser($user) + $severity = AphrontErrorView::SEVERITY_NOTICE; + $errors = array( + pht( + 'To upload files more easily, drag and drop them directly into '. + 'the text of your comment.')); + + $e_file = true; + if ($request->isFormPost()) { + $errors = array(); + $severity = AphrontErrorView::SEVERITY_ERROR; + + if (!$request->getFileExists('file')) { + $e_file = pht('Required'); + $errors[] = pht('You must select a file to upload.'); + } else { + try { + $file = PhabricatorFile::newFromPHPUpload( + $_FILES['file'], + array( + 'authorPHID' => $viewer->getPHID(), + 'isExplicitUpload' => true, + )); + } catch (Exception $ex) { + $e_file = pht('Error'); + $errors[] = $ex->getMessage(); + } + } + + if (!$errors) { + return id(new AphrontAjaxResponse())->setContent( + array( + 'id' => $file->getID(), + 'phid' => $file->getPHID(), + 'uri' => $file->getBestURI(), + )); + } + } + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild( + id(new AphrontFormFileControl()) + ->setName('file') + ->setError($e_file)); + + $dialog = $this->newDialog() + ->setFormEncoding('multipart/form-data') ->setTitle(pht('Upload File')) - ->appendChild(pht( - 'To add files, drag and drop them into the comment text area.')) - ->addCancelButton('/', pht('Close')); + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setErrors($errors) + ->setErrorSeverity($severity) + ->appendChild($form->buildLayoutView()) + ->addCancelButton('/', pht('Close')) + ->addSubmitButton(pht('Upload File')); return id(new AphrontDialogResponse())->setDialog($dialog); } diff --git a/src/view/AphrontDialogView.php b/src/view/AphrontDialogView.php --- a/src/view/AphrontDialogView.php +++ b/src/view/AphrontDialogView.php @@ -22,17 +22,28 @@ private $errors = array(); private $flush; private $validationException; - + private $errorSeverity = AphrontErrorView::SEVERITY_ERROR; + private $formEncoding; const WIDTH_DEFAULT = 'default'; const WIDTH_FORM = 'form'; const WIDTH_FULL = 'full'; + public function setFormEncoding($form_encoding) { + $this->formEncoding = $form_encoding; + return $this; + } + public function setMethod($method) { $this->method = $method; return $this; } + public function setErrorSeverity($error_severity) { + $this->errorSeverity = $error_severity; + return $this; + } + public function setIsStandalone($is_standalone) { $this->isStandalone = $is_standalone; return $this; @@ -240,9 +251,10 @@ ); $form_attributes = array( - 'action' => $this->submitURI, - 'method' => $this->method, - 'id' => $this->formID, + 'action' => $this->submitURI, + 'method' => $this->method, + 'id' => $this->formID, + 'enctype' => $this->formEncoding, ); $hidden_inputs = array(); @@ -287,7 +299,9 @@ if ($errors) { $children = array( - id(new AphrontErrorView())->setErrors($errors), + id(new AphrontErrorView()) + ->setErrors($errors) + ->setSeverity($this->errorSeverity), $children); } diff --git a/webroot/rsrc/externals/javelin/lib/Workflow.js b/webroot/rsrc/externals/javelin/lib/Workflow.js --- a/webroot/rsrc/externals/javelin/lib/Workflow.js +++ b/webroot/rsrc/externals/javelin/lib/Workflow.js @@ -33,9 +33,19 @@ statics : { _stack : [], newFromForm : function(form, data) { - var pairs = JX.DOM.convertFormToListOfPairs(form); - for (var k in data) { - pairs.push([k, data[k]]); + + var workflow = new JX.Workflow(form.getAttribute('action'), {}); + workflow.setMethod(form.getAttribute('method')); + + // If the form is "multipart/form-data", use FormData to marshall + if (form.enctype == 'multipart/form-data') { + workflow.setRawData(new FormData(form)); + } else { + var pairs = JX.DOM.convertFormToListOfPairs(form); + for (var k in data) { + pairs.push([k, data[k]]); + } + workflow.setDataWithListOfPairs(pairs); } // Disable form elements during the request @@ -51,15 +61,13 @@ } } - var workflow = new JX.Workflow(form.getAttribute('action'), {}); - workflow.setDataWithListOfPairs(pairs); - workflow.setMethod(form.getAttribute('method')); workflow.listen('finally', function() { // Re-enable form elements for (var ii = 0; ii < inputs.length; ii++) { inputs[ii] && (inputs[ii].disabled = false); } }); + return workflow; }, newFromLink : function(link) { @@ -130,17 +138,35 @@ return; } - var data = JX.DOM.convertFormToListOfPairs(form); - data.push([button.name, button.value || true]); + var data; + var raw_data; + if (form.enctype == 'multipart/form-data') { + raw_data = new FormData(form); + raw_data.append(button.name, button.value || 'true'); + } else { + data = JX.DOM.convertFormToListOfPairs(form); + data.push([button.name, button.value || true]); + } var active = JX.Workflow._getActiveWorkflow(); - var e = active.invoke('submit', {form: form, data: data}); + var submit_data = { + form: form, + data: data, + rawData: raw_data + }; + + var e = active.invoke('submit', submit_data); if (!e.getStopped()) { active._destroy(); - active - .setURI(form.getAttribute('action') || active.getURI()) - .setDataWithListOfPairs(data) - .start(); + active.setURI(form.getAttribute('action') || active.getURI()); + + if (raw_data) { + active.setRawData(raw_data); + } else { + active.setDataWithListOfPairs(data); + } + + active.start(); } }, _getActiveWorkflow : function() { @@ -153,6 +179,7 @@ _root : null, _pushed : false, _data : null, + _rawData: null, _onload : function(r) { // It is permissible to send back a falsey redirect to force a page // reload, so we need to take this branch if the key is present. @@ -240,13 +267,23 @@ var uri = this.getURI(); var method = this.getMethod(); var r = new JX.Request(uri, JX.bind(this, this._onload)); - var list_of_pairs = this._data; - list_of_pairs.push(['__wflow__', true]); - r.setDataWithListOfPairs(list_of_pairs); - r.setDataSerializer(this.getDataSerializer()); + + // If we have raw data, use that. Normally, this means we're submitting + // a form which includes a file input. + if (this._rawData) { + this._rawData.append('__wflow__', 'true'); + r.setRawData(this._rawData); + } else { + var list_of_pairs = this._data; + list_of_pairs.push(['__wflow__', true]); + r.setDataWithListOfPairs(list_of_pairs); + r.setDataSerializer(this.getDataSerializer()); + } + if (method) { r.setMethod(method); } + r.listen('finally', JX.bind(this, this.invoke, 'finally')); r.listen('error', JX.bind(this, function(error) { var e = this.invoke('error', error); @@ -257,6 +294,7 @@ // user to "/error/". We could emit a blanket 'workflow-failed' type // event instead. })); + r.send(); }, @@ -280,6 +318,11 @@ return this; }, + setRawData: function(form_data) { + this._rawData = form_data; + return this; + }, + setDataWithListOfPairs : function(list_of_pairs) { this._data = list_of_pairs; return this; diff --git a/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js b/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js --- a/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js +++ b/webroot/rsrc/js/core/behavior-phabricator-remarkup-assist.js @@ -7,6 +7,7 @@ * phabricator-textareautils * javelin-workflow * javelin-vector + * javelin-util */ JX.behavior('phabricator-remarkup-assist', function(config) { @@ -93,6 +94,11 @@ range.start + l.length + m.length); } + function onupload(area, response) { + var ref = '{F' + response.id + '}'; + JX.TextAreaUtils.setSelectionText(area, ref); + } + function assist(area, action, root) { // If the user has some text selected, we'll try to use that (for example, // if they have a word selected and want to bold it). Otherwise we'll insert @@ -150,7 +156,9 @@ .start(); break; case 'fa-cloud-upload': - new JX.Workflow('/file/uploaddialog/').start(); + new JX.Workflow('/file/uploaddialog/') + .setHandler(JX.bind(null, onupload, area)) + .start(); break; case 'fa-arrows-alt': if (edit_mode == 'fa-arrows-alt') {