mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-10 00:42:41 +01:00
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
This commit is contained in:
parent
67cb277bed
commit
f9f25c1e4d
11 changed files with 198 additions and 31 deletions
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -85,6 +85,8 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication {
|
|||
=> 'PhabricatorCalendarImportDisableController',
|
||||
'delete/(?P<id>[1-9]\d*)/'
|
||||
=> 'PhabricatorCalendarImportDeleteController',
|
||||
'drop/'
|
||||
=> 'PhabricatorCalendarImportDropController',
|
||||
'log/' => array(
|
||||
$this->getQueryRoutePattern()
|
||||
=> 'PhabricatorCalendarImportLogListController',
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorCalendarImportDropController
|
||||
extends PhabricatorCalendarController {
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$viewer = $request->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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,6 +94,4 @@ final class PhabricatorApplicationSearchResultView extends Phobject {
|
|||
return $this->header;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue