From 221562b29424f815e04c03b0dfb428b61c35cb53 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 16 Dec 2012 16:34:01 -0800 Subject: [PATCH] Modernize file uploads Summary: Modernizes file uploads. In particular: - Adds a mobile menu, with an "Upload File" item. - Adds crumbs to the list view, detail view and upload view. - Adds "Upload File" action to crumbs. - Moves upload file to a separate page. - Removes the combined upload file + recent files page. - Makes upload file use a normal file control by default (works on mobile). - Home page, file list and file upload page are now global drop targets which accept files dropped anywhere on them. Dragging a file into the window shows a mask and an instructional message. - User education on this is a little weak but I think that's a big can of worms? - Fixes a bug where dropping multiple files into a Remarkup text area produced bad results (resolves T2190). T879 is related, although it's specifically about Maniphest. I've declined to make global drop targets yet there because there are multiple drop targets on the page with different meanings. That UI needs updating in general. @chad, do we have an "upload" icon (counterpart to "download")? Test Plan: Uploaded files in Maniphest, Differential, Files, and from Home. Dragged and dropped multiple files into Differential. Used crumbs, mobile. Reviewers: chad, btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T2190 Differential Revision: https://secure.phabricator.com/D4200 --- scripts/celerity_mapper.php | 2 + src/__celerity_resource_map__.php | 278 +++++++++--------- src/__phutil_library_map__.php | 4 +- .../PhabricatorDirectoryMainController.php | 3 +- .../controller/PhabricatorFileController.php | 46 ++- .../PhabricatorFileListController.php | 113 +------ .../PhabricatorFileUploadController.php | 110 ++++++- .../files/view/PhabricatorFileUploadView.php | 73 ----- .../PhabricatorGlobalUploadTargetView.php | 37 +++ .../control/AphrontFormTextAreaControl.php | 1 - src/view/layout/AphrontSideNavFilterView.php | 9 +- src/view/layout/PhabricatorMenuView.php | 35 ++- webroot/rsrc/css/application/files/files.css | 10 - .../files/global-drag-and-drop.css | 22 ++ .../application/core/DragAndDropFileUpload.js | 23 +- .../core/behavior-drag-and-drop-textarea.js | 20 +- .../core/behavior-drag-and-drop.js | 10 +- .../core/behavior-files-drag-and-drop.js | 84 ------ .../core/behavior-global-drag-and-drop.js | 68 +++++ 19 files changed, 500 insertions(+), 448 deletions(-) delete mode 100644 src/applications/files/view/PhabricatorFileUploadView.php create mode 100644 src/applications/files/view/PhabricatorGlobalUploadTargetView.php delete mode 100644 webroot/rsrc/css/application/files/files.css create mode 100644 webroot/rsrc/css/application/files/global-drag-and-drop.css delete mode 100644 webroot/rsrc/js/application/core/behavior-files-drag-and-drop.js create mode 100644 webroot/rsrc/js/application/core/behavior-global-drag-and-drop.js diff --git a/scripts/celerity_mapper.php b/scripts/celerity_mapper.php index 1172c93a98..ab37b3482e 100755 --- a/scripts/celerity_mapper.php +++ b/scripts/celerity_mapper.php @@ -56,6 +56,7 @@ $package_spec = array( 'javelin-behavior-phabricator-remarkup-assist', 'phabricator-textareautils', 'phabricator-file-upload', + 'javelin-behavior-global-drag-and-drop', ), 'core.pkg.css' => array( 'phabricator-core-css', @@ -100,6 +101,7 @@ $package_spec = array( 'phabricator-side-menu-view-css', 'phabricator-crumbs-view-css', 'phabricator-object-item-list-view-css', + 'global-drag-and-drop-css', ), 'differential.pkg.css' => array( diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index bcaae2417b..4392213523 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -478,15 +478,15 @@ celerity_register_resource_map(array( ), '/rsrc/image/sprite-apps-large-X2.png' => array( - 'hash' => 'dffe647d8d8ddfab9b2f5f870b4e40b4', - 'uri' => '/res/dffe647d/rsrc/image/sprite-apps-large-X2.png', + 'hash' => 'f1218e52784088e7aabdb2744bda2cc3', + 'uri' => '/res/f1218e52/rsrc/image/sprite-apps-large-X2.png', 'disk' => '/rsrc/image/sprite-apps-large-X2.png', 'type' => 'png', ), '/rsrc/image/sprite-apps-large.png' => array( - 'hash' => 'cc0d81cafa185c541350a9db6f3befb7', - 'uri' => '/res/cc0d81ca/rsrc/image/sprite-apps-large.png', + 'hash' => 'f19222adadaddd0dd7e12bcd1b1fdba9', + 'uri' => '/res/f19222ad/rsrc/image/sprite-apps-large.png', 'disk' => '/rsrc/image/sprite-apps-large.png', 'type' => 'png', ), @@ -513,15 +513,15 @@ celerity_register_resource_map(array( ), '/rsrc/image/sprite-icon-X2.png' => array( - 'hash' => 'd4c36b33f4961bdcf63a0cc0bb4ecb1e', - 'uri' => '/res/d4c36b33/rsrc/image/sprite-icon-X2.png', + 'hash' => 'c9fae25bc6221922ce26517e654a18e4', + 'uri' => '/res/c9fae25b/rsrc/image/sprite-icon-X2.png', 'disk' => '/rsrc/image/sprite-icon-X2.png', 'type' => 'png', ), '/rsrc/image/sprite-icon.png' => array( - 'hash' => 'b29f5d1d2781b6589946bd73734100f1', - 'uri' => '/res/b29f5d1d/rsrc/image/sprite-icon.png', + 'hash' => 'b690ea69bf5f2abe84d0a6e9ef64b03d', + 'uri' => '/res/b690ea69/rsrc/image/sprite-icon.png', 'disk' => '/rsrc/image/sprite-icon.png', 'type' => 'png', ), @@ -690,7 +690,7 @@ celerity_register_resource_map(array( ), 'aphront-table-view-css' => array( - 'uri' => '/res/732d5e1f/rsrc/css/aphront/table-view.css', + 'uri' => '/res/d2cd4818/rsrc/css/aphront/table-view.css', 'type' => 'css', 'requires' => array( @@ -760,7 +760,7 @@ celerity_register_resource_map(array( ), 'differential-local-commits-view-css' => array( - 'uri' => '/res/86432ba7/rsrc/css/application/differential/local-commits-view.css', + 'uri' => '/res/224f3703/rsrc/css/application/differential/local-commits-view.css', 'type' => 'css', 'requires' => array( @@ -805,7 +805,7 @@ celerity_register_resource_map(array( ), 'differential-revision-history-css' => array( - 'uri' => '/res/71cffe43/rsrc/css/application/differential/revision-history.css', + 'uri' => '/res/d41bc64c/rsrc/css/application/differential/revision-history.css', 'type' => 'css', 'requires' => array( @@ -857,14 +857,14 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/application/diffusion/diffusion-source.css', ), - 'files-css' => + 'global-drag-and-drop-css' => array( - 'uri' => '/res/a265a77d/rsrc/css/application/files/files.css', + 'uri' => '/res/f3fb4a79/rsrc/css/application/files/global-drag-and-drop.css', 'type' => 'css', 'requires' => array( ), - 'disk' => '/rsrc/css/application/files/files.css', + 'disk' => '/rsrc/css/application/files/global-drag-and-drop.css', ), 'herald-css' => array( @@ -978,20 +978,19 @@ celerity_register_resource_map(array( ), 'javelin-behavior-aphront-drag-and-drop' => array( - 'uri' => '/res/0910fc0a/rsrc/js/application/core/behavior-drag-and-drop.js', + 'uri' => '/res/84a67d72/rsrc/js/application/core/behavior-drag-and-drop.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'phabricator-drag-and-drop-file-upload', + 2 => 'phabricator-drag-and-drop-file-upload', ), 'disk' => '/rsrc/js/application/core/behavior-drag-and-drop.js', ), 'javelin-behavior-aphront-drag-and-drop-textarea' => array( - 'uri' => '/res/ad737ce4/rsrc/js/application/core/behavior-drag-and-drop-textarea.js', + 'uri' => '/res/853e33b9/rsrc/js/application/core/behavior-drag-and-drop-textarea.js', 'type' => 'js', 'requires' => array( @@ -1363,18 +1362,19 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/core/behavior-fancy-datepicker.js', ), - 'javelin-behavior-files-drag-and-drop' => + 'javelin-behavior-global-drag-and-drop' => array( - 'uri' => '/res/4eb2f339/rsrc/js/application/core/behavior-files-drag-and-drop.js', + 'uri' => '/res/73ae3fd1/rsrc/js/application/core/behavior-global-drag-and-drop.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-uri', - 3 => 'phabricator-drag-and-drop-file-upload', + 3 => 'javelin-mask', + 4 => 'phabricator-drag-and-drop-file-upload', ), - 'disk' => '/rsrc/js/application/core/behavior-files-drag-and-drop.js', + 'disk' => '/rsrc/js/application/core/behavior-global-drag-and-drop.js', ), 'javelin-behavior-herald-rule-editor' => array( @@ -2473,7 +2473,7 @@ celerity_register_resource_map(array( ), 'phabricator-drag-and-drop-file-upload' => array( - 'uri' => '/res/496110e1/rsrc/js/application/core/DragAndDropFileUpload.js', + 'uri' => '/res/ce71f19a/rsrc/js/application/core/DragAndDropFileUpload.js', 'type' => 'js', 'requires' => array( @@ -2753,7 +2753,7 @@ celerity_register_resource_map(array( ), 'phabricator-property-list-view-css' => array( - 'uri' => '/res/9f4d6640/rsrc/css/layout/phabricator-property-list-view.css', + 'uri' => '/res/cd84ee5a/rsrc/css/layout/phabricator-property-list-view.css', 'type' => 'css', 'requires' => array( @@ -2810,7 +2810,7 @@ celerity_register_resource_map(array( ), 'phabricator-source-code-view-css' => array( - 'uri' => '/res/cf0c566c/rsrc/css/layout/phabricator-source-code-view.css', + 'uri' => '/res/aa04c202/rsrc/css/layout/phabricator-source-code-view.css', 'type' => 'css', 'requires' => array( @@ -3128,7 +3128,7 @@ celerity_register_resource_map(array( ), 'sprite-apps-large-css' => array( - 'uri' => '/res/3629ff92/rsrc/css/sprite-apps-large.css', + 'uri' => '/res/250ebd13/rsrc/css/sprite-apps-large.css', 'type' => 'css', 'requires' => array( @@ -3155,7 +3155,7 @@ celerity_register_resource_map(array( ), 'sprite-icon-css' => array( - 'uri' => '/res/2e174787/rsrc/css/sprite-icon.css', + 'uri' => '/res/ff841245/rsrc/css/sprite-icon.css', 'type' => 'css', 'requires' => array( @@ -3201,7 +3201,7 @@ celerity_register_resource_map(array( ), array( 'packages' => array( - 'e6ad9cda' => + 'a5058778' => array( 'name' => 'core.pkg.css', 'symbols' => @@ -3244,11 +3244,12 @@ celerity_register_resource_map(array( 35 => 'phabricator-side-menu-view-css', 36 => 'phabricator-crumbs-view-css', 37 => 'phabricator-object-item-list-view-css', + 38 => 'global-drag-and-drop-css', ), - 'uri' => '/res/pkg/e6ad9cda/core.pkg.css', + 'uri' => '/res/pkg/a5058778/core.pkg.css', 'type' => 'css', ), - 'ba3c323b' => + '70c8162b' => array( 'name' => 'core.pkg.js', 'symbols' => @@ -3284,8 +3285,9 @@ celerity_register_resource_map(array( 28 => 'javelin-behavior-phabricator-remarkup-assist', 29 => 'phabricator-textareautils', 30 => 'phabricator-file-upload', + 31 => 'javelin-behavior-global-drag-and-drop', ), - 'uri' => '/res/pkg/ba3c323b/core.pkg.js', + 'uri' => '/res/pkg/70c8162b/core.pkg.js', 'type' => 'js', ), '8edbada5' => @@ -3300,7 +3302,7 @@ celerity_register_resource_map(array( 'uri' => '/res/pkg/8edbada5/darkconsole.pkg.js', 'type' => 'js', ), - '94cb8965' => + '380df740' => array( 'name' => 'differential.pkg.css', 'symbols' => @@ -3320,10 +3322,10 @@ celerity_register_resource_map(array( 12 => 'differential-local-commits-view-css', 13 => 'inline-comment-summary-css', ), - 'uri' => '/res/pkg/94cb8965/differential.pkg.css', + 'uri' => '/res/pkg/380df740/differential.pkg.css', 'type' => 'css', ), - '7ecd31fa' => + 'a8e8f2b7' => array( 'name' => 'differential.pkg.js', 'symbols' => @@ -3348,7 +3350,7 @@ celerity_register_resource_map(array( 17 => 'javelin-behavior-differential-toggle-files', 18 => 'javelin-behavior-differential-user-select', ), - 'uri' => '/res/pkg/7ecd31fa/differential.pkg.js', + 'uri' => '/res/pkg/a8e8f2b7/differential.pkg.js', 'type' => 'js', ), 'c8ce2d88' => @@ -3433,82 +3435,84 @@ celerity_register_resource_map(array( 'reverse' => array( 'aphront-attached-file-view-css' => '7839ae2d', - 'aphront-crumbs-view-css' => 'e6ad9cda', - 'aphront-dialog-view-css' => 'e6ad9cda', - 'aphront-error-view-css' => 'e6ad9cda', - 'aphront-form-view-css' => 'e6ad9cda', - 'aphront-headsup-action-list-view-css' => '94cb8965', - 'aphront-headsup-view-css' => 'e6ad9cda', - 'aphront-list-filter-view-css' => 'e6ad9cda', - 'aphront-pager-view-css' => 'e6ad9cda', - 'aphront-panel-view-css' => 'e6ad9cda', - 'aphront-side-nav-view-css' => 'e6ad9cda', - 'aphront-table-view-css' => 'e6ad9cda', - 'aphront-tokenizer-control-css' => 'e6ad9cda', - 'aphront-tooltip-css' => 'e6ad9cda', - 'aphront-typeahead-control-css' => 'e6ad9cda', - 'differential-changeset-view-css' => '94cb8965', - 'differential-core-view-css' => '94cb8965', - 'differential-inline-comment-editor' => '7ecd31fa', - 'differential-local-commits-view-css' => '94cb8965', - 'differential-results-table-css' => '94cb8965', - 'differential-revision-add-comment-css' => '94cb8965', - 'differential-revision-comment-css' => '94cb8965', - 'differential-revision-comment-list-css' => '94cb8965', - 'differential-revision-history-css' => '94cb8965', - 'differential-revision-list-css' => '94cb8965', - 'differential-table-of-contents-css' => '94cb8965', + 'aphront-crumbs-view-css' => 'a5058778', + 'aphront-dialog-view-css' => 'a5058778', + 'aphront-error-view-css' => 'a5058778', + 'aphront-form-view-css' => 'a5058778', + 'aphront-headsup-action-list-view-css' => '380df740', + 'aphront-headsup-view-css' => 'a5058778', + 'aphront-list-filter-view-css' => 'a5058778', + 'aphront-pager-view-css' => 'a5058778', + 'aphront-panel-view-css' => 'a5058778', + 'aphront-side-nav-view-css' => 'a5058778', + 'aphront-table-view-css' => 'a5058778', + 'aphront-tokenizer-control-css' => 'a5058778', + 'aphront-tooltip-css' => 'a5058778', + 'aphront-typeahead-control-css' => 'a5058778', + 'differential-changeset-view-css' => '380df740', + 'differential-core-view-css' => '380df740', + 'differential-inline-comment-editor' => 'a8e8f2b7', + 'differential-local-commits-view-css' => '380df740', + 'differential-results-table-css' => '380df740', + 'differential-revision-add-comment-css' => '380df740', + 'differential-revision-comment-css' => '380df740', + 'differential-revision-comment-list-css' => '380df740', + 'differential-revision-history-css' => '380df740', + 'differential-revision-list-css' => '380df740', + 'differential-table-of-contents-css' => '380df740', 'diffusion-commit-view-css' => 'c8ce2d88', 'diffusion-icons-css' => 'c8ce2d88', - 'inline-comment-summary-css' => '94cb8965', - 'javelin-aphlict' => 'ba3c323b', + 'global-drag-and-drop-css' => 'a5058778', + 'inline-comment-summary-css' => '380df740', + 'javelin-aphlict' => '70c8162b', 'javelin-behavior' => 'db6d724d', - 'javelin-behavior-aphlict-dropdown' => 'ba3c323b', - 'javelin-behavior-aphlict-listen' => 'ba3c323b', - 'javelin-behavior-aphront-basic-tokenizer' => 'ba3c323b', - 'javelin-behavior-aphront-drag-and-drop' => '7ecd31fa', - 'javelin-behavior-aphront-drag-and-drop-textarea' => '7ecd31fa', - 'javelin-behavior-aphront-form-disable-on-submit' => 'ba3c323b', + 'javelin-behavior-aphlict-dropdown' => '70c8162b', + 'javelin-behavior-aphlict-listen' => '70c8162b', + 'javelin-behavior-aphront-basic-tokenizer' => '70c8162b', + 'javelin-behavior-aphront-drag-and-drop' => 'a8e8f2b7', + 'javelin-behavior-aphront-drag-and-drop-textarea' => 'a8e8f2b7', + 'javelin-behavior-aphront-form-disable-on-submit' => '70c8162b', 'javelin-behavior-audit-preview' => '5e68be89', 'javelin-behavior-dark-console' => '8edbada5', 'javelin-behavior-dark-console-ajax' => '8edbada5', - 'javelin-behavior-device' => 'ba3c323b', - 'javelin-behavior-differential-accept-with-errors' => '7ecd31fa', - 'javelin-behavior-differential-add-reviewers-and-ccs' => '7ecd31fa', - 'javelin-behavior-differential-comment-jump' => '7ecd31fa', - 'javelin-behavior-differential-diff-radios' => '7ecd31fa', - 'javelin-behavior-differential-dropdown-menus' => '7ecd31fa', - 'javelin-behavior-differential-edit-inline-comments' => '7ecd31fa', - 'javelin-behavior-differential-feedback-preview' => '7ecd31fa', - 'javelin-behavior-differential-keyboard-navigation' => '7ecd31fa', - 'javelin-behavior-differential-populate' => '7ecd31fa', - 'javelin-behavior-differential-show-more' => '7ecd31fa', - 'javelin-behavior-differential-toggle-files' => '7ecd31fa', - 'javelin-behavior-differential-user-select' => '7ecd31fa', + 'javelin-behavior-device' => '70c8162b', + 'javelin-behavior-differential-accept-with-errors' => 'a8e8f2b7', + 'javelin-behavior-differential-add-reviewers-and-ccs' => 'a8e8f2b7', + 'javelin-behavior-differential-comment-jump' => 'a8e8f2b7', + 'javelin-behavior-differential-diff-radios' => 'a8e8f2b7', + 'javelin-behavior-differential-dropdown-menus' => 'a8e8f2b7', + 'javelin-behavior-differential-edit-inline-comments' => 'a8e8f2b7', + 'javelin-behavior-differential-feedback-preview' => 'a8e8f2b7', + 'javelin-behavior-differential-keyboard-navigation' => 'a8e8f2b7', + 'javelin-behavior-differential-populate' => 'a8e8f2b7', + 'javelin-behavior-differential-show-more' => 'a8e8f2b7', + 'javelin-behavior-differential-toggle-files' => 'a8e8f2b7', + 'javelin-behavior-differential-user-select' => 'a8e8f2b7', 'javelin-behavior-diffusion-commit-graph' => '5e68be89', 'javelin-behavior-diffusion-pull-lastmodified' => '5e68be89', 'javelin-behavior-error-log' => '8edbada5', - 'javelin-behavior-konami' => 'ba3c323b', - 'javelin-behavior-lightbox-attachments' => 'ba3c323b', + 'javelin-behavior-global-drag-and-drop' => '70c8162b', + 'javelin-behavior-konami' => '70c8162b', + 'javelin-behavior-lightbox-attachments' => '70c8162b', 'javelin-behavior-maniphest-batch-selector' => '7707de41', 'javelin-behavior-maniphest-subpriority-editor' => '7707de41', 'javelin-behavior-maniphest-transaction-controls' => '7707de41', 'javelin-behavior-maniphest-transaction-expand' => '7707de41', 'javelin-behavior-maniphest-transaction-preview' => '7707de41', - 'javelin-behavior-phabricator-active-nav' => 'ba3c323b', - 'javelin-behavior-phabricator-autofocus' => 'ba3c323b', - 'javelin-behavior-phabricator-keyboard-shortcuts' => 'ba3c323b', - 'javelin-behavior-phabricator-nav' => 'ba3c323b', - 'javelin-behavior-phabricator-object-selector' => '7ecd31fa', - 'javelin-behavior-phabricator-oncopy' => 'ba3c323b', - 'javelin-behavior-phabricator-remarkup-assist' => 'ba3c323b', - 'javelin-behavior-phabricator-search-typeahead' => 'ba3c323b', - 'javelin-behavior-phabricator-tooltips' => 'ba3c323b', - 'javelin-behavior-phabricator-watch-anchor' => 'ba3c323b', - 'javelin-behavior-refresh-csrf' => 'ba3c323b', - 'javelin-behavior-repository-crossreference' => '7ecd31fa', - 'javelin-behavior-toggle-class' => 'ba3c323b', - 'javelin-behavior-workflow' => 'ba3c323b', + 'javelin-behavior-phabricator-active-nav' => '70c8162b', + 'javelin-behavior-phabricator-autofocus' => '70c8162b', + 'javelin-behavior-phabricator-keyboard-shortcuts' => '70c8162b', + 'javelin-behavior-phabricator-nav' => '70c8162b', + 'javelin-behavior-phabricator-object-selector' => 'a8e8f2b7', + 'javelin-behavior-phabricator-oncopy' => '70c8162b', + 'javelin-behavior-phabricator-remarkup-assist' => '70c8162b', + 'javelin-behavior-phabricator-search-typeahead' => '70c8162b', + 'javelin-behavior-phabricator-tooltips' => '70c8162b', + 'javelin-behavior-phabricator-watch-anchor' => '70c8162b', + 'javelin-behavior-refresh-csrf' => '70c8162b', + 'javelin-behavior-repository-crossreference' => 'a8e8f2b7', + 'javelin-behavior-toggle-class' => '70c8162b', + 'javelin-behavior-workflow' => '70c8162b', 'javelin-dom' => 'db6d724d', 'javelin-event' => 'db6d724d', 'javelin-install' => 'db6d724d', @@ -3527,48 +3531,48 @@ celerity_register_resource_map(array( 'javelin-util' => 'db6d724d', 'javelin-vector' => 'db6d724d', 'javelin-workflow' => 'db6d724d', - 'lightbox-attachment-css' => 'e6ad9cda', + 'lightbox-attachment-css' => 'a5058778', 'maniphest-task-summary-css' => '7839ae2d', 'maniphest-transaction-detail-css' => '7839ae2d', - 'phabricator-app-buttons-css' => 'e6ad9cda', - 'phabricator-busy' => 'ba3c323b', - 'phabricator-content-source-view-css' => '94cb8965', - 'phabricator-core-buttons-css' => 'e6ad9cda', - 'phabricator-core-css' => 'e6ad9cda', - 'phabricator-crumbs-view-css' => 'e6ad9cda', - 'phabricator-directory-css' => 'e6ad9cda', - 'phabricator-drag-and-drop-file-upload' => '7ecd31fa', - 'phabricator-dropdown-menu' => 'ba3c323b', - 'phabricator-file-upload' => 'ba3c323b', - 'phabricator-filetree-view-css' => 'e6ad9cda', - 'phabricator-flag-css' => 'e6ad9cda', - 'phabricator-form-view-css' => 'e6ad9cda', - 'phabricator-header-view-css' => 'e6ad9cda', - 'phabricator-jump-nav' => 'e6ad9cda', - 'phabricator-keyboard-shortcut' => 'ba3c323b', - 'phabricator-keyboard-shortcut-manager' => 'ba3c323b', - 'phabricator-main-menu-view' => 'e6ad9cda', - 'phabricator-menu-item' => 'ba3c323b', - 'phabricator-nav-view-css' => 'e6ad9cda', - 'phabricator-notification' => 'ba3c323b', - 'phabricator-notification-css' => 'e6ad9cda', - 'phabricator-notification-menu-css' => 'e6ad9cda', - 'phabricator-object-item-list-view-css' => 'e6ad9cda', - 'phabricator-object-selector-css' => '94cb8965', - 'phabricator-paste-file-upload' => 'ba3c323b', - 'phabricator-prefab' => 'ba3c323b', + 'phabricator-app-buttons-css' => 'a5058778', + 'phabricator-busy' => '70c8162b', + 'phabricator-content-source-view-css' => '380df740', + 'phabricator-core-buttons-css' => 'a5058778', + 'phabricator-core-css' => 'a5058778', + 'phabricator-crumbs-view-css' => 'a5058778', + 'phabricator-directory-css' => 'a5058778', + 'phabricator-drag-and-drop-file-upload' => 'a8e8f2b7', + 'phabricator-dropdown-menu' => '70c8162b', + 'phabricator-file-upload' => '70c8162b', + 'phabricator-filetree-view-css' => 'a5058778', + 'phabricator-flag-css' => 'a5058778', + 'phabricator-form-view-css' => 'a5058778', + 'phabricator-header-view-css' => 'a5058778', + 'phabricator-jump-nav' => 'a5058778', + 'phabricator-keyboard-shortcut' => '70c8162b', + 'phabricator-keyboard-shortcut-manager' => '70c8162b', + 'phabricator-main-menu-view' => 'a5058778', + 'phabricator-menu-item' => '70c8162b', + 'phabricator-nav-view-css' => 'a5058778', + 'phabricator-notification' => '70c8162b', + 'phabricator-notification-css' => 'a5058778', + 'phabricator-notification-menu-css' => 'a5058778', + 'phabricator-object-item-list-view-css' => 'a5058778', + 'phabricator-object-selector-css' => '380df740', + 'phabricator-paste-file-upload' => '70c8162b', + 'phabricator-prefab' => '70c8162b', 'phabricator-project-tag-css' => '7839ae2d', - 'phabricator-remarkup-css' => 'e6ad9cda', - 'phabricator-shaped-request' => '7ecd31fa', - 'phabricator-side-menu-view-css' => 'e6ad9cda', - 'phabricator-standard-page-view' => 'e6ad9cda', - 'phabricator-textareautils' => 'ba3c323b', - 'phabricator-tooltip' => 'ba3c323b', - 'phabricator-transaction-view-css' => 'e6ad9cda', - 'sprite-apps-large-css' => 'e6ad9cda', - 'sprite-gradient-css' => 'e6ad9cda', - 'sprite-icon-css' => 'e6ad9cda', - 'sprite-menu-css' => 'e6ad9cda', - 'syntax-highlighting-css' => 'e6ad9cda', + 'phabricator-remarkup-css' => 'a5058778', + 'phabricator-shaped-request' => 'a8e8f2b7', + 'phabricator-side-menu-view-css' => 'a5058778', + 'phabricator-standard-page-view' => 'a5058778', + 'phabricator-textareautils' => '70c8162b', + 'phabricator-tooltip' => '70c8162b', + 'phabricator-transaction-view-css' => 'a5058778', + 'sprite-apps-large-css' => 'a5058778', + 'sprite-gradient-css' => 'a5058778', + 'sprite-icon-css' => 'a5058778', + 'sprite-menu-css' => 'a5058778', + 'syntax-highlighting-css' => 'a5058778', ), )); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a2c5a03c02..1b8324a2e1 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -788,7 +788,6 @@ phutil_register_library_map(array( 'PhabricatorFileTransformController' => 'applications/files/controller/PhabricatorFileTransformController.php', 'PhabricatorFileUploadController' => 'applications/files/controller/PhabricatorFileUploadController.php', 'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php', - 'PhabricatorFileUploadView' => 'applications/files/view/PhabricatorFileUploadView.php', 'PhabricatorFilesManagementEnginesWorkflow' => 'applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php', 'PhabricatorFilesManagementMigrateWorkflow' => 'applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php', 'PhabricatorFilesManagementWorkflow' => 'applications/files/management/PhabricatorFilesManagementWorkflow.php', @@ -807,6 +806,7 @@ phutil_register_library_map(array( 'PhabricatorGarbageCollectorDaemon' => 'infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php', 'PhabricatorGitGraphStream' => 'applications/repository/daemon/PhabricatorGitGraphStream.php', 'PhabricatorGlobalLock' => 'infrastructure/util/PhabricatorGlobalLock.php', + 'PhabricatorGlobalUploadTargetView' => 'applications/files/view/PhabricatorGlobalUploadTargetView.php', 'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php', 'PhabricatorHash' => 'infrastructure/util/PhabricatorHash.php', 'PhabricatorHeaderView' => 'view/layout/PhabricatorHeaderView.php', @@ -2070,7 +2070,6 @@ phutil_register_library_map(array( 'PhabricatorFileTransformController' => 'PhabricatorFileController', 'PhabricatorFileUploadController' => 'PhabricatorFileController', 'PhabricatorFileUploadException' => 'Exception', - 'PhabricatorFileUploadView' => 'AphrontView', 'PhabricatorFilesManagementEnginesWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementMigrateWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementWorkflow' => 'PhutilArgumentWorkflow', @@ -2086,6 +2085,7 @@ phutil_register_library_map(array( 'PhabricatorFormExample' => 'PhabricatorUIExample', 'PhabricatorGarbageCollectorDaemon' => 'PhabricatorDaemon', 'PhabricatorGlobalLock' => 'PhutilLock', + 'PhabricatorGlobalUploadTargetView' => 'AphrontView', 'PhabricatorHeaderView' => 'AphrontView', 'PhabricatorHelpController' => 'PhabricatorController', 'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController', diff --git a/src/applications/directory/controller/PhabricatorDirectoryMainController.php b/src/applications/directory/controller/PhabricatorDirectoryMainController.php index 1aa2a86819..b5a5d53b51 100644 --- a/src/applications/directory/controller/PhabricatorDirectoryMainController.php +++ b/src/applications/directory/controller/PhabricatorDirectoryMainController.php @@ -76,6 +76,7 @@ final class PhabricatorDirectoryMainController ); $nav->appendChild($content); + $nav->appendChild(new PhabricatorGlobalUploadTargetView()); return $this->buildStandardPageResponse( $nav, @@ -491,7 +492,7 @@ final class PhabricatorDirectoryMainController $nav_buttons[] = array( 'Upload File', - '/file/', + '/file/upload/', 'upload-file', 'Share Files'); $nav_buttons[] = array( diff --git a/src/applications/files/controller/PhabricatorFileController.php b/src/applications/files/controller/PhabricatorFileController.php index a850f8c847..9c6a1a68b1 100644 --- a/src/applications/files/controller/PhabricatorFileController.php +++ b/src/applications/files/controller/PhabricatorFileController.php @@ -2,17 +2,43 @@ abstract class PhabricatorFileController extends PhabricatorController { - public function buildStandardPageResponse($view, array $data) { - $page = $this->buildStandardPageView(); + public function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + $crumbs->addAction( + id(new PhabricatorMenuItemView()) + ->setName(pht('Upload File')) + ->setIcon('create') // TODO: Get @chad to build an "upload" icon. + ->setHref($this->getApplicationURI('/upload/'))); - $page->setApplicationName('Files'); - $page->setBaseURI('/file/'); - $page->setTitle(idx($data, 'title')); - $page->setGlyph("\xE2\x87\xAA"); - $page->appendChild($view); - - $response = new AphrontWebpageResponse(); - return $response->setContent($page->render()); + return $crumbs; } + protected function buildSideNavView() { + $menu = $this->buildMenu($for_devices = false); + return AphrontSideNavFilterView::newFromMenu($menu); + } + + protected function buildApplicationMenu() { + return $this->buildMenu($for_devices = true); + } + + private function buildMenu($for_devices) { + $menu = new PhabricatorMenuView(); + + if ($for_devices) { + $menu->newLink(pht('Upload File'), $this->getApplicationURI('/upload/')); + } + + $menu->newLabel(pht('Filters')); + + $menu->newLink(pht('My Files'), $this->getApplicationURI('filter/my/')) + ->setKey('my'); + + $menu->newLink(pht('All Files'), $this->getApplicationURI('filter/all/')) + ->setKey('all'); + + return $menu; + } + + } diff --git a/src/applications/files/controller/PhabricatorFileListController.php b/src/applications/files/controller/PhabricatorFileListController.php index c88c9a06aa..ecdaba4d6b 100644 --- a/src/applications/files/controller/PhabricatorFileListController.php +++ b/src/applications/files/controller/PhabricatorFileListController.php @@ -1,35 +1,20 @@ filter = $filter; return $this; } + private function getFilter() { return $this->filter; } - private function useBasicUploader() { - return $this->getUseBasicUploader(); - } - private function getUseBasicUploader() { - return $this->useBasicUploader; - } - private function setUseBasicUploader($use_basic_uploader) { - $this->useBasicUploader = $use_basic_uploader; - return $this; - } - public function willProcessRequest(array $data) { - $this->setFilter(idx($data, 'filter', 'upload')); + $this->setFilter(idx($data, 'filter', 'my')); } public function processRequest() { @@ -42,26 +27,13 @@ final class PhabricatorFileListController extends PhabricatorFileController { $query = id(new PhabricatorFileQuery()) ->setViewer($user); - $show_pager = true; - $show_upload = false; - switch ($this->getFilter()) { - case 'upload': - default: - $this->setUseBasicUploader($request->getExists('basic_uploader')); - - $query->withAuthorPHIDs(array($user->getPHID())); - $pager->setPageSize(10); - - $header = pht('Recently Uploaded Files'); - $show_pager = false; - $show_upload = true; - break; case 'my': $query->withAuthorPHIDs(array($user->getPHID())); $header = pht('Files You Uploaded'); break; case 'all': + default: $header = pht('All Files'); break; } @@ -74,9 +46,6 @@ final class PhabricatorFileListController extends PhabricatorFileController { $side_nav = $this->buildSideNavView(); $side_nav->selectFilter($this->getFilter()); - if ($show_upload) { - $side_nav->appendChild($this->renderUploadPanel()); - } $header_view = id(new PhabricatorHeaderView()) ->setHeader($header); @@ -85,13 +54,23 @@ final class PhabricatorFileListController extends PhabricatorFileController { array( $header_view, $file_list, - $show_pager ? $pager : null, + $pager, + new PhabricatorGlobalUploadTargetView(), )); + $side_nav->setCrumbs( + $this + ->buildApplicationCrumbs() + ->addCrumb( + id(new PhabricatorCrumbView()) + ->setName($header) + ->setHref($request->getRequestURI()))); + return $this->buildApplicationPage( $side_nav, array( 'title' => 'Files', + 'device' => true, )); } @@ -141,66 +120,4 @@ final class PhabricatorFileListController extends PhabricatorFileController { return $list_view; } - private function buildSideNavView() { - $view = new AphrontSideNavFilterView(); - $view->setBaseURI(new PhutilURI($this->getApplicationURI('/filter/'))); - - $view->addLabel('Files'); - $view->addFilter('upload', 'Upload File'); - $view->addFilter('my', 'My Files'); - $view->addFilter('all', 'All Files'); - - return $view; - } - - private function renderUploadPanel() { - $request = $this->getRequest(); - $user = $request->getUser(); - - $limit_text = PhabricatorFileUploadView::renderUploadLimit(); - - if ($this->useBasicUploader()) { - - $upload_panel = new PhabricatorFileUploadView(); - $upload_panel->setUser($user); - - } else { - - 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->setCaption($limit_text); - $upload_panel->setCreateButton('Basic Uploader', - $request->getRequestURI()->setQueryParam('basic_uploader', true) - ); - - $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/filter/my/', - 'control' => $upload_id, - 'target' => $panel_id, - 'activatedClass' => 'aphront-panel-view-drag-and-drop', - )); - } - - return $upload_panel; - } } diff --git a/src/applications/files/controller/PhabricatorFileUploadController.php b/src/applications/files/controller/PhabricatorFileUploadController.php index 8055211119..f94e587a19 100644 --- a/src/applications/files/controller/PhabricatorFileUploadController.php +++ b/src/applications/files/controller/PhabricatorFileUploadController.php @@ -3,28 +3,114 @@ final class PhabricatorFileUploadController extends PhabricatorFileController { public function processRequest() { - $request = $this->getRequest(); $user = $request->getUser(); + $e_file = true; + $errors = array(); if ($request->isFormPost()) { - $file = PhabricatorFile::newFromPHPUpload( - idx($_FILES, 'file'), - array( - 'name' => $request->getStr('name'), - 'authorPHID' => $user->getPHID(), - )); + if (!$request->getFileExists('file')) { + $e_file = pht('Required'); + $errors[] = pht('You must select a file to upload.'); + } else { + $file = PhabricatorFile::newFromPHPUpload( + idx($_FILES, 'file'), + array( + 'name' => $request->getStr('name'), + 'authorPHID' => $user->getPHID(), + )); + } - return id(new AphrontRedirectResponse())->setURI($file->getBestURI()); + if (!$errors) { + return id(new AphrontRedirectResponse())->setURI($file->getViewURI()); + } } - $panel = new PhabricatorFileUploadView(); - $panel->setUser($user); + $support_id = celerity_generate_unique_node_id(); + $instructions = id(new AphrontFormMarkupControl()) + ->setControlID($support_id) + ->setControlStyle('display: none') + ->setValue( + '

'. + pht( + 'Drag and Drop: You can also upload files by '. + 'dragging and dropping them from your desktop onto this page or '. + 'the Phabricator home page.'). + '

'); - return $this->buildStandardPageResponse( - array($panel), + $form = id(new AphrontFormView()) + ->setFlexible(true) + ->setUser($user) + ->setEncType('multipart/form-data') + ->appendChild( + id(new AphrontFormFileControl()) + ->setLabel(pht('File')) + ->setName('file') + ->setError($e_file) + ->setCaption($this->renderUploadLimit())) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Name')) + ->setName('name') + ->setValue($request->getStr('name')) + ->setCaption('Optional file display name.')) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Upload')) + ->addCancelButton('/file/')) + ->appendChild($instructions); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName(pht('Upload')) + ->setHref($request->getRequestURI())); + + $header = id(new PhabricatorHeaderView()) + ->setHeader(pht('Upload File')); + + if ($errors) { + $errors = id(new AphrontErrorView()) + ->setTitle('Form Errors') + ->setErrors($errors); + } + + $global_upload = id(new PhabricatorGlobalUploadTargetView()) + ->setShowIfSupportedID($support_id); + + return $this->buildApplicationPage( + array( + $crumbs, + $header, + $errors, + $form, + $global_upload, + ), array( 'title' => 'Upload File', + 'device' => true, )); } + + private function renderUploadLimit() { + $limit = PhabricatorEnv::getEnvConfig('storage.upload-size-limit'); + $limit = phabricator_parse_bytes($limit); + if ($limit) { + $formatted = phabricator_format_bytes($limit); + return 'Maximum file size: '.phutil_escape_html($formatted); + } + + $doc_href = PhabricatorEnv::getDocLink( + 'article/Configuring_File_Upload_Limits.html'); + $doc_link = phutil_render_tag( + 'a', + array( + 'href' => $doc_href, + 'target' => '_blank', + ), + 'Configuring File Upload Limits'); + + return 'Upload limit is not configured, see '.$doc_link.'.'; + } + } diff --git a/src/applications/files/view/PhabricatorFileUploadView.php b/src/applications/files/view/PhabricatorFileUploadView.php deleted file mode 100644 index b9160bf278..0000000000 --- a/src/applications/files/view/PhabricatorFileUploadView.php +++ /dev/null @@ -1,73 +0,0 @@ -user; - } - public function setUser(PhabricatorUser $user) { - $this->user = $user; - return $this; - } - - public function render() { - $user = $this->getUser(); - if (!$user) { - throw new Exception("Call setUser() before render()!"); - } - - $form = new AphrontFormView(); - $form->setAction('/file/upload/'); - $form->setUser($user); - - $form - ->setEncType('multipart/form-data') - ->appendChild( - id(new AphrontFormFileControl()) - ->setLabel('File') - ->setName('file') - ->setError(true) - ->setCaption(self::renderUploadLimit())) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel('Name') - ->setName('name') - ->setCaption('Optional file display name.')) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue('Upload') - ->addCancelButton('/file/')); - - $panel = new AphrontPanelView(); - $panel->setHeader('Upload File'); - - $panel->appendChild($form); - $panel->setWidth(AphrontPanelView::WIDTH_FULL); - - return $panel->render(); - } - - public static function renderUploadLimit() { - $limit = PhabricatorEnv::getEnvConfig('storage.upload-size-limit'); - $limit = phabricator_parse_bytes($limit); - if ($limit) { - $formatted = phabricator_format_bytes($limit); - return 'Maximum file size: '.phutil_escape_html($formatted); - } - - $doc_href = PhabricatorEnv::getDocLink( - 'article/Configuring_File_Upload_Limits.html'); - $doc_link = phutil_render_tag( - 'a', - array( - 'href' => $doc_href, - 'target' => '_blank', - ), - 'Configuring File Upload Limits'); - - return 'Upload limit is not configured, see '.$doc_link.'.'; - } -} - diff --git a/src/applications/files/view/PhabricatorGlobalUploadTargetView.php b/src/applications/files/view/PhabricatorGlobalUploadTargetView.php new file mode 100644 index 0000000000..6b8a0f566a --- /dev/null +++ b/src/applications/files/view/PhabricatorGlobalUploadTargetView.php @@ -0,0 +1,37 @@ +showIfSupportedID = $show_if_supported_id; + return $this; + } + + public function getShowIfSupportedID() { + return $this->showIfSupportedID; + } + + public function render() { + $instructions_id = celerity_generate_unique_node_id(); + + require_celerity_resource('global-drag-and-drop-css'); + + Javelin::initBehavior('global-drag-and-drop', array( + 'ifSupported' => $this->showIfSupportedID, + 'instructions' => $instructions_id, + 'uploadURI' => '/file/dropupload/', + 'browseURI' => '/file/filter/my/', + )); + + return phutil_render_tag( + 'div', + array( + 'id' => $instructions_id, + 'class' => 'phabricator-global-upload-instructions', + 'style' => 'display: none;', + ), + pht("\xE2\x87\xAA Drop Files to Upload")); + } +} diff --git a/src/view/form/control/AphrontFormTextAreaControl.php b/src/view/form/control/AphrontFormTextAreaControl.php index 378fa5e8bf..55327e40cd 100644 --- a/src/view/form/control/AphrontFormTextAreaControl.php +++ b/src/view/form/control/AphrontFormTextAreaControl.php @@ -11,7 +11,6 @@ class AphrontFormTextAreaControl extends AphrontFormControl { private $height; private $readOnly; - private $enableDragAndDropFileUploads; private $customClass; public function setHeight($height) { diff --git a/src/view/layout/AphrontSideNavFilterView.php b/src/view/layout/AphrontSideNavFilterView.php index 3d7629a200..574f9f31ef 100644 --- a/src/view/layout/AphrontSideNavFilterView.php +++ b/src/view/layout/AphrontSideNavFilterView.php @@ -32,6 +32,13 @@ final class AphrontSideNavFilterView extends AphrontView { $this->menu = new PhabricatorMenuView(); } + public static function newFromMenu(PhabricatorMenuView $menu) { + $object = new AphrontSideNavFilterView(); + $object->setBaseURI(new PhutilURI('/')); + $object->menu = $menu; + return $object; + } + public function setCrumbs(PhabricatorCrumbsView $crumbs) { $this->crumbs = $crumbs; return $this; @@ -128,7 +135,7 @@ final class AphrontSideNavFilterView extends AphrontView { public function selectFilter($key, $default = null) { $this->selectedFilter = $default; - if ($this->menu->getItem($key)) { + if ($this->menu->getItem($key) && strlen($key)) { $this->selectedFilter = $key; } return $this->selectedFilter; diff --git a/src/view/layout/PhabricatorMenuView.php b/src/view/layout/PhabricatorMenuView.php index eaaa04520c..5bd799db0a 100644 --- a/src/view/layout/PhabricatorMenuView.php +++ b/src/view/layout/PhabricatorMenuView.php @@ -3,7 +3,6 @@ final class PhabricatorMenuView extends AphrontView { private $items = array(); - private $map = array(); private $classes = array(); public function addClass($class) { @@ -34,14 +33,6 @@ final class PhabricatorMenuView extends AphrontView { public function addMenuItem(PhabricatorMenuItemView $item) { $key = $item->getKey(); - if ($key !== null) { - if (isset($this->map[$key])) { - throw new Exception( - "Menu contains duplicate items with key '{$key}'!"); - } - $this->map[$key] = $item; - } - $this->items[] = $item; $this->appendChild($item); @@ -49,7 +40,19 @@ final class PhabricatorMenuView extends AphrontView { } public function getItem($key) { - return idx($this->map, (string)$key); + $key = (string)$key; + + // NOTE: We could optimize this, but need to update any map when items have + // their keys change. Since that's moderately complex, wait for a profile + // or use case. + + foreach ($this->items as $item) { + if ($item->getKey() == $key) { + return $item; + } + } + + return null; } public function getItems() { @@ -57,6 +60,18 @@ final class PhabricatorMenuView extends AphrontView { } public function render() { + $key_map = array(); + foreach ($this->items as $item) { + $key = $item->getKey(); + if ($key !== null) { + if (isset($key_map[$key])) { + throw new Exception( + "Menu contains duplicate items with key '{$key}'!"); + } + $key_map[$key] = $item; + } + } + $classes = $this->classes; $classes[] = 'phabricator-menu-view'; diff --git a/webroot/rsrc/css/application/files/files.css b/webroot/rsrc/css/application/files/files.css deleted file mode 100644 index beaff56c38..0000000000 --- a/webroot/rsrc/css/application/files/files.css +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @provides files-css - */ - -.files-drag-and-drop { - text-align: center; - padding: 0em 1em .5em; - font-size: 15px; - color: #666666; -} diff --git a/webroot/rsrc/css/application/files/global-drag-and-drop.css b/webroot/rsrc/css/application/files/global-drag-and-drop.css new file mode 100644 index 0000000000..d4c5af095f --- /dev/null +++ b/webroot/rsrc/css/application/files/global-drag-and-drop.css @@ -0,0 +1,22 @@ +/** + * @provides global-drag-and-drop-css + */ + +.phabricator-global-upload-instructions { + text-align: center; + position: fixed; + font-size: 36px; + font-weight: bold; + + margin: 0 10%; + width: 80%; + left: 0; + top: 30%; + padding: 18px 0; + + color: #ffffff; + background: rgba(0, 0, 0, 0.8); + z-index: 8; + border-radius: 18px; + +} diff --git a/webroot/rsrc/js/application/core/DragAndDropFileUpload.js b/webroot/rsrc/js/application/core/DragAndDropFileUpload.js index d77e7c2986..e3e3b69706 100644 --- a/webroot/rsrc/js/application/core/DragAndDropFileUpload.js +++ b/webroot/rsrc/js/application/core/DragAndDropFileUpload.js @@ -15,12 +15,19 @@ JX.install('PhabricatorDragAndDropFileUpload', { this._node = node; }, - events : ['willUpload', 'progress', 'didUpload', 'didError'], + events : [ + 'didBeginDrag', + 'didEndDrag', + 'willUpload', + 'progress', + 'didUpload', + 'didError'], statics : { isSupported : function() { // TODO: Is there a better capability test for this? This seems okay in // Safari, Firefox and Chrome. + return !!window.FileList; } }, @@ -29,11 +36,17 @@ JX.install('PhabricatorDragAndDropFileUpload', { _node : null, _depth : 0, _updateDepth : function(delta) { + if (this._depth == 0 && delta > 0) { + JX.log('begin: ' + this._depth + ' @ ' + delta); + this.invoke('didBeginDrag'); + } + this._depth += delta; - JX.DOM.alterClass( - this._node, - this.getActivatedClass(), - (this._depth > 0)); + + if (this._depth == 0 && delta < 0) { + JX.log('end: ' + this._depth + ' @ ' + delta); + this.invoke('didEndDrag'); + } }, start : function() { diff --git a/webroot/rsrc/js/application/core/behavior-drag-and-drop-textarea.js b/webroot/rsrc/js/application/core/behavior-drag-and-drop-textarea.js index 9ad1cd11fb..0deff3dda7 100644 --- a/webroot/rsrc/js/application/core/behavior-drag-and-drop-textarea.js +++ b/webroot/rsrc/js/application/core/behavior-drag-and-drop-textarea.js @@ -12,13 +12,29 @@ JX.behavior('aphront-drag-and-drop-textarea', function(config) { var target = JX.$(config.target); function onupload(f) { - JX.TextAreaUtils.setSelectionText(target, '{F' + f.getID() + '}'); + var text = JX.TextAreaUtils.getSelectionText(target); + var ref = '{F' + f.getID() + '}'; + + // If the user has dragged and dropped multiple files, we'll get an event + // each time an upload completes. Rather than overwriting the first + // reference, append the new reference if the selected text looks like an + // existing file reference. + if (text.match(/^\{F/)) { + ref = text + "\n\n" + ref; + } + + JX.TextAreaUtils.setSelectionText(target, ref); } if (JX.PhabricatorDragAndDropFileUpload.isSupported()) { var drop = new JX.PhabricatorDragAndDropFileUpload(target) - .setActivatedClass(config.activatedClass) .setURI(config.uri); + drop.listen('didBeginDrag', function(e) { + JX.DOM.alterClass(target, config.activatedClass, true); + }); + drop.listen('didEndDrag', function(e) { + JX.DOM.alterClass(target, config.activatedClass, false); + }); drop.listen('didUpload', onupload); drop.start(); } diff --git a/webroot/rsrc/js/application/core/behavior-drag-and-drop.js b/webroot/rsrc/js/application/core/behavior-drag-and-drop.js index 6788f38d0e..4e115e1826 100644 --- a/webroot/rsrc/js/application/core/behavior-drag-and-drop.js +++ b/webroot/rsrc/js/application/core/behavior-drag-and-drop.js @@ -2,7 +2,6 @@ * @provides javelin-behavior-aphront-drag-and-drop * @requires javelin-behavior * javelin-dom - * javelin-util * phabricator-drag-and-drop-file-upload */ @@ -23,9 +22,16 @@ JX.behavior('aphront-drag-and-drop', function(config) { var list = JX.$(config.list); var drop = new JX.PhabricatorDragAndDropFileUpload(JX.$(config.list)) - .setActivatedClass(config.activatedClass) .setURI(config.uri); + drop.listen('didBeginDrag', function(e) { + JX.DOM.alterClass(list, config.activatedClass, true); + }); + + drop.listen('didEndDrag', function(e) { + JX.DOM.alterClass(list, config.activatedClass, false); + }); + drop.listen('willUpload', function(f) { pending++; redraw(); 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 deleted file mode 100644 index b2da9efa01..0000000000 --- a/webroot/rsrc/js/application/core/behavior-files-drag-and-drop.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @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 errors = false; - - 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 && !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].getID()); - } - 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.listen('didError', function(f) { - pending--; - errors = true; - 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); - } - -}); - diff --git a/webroot/rsrc/js/application/core/behavior-global-drag-and-drop.js b/webroot/rsrc/js/application/core/behavior-global-drag-and-drop.js new file mode 100644 index 0000000000..8d4b7a443b --- /dev/null +++ b/webroot/rsrc/js/application/core/behavior-global-drag-and-drop.js @@ -0,0 +1,68 @@ +/** + * @provides javelin-behavior-global-drag-and-drop + * @requires javelin-behavior + * javelin-dom + * javelin-uri + * javelin-mask + * phabricator-drag-and-drop-file-upload + */ + +JX.behavior('global-drag-and-drop', function(config) { + if (!JX.PhabricatorDragAndDropFileUpload.isSupported()) { + return; + } + + var pending = 0; + var files = []; + var errors = false; + + if (config.ifSupported) { + JX.$(config.ifSupported).style.display = ''; + } + + var drop = new JX.PhabricatorDragAndDropFileUpload(document.documentElement) + .setURI(config.uploadURI); + + drop.listen('didBeginDrag', function(f) { + JX.Mask.show(); + JX.DOM.show(JX.$(config.instructions)); + }); + + drop.listen('didEndDrag', function(f) { + JX.Mask.hide(); + JX.DOM.hide(JX.$(config.instructions)); + }); + + drop.listen('willUpload', function(f) { + pending++; + }); + + drop.listen('didUpload', function(f) { + files.push(f); + + pending--; + 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].getID()); + } + uri.setQueryParam('h', ids.join(',')); + + files = []; + + uri.go(); + } + }); + + drop.listen('didError', function(f) { + pending--; + errors = true; + }); + + drop.start(); +}); +