From f9f25c1e4d5b3636102bca5a51297239bfedc3a5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 18 Oct 2016 14:24:17 -0700 Subject: [PATCH] Allow users to drop .ics files on calendar views to import them Summary: Ref T10747. When a user drops a ".ics" file or a bunch of ".ics" files into a calendar view, import the events. (Possibly we should just do this if you drop ".ics" files into any application, but we can look at that later.) Test Plan: Dropped some .ics files into calendar views, got imports. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16722 --- resources/celerity/map.php | 20 ++--- src/__phutil_library_map__.php | 2 + .../PhabricatorCalendarApplication.php | 2 + ...PhabricatorCalendarEventListController.php | 4 + ...habricatorCalendarImportDropController.php | 86 +++++++++++++++++++ .../PhabricatorCalendarImportEngine.php | 4 +- .../PhabricatorCalendarEventSearchEngine.php | 40 ++++++--- .../storage/PhabricatorCalendarImport.php | 1 + .../PhabricatorGlobalUploadTargetView.php | 52 ++++++++++- ...PhabricatorApplicationSearchResultView.php | 2 - .../js/core/behavior-global-drag-and-drop.js | 16 +++- 11 files changed, 198 insertions(+), 31 deletions(-) create mode 100644 src/applications/calendar/controller/PhabricatorCalendarImportDropController.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index bf8f0eed15..9f13ec0351 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'conpherence.pkg.css' => '6412a825', 'conpherence.pkg.js' => 'cbe4d9be', 'core.pkg.css' => 'b99bbf5e', - 'core.pkg.js' => '3eb7abf7', + 'core.pkg.js' => '2d9fc958', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'e1d704ce', 'differential.pkg.js' => '634399e9', @@ -555,7 +555,7 @@ return array( 'rsrc/js/core/behavior-file-tree.js' => '88236f00', 'rsrc/js/core/behavior-form.js' => '5c54cbf3', 'rsrc/js/core/behavior-gesture.js' => '3ab51e2c', - 'rsrc/js/core/behavior-global-drag-and-drop.js' => 'c8e57404', + 'rsrc/js/core/behavior-global-drag-and-drop.js' => '960f6a39', 'rsrc/js/core/behavior-high-security-warning.js' => 'a464fe03', 'rsrc/js/core/behavior-history-install.js' => '7ee2b591', 'rsrc/js/core/behavior-hovercard.js' => 'bcaccd64', @@ -701,7 +701,7 @@ return array( 'javelin-behavior-error-log' => '6882e80a', 'javelin-behavior-event-all-day' => '937bb700', 'javelin-behavior-fancy-datepicker' => '568931f3', - 'javelin-behavior-global-drag-and-drop' => 'c8e57404', + 'javelin-behavior-global-drag-and-drop' => '960f6a39', 'javelin-behavior-herald-rule-editor' => '7ebaeed3', 'javelin-behavior-high-security-warning' => 'a464fe03', 'javelin-behavior-history-install' => '7ee2b591', @@ -1735,6 +1735,13 @@ return array( 'javelin-dom', 'phabricator-busy', ), + '960f6a39' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-uri', + 'javelin-mask', + 'phabricator-drag-and-drop-file-upload', + ), '988040b4' => array( 'javelin-install', 'javelin-dom', @@ -1982,13 +1989,6 @@ return array( 'c7ccd872' => array( 'phui-fontkit-css', ), - 'c8e57404' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-uri', - 'javelin-mask', - 'phabricator-drag-and-drop-file-upload', - ), 'c90a04fc' => array( 'javelin-dom', 'javelin-dynval', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9ce3cab82a..d225434c9c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2109,6 +2109,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportDeleteTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDeleteTransaction.php', 'PhabricatorCalendarImportDisableController' => 'applications/calendar/controller/PhabricatorCalendarImportDisableController.php', 'PhabricatorCalendarImportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDisableTransaction.php', + 'PhabricatorCalendarImportDropController' => 'applications/calendar/controller/PhabricatorCalendarImportDropController.php', 'PhabricatorCalendarImportDuplicateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDuplicateLogType.php', 'PhabricatorCalendarImportEditController' => 'applications/calendar/controller/PhabricatorCalendarImportEditController.php', 'PhabricatorCalendarImportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarImportEditEngine.php', @@ -6944,6 +6945,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportDeleteTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportDisableController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportDisableTransaction' => 'PhabricatorCalendarImportTransactionType', + 'PhabricatorCalendarImportDropController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportDuplicateLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportEditController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportEditEngine' => 'PhabricatorEditEngine', diff --git a/src/applications/calendar/application/PhabricatorCalendarApplication.php b/src/applications/calendar/application/PhabricatorCalendarApplication.php index 807e2b9956..fa95f6951b 100644 --- a/src/applications/calendar/application/PhabricatorCalendarApplication.php +++ b/src/applications/calendar/application/PhabricatorCalendarApplication.php @@ -85,6 +85,8 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication { => 'PhabricatorCalendarImportDisableController', 'delete/(?P[1-9]\d*)/' => 'PhabricatorCalendarImportDeleteController', + 'drop/' + => 'PhabricatorCalendarImportDropController', 'log/' => array( $this->getQueryRoutePattern() => 'PhabricatorCalendarImportLogListController', diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventListController.php b/src/applications/calendar/controller/PhabricatorCalendarEventListController.php index cc9bed332a..711b2eeb3e 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventListController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventListController.php @@ -7,6 +7,10 @@ final class PhabricatorCalendarEventListController return true; } + public function isGlobalDragAndDropUploadEnabled() { + return true; + } + public function handleRequest(AphrontRequest $request) { $year = $request->getURIData('year'); $month = $request->getURIData('month'); diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportDropController.php b/src/applications/calendar/controller/PhabricatorCalendarImportDropController.php new file mode 100644 index 0000000000..03f515087b --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarImportDropController.php @@ -0,0 +1,86 @@ +getViewer(); + + if (!$request->validateCSRF()) { + return new Aphront400Response(); + } + + $cancel_uri = $this->getApplicationURI(); + + $ids = $request->getStrList('h'); + if ($ids) { + $files = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->setRaisePolicyExceptions(true) + ->execute(); + } else { + $files = array(); + } + + if (!$files) { + return $this->newDialog() + ->setTitle(pht('Nothing Uploaded')) + ->appendParagraph( + pht( + 'Drag and drop .ics files to upload them and import them into '. + 'Calendar.')) + ->addCancelButton($cancel_uri, pht('Done')); + } + + $engine = new PhabricatorCalendarICSImportEngine(); + $imports = array(); + foreach ($files as $file) { + $import = PhabricatorCalendarImport::initializeNewCalendarImport( + $viewer, + clone $engine); + + $xactions = array(); + $xactions[] = id(new PhabricatorCalendarImportTransaction()) + ->setTransactionType( + PhabricatorCalendarImportICSFileTransaction::TRANSACTIONTYPE) + ->setNewValue($file->getPHID()); + + $editor = id(new PhabricatorCalendarImportEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($import, $xactions); + + $imports[] = $import; + } + + $import_phids = mpull($imports, 'getPHID'); + $events = id(new PhabricatorCalendarEventQuery()) + ->setViewer($viewer) + ->withImportSourcePHIDs($import_phids) + ->execute(); + + if (count($events) == 1) { + // The user imported exactly one event. This is consistent with dropping + // a .ics file from an email; just take them to the event. + $event = head($events); + $next_uri = $event->getURI(); + } else if (count($imports) > 1) { + // The user imported multiple different files. Take them to a summary + // list of generated import activity. + $source_phids = implode(',', $import_phids); + $next_uri = '/calendar/import/log/?importSourcePHIDs='.$source_phids; + } else { + // The user imported one file, which had zero or more than one event. + // Take them to the import detail page. + $import = head($imports); + $next_uri = $import->getURI(); + } + + return id(new AphrontRedirectResponse())->setURI($next_uri); + } + +} diff --git a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php index 25eb65a720..e9270b3d41 100644 --- a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php @@ -390,9 +390,9 @@ abstract class PhabricatorCalendarImportEngine if ($rrule) { $event->setRecurrenceRule($rrule); - $until_datetime = $rrule->getUntil() - ->setViewerTimezone($timezone); + $until_datetime = $rrule->getUntil(); if ($until_datetime) { + $until_datetime->setViewerTimezone($timezone); $event->setUntilDateTime($until_datetime); } } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index 8d55da994a..26c3dc938b 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -321,11 +321,9 @@ final class PhabricatorCalendarEventSearchEngine $list->addItem($item); } - $result = new PhabricatorApplicationSearchResultView(); - $result->setObjectList($list); - $result->setNoDataString(pht('No events found.')); - - return $result; + return $this->newResultView() + ->setObjectList($list) + ->setNoDataString(pht('No events found.')); } private function buildCalendarMonthView( @@ -393,10 +391,9 @@ final class PhabricatorCalendarEventSearchEngine ->setProfileHeader(true) ->setHeader($from->format('F Y')); - return id(new PhabricatorApplicationSearchResultView()) + return $this->newResultView($month_view) ->setCrumbs($crumbs) - ->setHeader($header) - ->setContent($month_view); + ->setHeader($header); } private function buildCalendarDayView( @@ -467,10 +464,9 @@ final class PhabricatorCalendarEventSearchEngine ->setProfileHeader(true) ->setHeader($from->format('D, F jS')); - return id(new PhabricatorApplicationSearchResultView()) + return $this->newResultView($day_view) ->setCrumbs($crumbs) - ->setHeader($header) - ->setContent($day_view); + ->setHeader($header); } private function getDisplayYearAndMonthAndDay( @@ -596,4 +592,26 @@ final class PhabricatorCalendarEventSearchEngine ); } + + private function newResultView($content = null) { + // If we aren't rendering a dashboard panel, activate global drag-and-drop + // so you can import ".ics" files by dropping them directly onto the + // calendar. + if (!$this->isPanelContext()) { + $drop_upload = id(new PhabricatorGlobalUploadTargetView()) + ->setViewer($this->requireViewer()) + ->setHintText("\xE2\x87\xAA ".pht('Drop .ics Files to Import')) + ->setSubmitURI('/calendar/import/drop/') + ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE); + + $content = array( + $drop_upload, + $content, + ); + } + + return id(new PhabricatorApplicationSearchResultView()) + ->setContent($content); + } + } diff --git a/src/applications/calendar/storage/PhabricatorCalendarImport.php b/src/applications/calendar/storage/PhabricatorCalendarImport.php index b1d0ddf58b..93b0897ee5 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarImport.php +++ b/src/applications/calendar/storage/PhabricatorCalendarImport.php @@ -21,6 +21,7 @@ final class PhabricatorCalendarImport PhabricatorUser $actor, PhabricatorCalendarImportEngine $engine) { return id(new self()) + ->setName('') ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($actor->getPHID()) ->setEditPolicy($actor->getPHID()) diff --git a/src/applications/files/view/PhabricatorGlobalUploadTargetView.php b/src/applications/files/view/PhabricatorGlobalUploadTargetView.php index 3e0690815c..76aacbe36f 100644 --- a/src/applications/files/view/PhabricatorGlobalUploadTargetView.php +++ b/src/applications/files/view/PhabricatorGlobalUploadTargetView.php @@ -14,6 +14,9 @@ final class PhabricatorGlobalUploadTargetView extends AphrontView { private $showIfSupportedID; + private $hintText; + private $viewPolicy; + private $submitURI; public function setShowIfSupportedID($show_if_supported_id) { $this->showIfSupportedID = $show_if_supported_id; @@ -24,8 +27,37 @@ final class PhabricatorGlobalUploadTargetView extends AphrontView { return $this->showIfSupportedID; } + public function setHintText($hint_text) { + $this->hintText = $hint_text; + return $this; + } + + public function getHintText() { + return $this->hintText; + } + + public function setViewPolicy($view_policy) { + $this->viewPolicy = $view_policy; + return $this; + } + + public function getViewPolicy() { + return $this->viewPolicy; + } + + public function setSubmitURI($submit_uri) { + $this->submitURI = $submit_uri; + return $this; + } + + public function getSubmitURI() { + return $this->submitURI; + } + + + public function render() { - $viewer = $this->getUser(); + $viewer = $this->getViewer(); if (!$viewer->isLoggedIn()) { return null; } @@ -34,18 +66,30 @@ final class PhabricatorGlobalUploadTargetView extends AphrontView { require_celerity_resource('global-drag-and-drop-css'); + $hint_text = $this->getHintText(); + if (!strlen($hint_text)) { + $hint_text = "\xE2\x87\xAA ".pht('Drop Files to Upload'); + } + // Use the configured default view policy. Drag and drop uploads use // a more restrictive view policy if we don't specify a policy explicitly, // as the more restrictive policy is correct for most drop targets (like // Pholio uploads and Remarkup text areas). - $view_policy = PhabricatorFile::initializeNewFile()->getViewPolicy(); + $view_policy = $this->getViewPolicy(); + if ($view_policy === null) { + $view_policy = PhabricatorFile::initializeNewFile()->getViewPolicy(); + } + + $submit_uri = $this->getSubmitURI(); + $done_uri = '/file/query/authored/'; Javelin::initBehavior('global-drag-and-drop', array( 'ifSupported' => $this->showIfSupportedID, 'instructions' => $instructions_id, 'uploadURI' => '/file/dropupload/', - 'browseURI' => '/file/query/authored/', + 'submitURI' => $submit_uri, + 'browseURI' => $done_uri, 'viewPolicy' => $view_policy, 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), )); @@ -57,6 +101,6 @@ final class PhabricatorGlobalUploadTargetView extends AphrontView { 'class' => 'phabricator-global-upload-instructions', 'style' => 'display: none;', ), - "\xE2\x87\xAA ".pht('Drop Files to Upload')); + $hint_text); } } diff --git a/src/applications/search/view/PhabricatorApplicationSearchResultView.php b/src/applications/search/view/PhabricatorApplicationSearchResultView.php index 1c3f4aad65..3576d5238e 100644 --- a/src/applications/search/view/PhabricatorApplicationSearchResultView.php +++ b/src/applications/search/view/PhabricatorApplicationSearchResultView.php @@ -94,6 +94,4 @@ final class PhabricatorApplicationSearchResultView extends Phobject { return $this->header; } - - } diff --git a/webroot/rsrc/js/core/behavior-global-drag-and-drop.js b/webroot/rsrc/js/core/behavior-global-drag-and-drop.js index a291772b6e..08b0fd1226 100644 --- a/webroot/rsrc/js/core/behavior-global-drag-and-drop.js +++ b/webroot/rsrc/js/core/behavior-global-drag-and-drop.js @@ -70,7 +70,14 @@ JX.behavior('global-drag-and-drop', function(config, statics) { // If whatever the user dropped in has finished uploading, send them to // their uploads. var uri; - uri = JX.$U(config.browseURI); + var is_submit = !!config.submitURI; + + if (is_submit) { + uri = JX.$U(config.submitURI); + } else { + uri = JX.$U(config.browseURI); + } + var ids = []; for (var ii = 0; ii < statics.files.length; ii++) { ids.push(statics.files[ii].getID()); @@ -79,7 +86,12 @@ JX.behavior('global-drag-and-drop', function(config, statics) { statics.files = []; - uri.go(); + if (is_submit) { + new JX.Workflow(uri) + .start(); + } else { + uri.go(); + } } });