From 90cbf8459c33559c1d81e5664f79aa1d4bcbbfed Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 29 Jul 2011 16:01:59 -0700 Subject: [PATCH] Streamline Files interfaces Summary: - There's no way you can figure out the ID of a file right now. Expose that more prominently. - Put the drag-and-drop uploader on the main page so you don't have to click through. - Restore the basic uploader so IE users can theoretically use the suite I guess? Added author info to basic uploader. - Show author information in the table. - Show date information in the table. - Link file names. - Rename table for filter views. - When you upload one file, just jump to it. When you upload multiple files, jump to your uploads and highlight them. - Add an "arc download" hint. Test Plan: Uploaded single files, groups of files, and files via simple uploader. Reviewers: codeblock, jungejason, tuomaspelkonen, aran Commenters: codeblock CC: aran, codeblock, epriestley Differential Revision: 746 --- src/__celerity_resource_map__.php | 126 ++++++++++-------- .../PhabricatorFileDropUploadController.php | 2 + .../list/PhabricatorFileListController.php | 105 +++++++++++++-- .../files/controller/list/__init__.php | 4 + .../PhabricatorFileUploadController.php | 38 +++--- .../files/controller/upload/__init__.php | 6 +- .../view/PhabricatorFileViewController.php | 11 ++ webroot/rsrc/css/aphront/table-view.css | 7 +- webroot/rsrc/css/application/files/files.css | 10 ++ .../core/behavior-files-drag-and-drop.js | 82 ++++++++++++ 10 files changed, 303 insertions(+), 88 deletions(-) create mode 100644 webroot/rsrc/css/application/files/files.css create mode 100644 webroot/rsrc/js/application/core/behavior-files-drag-and-drop.js diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 2ef3a110c1..679eee77a3 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -117,7 +117,7 @@ celerity_register_resource_map(array( ), 'aphront-table-view-css' => array( - 'uri' => '/res/910e83ec/rsrc/css/aphront/table-view.css', + 'uri' => '/res/3ac9ba50/rsrc/css/aphront/table-view.css', 'type' => 'css', 'requires' => array( @@ -247,6 +247,15 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/application/diffusion/diffusion-source.css', ), + 'files-css' => + array( + 'uri' => '/res/a265a77d/rsrc/css/application/files/files.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/files/files.css', + ), 'herald-css' => array( 'uri' => '/res/5051f3ab/rsrc/css/application/herald/herald.css', @@ -294,17 +303,6 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/javelin/lib/behavior.js', ), - 0 => - array( - 'uri' => '/res/1da00bfe/rsrc/js/javelin/lib/__tests__/URI.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-uri', - 1 => 'javelin-php-serializer', - ), - 'disk' => '/rsrc/js/javelin/lib/__tests__/URI.js', - ), 'javelin-behavior-aphront-basic-tokenizer' => array( 'uri' => '/res/5e183bd5/rsrc/js/application/core/behavior-tokenizer.js', @@ -538,6 +536,19 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/core/behavior-error-log.js', ), + 'javelin-behavior-files-drag-and-drop' => + array( + 'uri' => '/res/0e84cc42/rsrc/js/application/core/behavior-files-drag-and-drop.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-uri', + 3 => 'phabricator-drag-and-drop-file-upload', + ), + 'disk' => '/rsrc/js/application/core/behavior-files-drag-and-drop.js', + ), 'javelin-behavior-herald-rule-editor' => array( 'uri' => '/res/77a0c945/rsrc/js/application/herald/herald-rule-editor.js', @@ -626,6 +637,17 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/core/behavior-keyboard-shortcuts.js', ), + 0 => + array( + 'uri' => '/res/1da00bfe/rsrc/js/javelin/lib/__tests__/URI.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-uri', + 1 => 'javelin-php-serializer', + ), + 'disk' => '/rsrc/js/javelin/lib/__tests__/URI.js', + ), 'javelin-behavior-phabricator-object-selector' => array( 'uri' => '/res/34f9a11e/rsrc/js/application/core/behavior-object-selector.js', @@ -942,7 +964,7 @@ celerity_register_resource_map(array( ), 'mainphest-task-detail-css' => array( - 'uri' => '/res/dbefc148/rsrc/css/application/maniphest/task-detail.css', + 'uri' => '/res/e2fbb468/rsrc/css/application/maniphest/task-detail.css', 'type' => 'css', 'requires' => array( @@ -1247,30 +1269,6 @@ celerity_register_resource_map(array( 'uri' => '/res/pkg/25f94e94/typeahead.pkg.js', 'type' => 'js', ), - '2ac15016' => - array ( - 'name' => 'core.pkg.css', - 'symbols' => - array ( - 0 => 'phabricator-core-css', - 1 => 'phabricator-core-buttons-css', - 2 => 'phabricator-standard-page-view', - 3 => 'aphront-dialog-view-css', - 4 => 'aphront-form-view-css', - 5 => 'aphront-panel-view-css', - 6 => 'aphront-side-nav-view-css', - 7 => 'aphront-table-view-css', - 8 => 'aphront-crumbs-view-css', - 9 => 'aphront-tokenizer-control-css', - 10 => 'aphront-typeahead-control-css', - 11 => 'aphront-list-filter-view-css', - 12 => 'phabricator-directory-css', - 13 => 'phabricator-remarkup-css', - 14 => 'syntax-highlighting-css', - ), - 'uri' => '/res/pkg/2ac15016/core.pkg.css', - 'type' => 'css', - ), '307df223' => array ( 'name' => 'javelin.pkg.js', @@ -1307,6 +1305,30 @@ celerity_register_resource_map(array( 'uri' => '/res/pkg/95b66c1a/differential.pkg.css', 'type' => 'css', ), + 'b3fd9e3f' => + array ( + 'name' => 'core.pkg.css', + 'symbols' => + array ( + 0 => 'phabricator-core-css', + 1 => 'phabricator-core-buttons-css', + 2 => 'phabricator-standard-page-view', + 3 => 'aphront-dialog-view-css', + 4 => 'aphront-form-view-css', + 5 => 'aphront-panel-view-css', + 6 => 'aphront-side-nav-view-css', + 7 => 'aphront-table-view-css', + 8 => 'aphront-crumbs-view-css', + 9 => 'aphront-tokenizer-control-css', + 10 => 'aphront-typeahead-control-css', + 11 => 'aphront-list-filter-view-css', + 12 => 'phabricator-directory-css', + 13 => 'phabricator-remarkup-css', + 14 => 'syntax-highlighting-css', + ), + 'uri' => '/res/pkg/b3fd9e3f/core.pkg.css', + 'type' => 'css', + ), 'd0713563' => array ( 'name' => 'workflow.pkg.js', @@ -1340,15 +1362,15 @@ celerity_register_resource_map(array( ), 'reverse' => array ( - 'aphront-crumbs-view-css' => '2ac15016', - 'aphront-dialog-view-css' => '2ac15016', - 'aphront-form-view-css' => '2ac15016', - 'aphront-list-filter-view-css' => '2ac15016', - 'aphront-panel-view-css' => '2ac15016', - 'aphront-side-nav-view-css' => '2ac15016', - 'aphront-table-view-css' => '2ac15016', - 'aphront-tokenizer-control-css' => '2ac15016', - 'aphront-typeahead-control-css' => '2ac15016', + 'aphront-crumbs-view-css' => 'b3fd9e3f', + 'aphront-dialog-view-css' => 'b3fd9e3f', + 'aphront-form-view-css' => 'b3fd9e3f', + 'aphront-list-filter-view-css' => 'b3fd9e3f', + 'aphront-panel-view-css' => 'b3fd9e3f', + 'aphront-side-nav-view-css' => 'b3fd9e3f', + 'aphront-table-view-css' => 'b3fd9e3f', + 'aphront-tokenizer-control-css' => 'b3fd9e3f', + 'aphront-typeahead-control-css' => 'b3fd9e3f', 'differential-changeset-view-css' => '95b66c1a', 'differential-core-view-css' => '95b66c1a', 'differential-revision-add-comment-css' => '95b66c1a', @@ -1385,13 +1407,13 @@ celerity_register_resource_map(array( 'javelin-util' => '307df223', 'javelin-vector' => '307df223', 'javelin-workflow' => 'd0713563', - 'phabricator-core-buttons-css' => '2ac15016', - 'phabricator-core-css' => '2ac15016', - 'phabricator-directory-css' => '2ac15016', + 'phabricator-core-buttons-css' => 'b3fd9e3f', + 'phabricator-core-css' => 'b3fd9e3f', + 'phabricator-directory-css' => 'b3fd9e3f', 'phabricator-keyboard-shortcut' => 'd0713563', 'phabricator-keyboard-shortcut-manager' => 'd0713563', - 'phabricator-remarkup-css' => '2ac15016', - 'phabricator-standard-page-view' => '2ac15016', - 'syntax-highlighting-css' => '2ac15016', + 'phabricator-remarkup-css' => 'b3fd9e3f', + 'phabricator-standard-page-view' => 'b3fd9e3f', + 'syntax-highlighting-css' => 'b3fd9e3f', ), )); diff --git a/src/applications/files/controller/dropupload/PhabricatorFileDropUploadController.php b/src/applications/files/controller/dropupload/PhabricatorFileDropUploadController.php index 2f676ebff3..820482f39e 100644 --- a/src/applications/files/controller/dropupload/PhabricatorFileDropUploadController.php +++ b/src/applications/files/controller/dropupload/PhabricatorFileDropUploadController.php @@ -37,8 +37,10 @@ class PhabricatorFileDropUploadController extends PhabricatorFileController { return id(new AphrontAjaxResponse())->setContent( array( + 'id' => $file->getID(), 'phid' => $file->getPHID(), 'html' => $view->render(), + 'uri' => $file->getBestURI(), )); } diff --git a/src/applications/files/controller/list/PhabricatorFileListController.php b/src/applications/files/controller/list/PhabricatorFileListController.php index 85befebfcd..1aef4aea2f 100644 --- a/src/applications/files/controller/list/PhabricatorFileListController.php +++ b/src/applications/files/controller/list/PhabricatorFileListController.php @@ -21,6 +21,9 @@ class PhabricatorFileListController extends PhabricatorFileController { public function processRequest() { $request = $this->getRequest(); + $user = $request->getUser(); + + $upload_panel = $this->renderUploadPanel(); $author = null; $author_username = $request->getStr('author'); @@ -32,6 +35,10 @@ class PhabricatorFileListController extends PhabricatorFileController { if (!$author) { return id(new Aphront404Response()); } + + $title = 'Files Uploaded by '.phutil_escape_html($author->getUsername()); + } else { + $title = 'Files'; } $pager = new AphrontPagerView(); @@ -53,7 +60,15 @@ class PhabricatorFileListController extends PhabricatorFileController { $files = $pager->sliceResults($files); $pager->setURI($request->getRequestURI(), 'page'); + $phids = mpull($files, 'getAuthorPHID'); + $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); + + $highlighted = $request->getStr('h'); + $highlighted = explode('-', $highlighted); + $highlighted = array_fill_keys($highlighted, true); + $rows = array(); + $rowc = array(); foreach ($files as $file) { if ($file->isViewableInBrowser()) { $view_button = phutil_render_tag( @@ -66,10 +81,25 @@ class PhabricatorFileListController extends PhabricatorFileController { } else { $view_button = null; } + + if (isset($highlighted[$file->getID()])) { + $rowc[] = 'highlighted'; + } else { + $rowc[] = ''; + } + $rows[] = array( - phutil_escape_html($file->getPHID()), - phutil_escape_html($file->getName()), - phutil_escape_html($file->getByteSize()), + phutil_escape_html('F'.$file->getID()), + $file->getAuthorPHID() + ? $handles[$file->getAuthorPHID()]->renderLink() + : null, + phutil_render_tag( + 'a', + array( + 'href' => $file->getViewURI(), + ), + phutil_escape_html($file->getName())), + phutil_escape_html(number_format($file->getByteSize()).' bytes'), phutil_render_tag( 'a', array( @@ -85,38 +115,91 @@ class PhabricatorFileListController extends PhabricatorFileController { 'href' => '/file/download/'.$file->getPHID().'/', ), 'Download'), + phabricator_date($file->getDateCreated(), $user), + phabricator_time($file->getDateCreated(), $user), ); } $table = new AphrontTableView($rows); + $table->setRowClasses($rowc); $table->setHeaders( array( - 'PHID', + 'File ID', + 'Author', 'Name', 'Size', '', '', '', + 'Created', + '', )); $table->setColumnClasses( array( null, - 'wide', - null, + '', + 'wide pri', + 'right', 'action', 'action', 'action', + '', + 'right', )); $panel = new AphrontPanelView(); $panel->appendChild($table); - $panel->setHeader('Files'); - $panel->setCreateButton('Upload File', '/file/upload/'); + $panel->setHeader($title); $panel->appendChild($pager); - return $this->buildStandardPageResponse($panel, array( - 'title' => 'Files', - 'tab' => 'files', + return $this->buildStandardPageResponse( + array( + $upload_panel, + $panel, + ), + array( + 'title' => 'Files', + 'tab' => 'files', )); } + + private function renderUploadPanel() { + $request = $this->getRequest(); + $user = $request->getUser(); + + require_celerity_resource('files-css'); + $upload_id = celerity_generate_unique_node_id(); + $panel_id = celerity_generate_unique_node_id(); + + $upload_panel = new AphrontPanelView(); + $upload_panel->setHeader('Upload Files'); + $upload_panel->setCreateButton( + 'Basic Uploader', '/file/upload/'); + + $upload_panel->setWidth(AphrontPanelView::WIDTH_FULL); + $upload_panel->setID($panel_id); + + $upload_panel->appendChild( + phutil_render_tag( + 'div', + array( + 'id' => $upload_id, + 'style' => 'display: none;', + 'class' => 'files-drag-and-drop', + ), + '')); + + Javelin::initBehavior( + 'files-drag-and-drop', + array( + 'uri' => '/file/dropupload/', + 'browseURI' => '/file/?author='.$user->getUsername(), + 'control' => $upload_id, + 'target' => $panel_id, + 'activatedClass' => 'aphront-panel-view-drag-and-drop', + )); + + return $upload_panel; + } + } diff --git a/src/applications/files/controller/list/__init__.php b/src/applications/files/controller/list/__init__.php index d858fc288e..faa1cc60a3 100644 --- a/src/applications/files/controller/list/__init__.php +++ b/src/applications/files/controller/list/__init__.php @@ -10,9 +10,13 @@ phutil_require_module('phabricator', 'aphront/response/404'); phutil_require_module('phabricator', 'applications/files/controller/base'); phutil_require_module('phabricator', 'applications/files/storage/file'); phutil_require_module('phabricator', 'applications/people/storage/user'); +phutil_require_module('phabricator', 'applications/phid/handle/data'); +phutil_require_module('phabricator', 'infrastructure/celerity/api'); +phutil_require_module('phabricator', 'infrastructure/javelin/api'); phutil_require_module('phabricator', 'view/control/pager'); phutil_require_module('phabricator', 'view/control/table'); phutil_require_module('phabricator', 'view/layout/panel'); +phutil_require_module('phabricator', 'view/utils'); phutil_require_module('phutil', 'markup'); phutil_require_module('phutil', 'utils'); diff --git a/src/applications/files/controller/upload/PhabricatorFileUploadController.php b/src/applications/files/controller/upload/PhabricatorFileUploadController.php index 5bb5f2c333..3efe9f593c 100644 --- a/src/applications/files/controller/upload/PhabricatorFileUploadController.php +++ b/src/applications/files/controller/upload/PhabricatorFileUploadController.php @@ -24,44 +24,42 @@ class PhabricatorFileUploadController extends PhabricatorFileController { $user = $request->getUser(); if ($request->isFormPost()) { - $files = $request->getArr('file'); + $file = PhabricatorFile::newFromPHPUpload( + idx($_FILES, 'file'), + array( + 'name' => $request->getStr('name'), + 'authorPHID' => $user->getPHID(), + )); - if (count($files) > 1) { - return id(new AphrontRedirectResponse()) - ->setURI('/file/?author='.phutil_escape_uri($user->getUserName())); - } else { - return id(new AphrontRedirectResponse()) - ->setURI('/file/info/'.end($files).'/'); - } + return id(new AphrontRedirectResponse())->setURI($file->getBestURI()); } - $panel_id = celerity_generate_unique_node_id(); - $form = new AphrontFormView(); $form->setAction('/file/upload/'); $form->setUser($request->getUser()); $form ->setEncType('multipart/form-data') - ->appendChild( - id(new AphrontFormDragAndDropUploadControl()) - ->setLabel('Files') - ->setName('file') - ->setError(true) - ->setDragAndDropTarget($panel_id) - ->setActivatedClass('aphront-panel-view-drag-and-drop')) - + id(new AphrontFormFileControl()) + ->setLabel('File') + ->setName('file') + ->setError(true)) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Name') + ->setName('name') + ->setCaption('Optional file display name.')) ->appendChild( id(new AphrontFormSubmitControl()) - ->setValue('Done here!')); + ->setValue('Upload') + ->addCancelButton('/file/')); $panel = new AphrontPanelView(); $panel->setHeader('Upload File'); $panel->appendChild($form); $panel->setWidth(AphrontPanelView::WIDTH_FORM); - $panel->setID($panel_id); return $this->buildStandardPageResponse( array($panel), diff --git a/src/applications/files/controller/upload/__init__.php b/src/applications/files/controller/upload/__init__.php index 8867ecb9c3..0bb979638e 100644 --- a/src/applications/files/controller/upload/__init__.php +++ b/src/applications/files/controller/upload/__init__.php @@ -8,13 +8,13 @@ phutil_require_module('phabricator', 'aphront/response/redirect'); phutil_require_module('phabricator', 'applications/files/controller/base'); -phutil_require_module('phabricator', 'infrastructure/celerity/api'); +phutil_require_module('phabricator', 'applications/files/storage/file'); phutil_require_module('phabricator', 'view/form/base'); -phutil_require_module('phabricator', 'view/form/control/draganddropupload'); +phutil_require_module('phabricator', 'view/form/control/file'); phutil_require_module('phabricator', 'view/form/control/submit'); +phutil_require_module('phabricator', 'view/form/control/text'); phutil_require_module('phabricator', 'view/layout/panel'); -phutil_require_module('phutil', 'markup'); phutil_require_module('phutil', 'utils'); diff --git a/src/applications/files/controller/view/PhabricatorFileViewController.php b/src/applications/files/controller/view/PhabricatorFileViewController.php index a4e035e2c3..47a9e57df3 100644 --- a/src/applications/files/controller/view/PhabricatorFileViewController.php +++ b/src/applications/files/controller/view/PhabricatorFileViewController.php @@ -94,6 +94,9 @@ class PhabricatorFileViewController extends PhabricatorFileController { $form->setAction('/file/download/'.$file->getPHID().'/'); $button_name = 'Download File'; } + + $file_id = 'F'.$file->getID(); + $form->setUser($user); $form ->appendChild( @@ -101,6 +104,14 @@ class PhabricatorFileViewController extends PhabricatorFileController { ->setLabel('Name') ->setName('name') ->setValue($file->getName())) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel('ID') + ->setName('id') + ->setValue($file_id) + ->setCaption( + 'Download this file with: arc download '. + phutil_escape_html($file_id).'')) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('PHID') diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css index ce53b32e84..7d7e58557b 100644 --- a/webroot/rsrc/css/aphront/table-view.css +++ b/webroot/rsrc/css/aphront/table-view.css @@ -74,9 +74,12 @@ white-space: normal; } -.aphront-table-view tr.highlighted, +.aphront-table-view tr.highlighted { + background: #ffff99; +} + .aphront-table-view tr.alt-highlighted { - background: #ffff66; + background: #f3f399; } diff --git a/webroot/rsrc/css/application/files/files.css b/webroot/rsrc/css/application/files/files.css new file mode 100644 index 0000000000..beaff56c38 --- /dev/null +++ b/webroot/rsrc/css/application/files/files.css @@ -0,0 +1,10 @@ +/** + * @provides files-css + */ + +.files-drag-and-drop { + text-align: center; + padding: 0em 1em .5em; + font-size: 15px; + color: #666666; +} diff --git a/webroot/rsrc/js/application/core/behavior-files-drag-and-drop.js b/webroot/rsrc/js/application/core/behavior-files-drag-and-drop.js new file mode 100644 index 0000000000..f85f0b616e --- /dev/null +++ b/webroot/rsrc/js/application/core/behavior-files-drag-and-drop.js @@ -0,0 +1,82 @@ +/** + * @provides javelin-behavior-files-drag-and-drop + * @requires javelin-behavior + * javelin-dom + * javelin-uri + * phabricator-drag-and-drop-file-upload + */ + +JX.behavior('files-drag-and-drop', function(config) { + + // The control renders hidden by default; if we don't have support for + // drag-and-drop just leave it hidden. + if (!JX.PhabricatorDragAndDropFileUpload.isSupported()) { + return; + } + + var pending = 0; + var files = []; + + var control = JX.$(config.control); + // Show the control, since we have browser support. + control.style.display = ''; + + var drop = new JX.PhabricatorDragAndDropFileUpload(JX.$(config.target)) + .setActivatedClass(config.activatedClass) + .setURI(config.uri); + + drop.listen('willUpload', function(f) { + pending++; + redraw(); + }); + + drop.listen('didUpload', function(f) { + files.push(f); + + pending--; + if (pending == 0) { + // If whatever the user dropped in has finished uploading, either send + // them to the file itself (if they uploaded only one) or to their + // uploads (if they uploaded several). + var uri; + if (files.length == 1) { + uri = JX.$U(files[0].uri); + } else { + uri = JX.$U(config.browseURI); + var ids = []; + for (var ii = 0; ii < files.length; ii++) { + ids.push(files[ii].id); + } + uri.setQueryParam('h', ids.join('-')); + } + + // Reset so if you hit 'back' into the bfcache the page is still in a + // sensible state. + redraw(); + files = []; + + uri.go(); + } else { + redraw(); + } + }); + + drop.start(); + redraw(); + + function redraw() { + + var status; + if (pending) { + status = 'Uploading ' + pending + ' files...'; + } else { + var arrow = String.fromCharCode(0x21EA); + status = JX.$H( + arrow + ' Drag and Drop files here to upload them.'); + } + + JX.DOM.setContent(control, status); + } + +}); +