diff --git a/src/applications/calendar/application/PhabricatorCalendarApplication.php b/src/applications/calendar/application/PhabricatorCalendarApplication.php --- a/src/applications/calendar/application/PhabricatorCalendarApplication.php +++ b/src/applications/calendar/application/PhabricatorCalendarApplication.php @@ -57,7 +57,7 @@ => 'PhabricatorCalendarEventEditController', 'drag/(?P<id>[1-9]\d*)/' => 'PhabricatorCalendarEventDragController', - 'cancel/(?P<id>[1-9]\d*)/' + 'cancel/(?P<id>[1-9]\d*)/(?:(?P<sequence>\d+)/)?' => 'PhabricatorCalendarEventCancelController', '(?P<action>join|decline|accept)/(?P<id>[1-9]\d*)/' => 'PhabricatorCalendarEventJoinController', diff --git a/src/applications/calendar/controller/PhabricatorCalendarController.php b/src/applications/calendar/controller/PhabricatorCalendarController.php --- a/src/applications/calendar/controller/PhabricatorCalendarController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarController.php @@ -26,4 +26,47 @@ return $crumbs; } + protected function getEventAtIndexForGhostPHID($viewer, $phid, $index) { + $result = id(new PhabricatorCalendarEventQuery()) + ->setViewer($viewer) + ->withInstanceSequencePairs( + array( + array( + $phid, + $index, + ), + )) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + + return $result; + } + + protected function createEventFromGhost($viewer, $event, $index) { + $invitees = $event->getInvitees(); + + $new_ghost = $event->generateNthGhost($index, $viewer); + $new_ghost->attachParentEvent($event); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $new_ghost + ->setID(null) + ->setPHID(null) + ->removeViewerTimezone($viewer) + ->save(); + $ghost_invitees = array(); + foreach ($invitees as $invitee) { + $ghost_invitee = clone $invitee; + $ghost_invitee + ->setID(null) + ->setEventPHID($new_ghost->getPHID()) + ->save(); + } + unset($unguarded); + return $new_ghost; + } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php b/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php --- a/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php @@ -12,8 +12,9 @@ public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); + $sequence = $request->getURIData('sequence'); - $status = id(new PhabricatorCalendarEventQuery()) + $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( @@ -23,15 +24,40 @@ )) ->executeOne(); - if (!$status) { + if ($sequence) { + $parent_event = $event; + $event = $parent_event->generateNthGhost($sequence, $user); + $event->attachParentEvent($parent_event); + } + + if (!$event) { return new Aphront404Response(); } - $cancel_uri = '/E'.$status->getID(); + if (!$sequence) { + $cancel_uri = '/E'.$event->getID(); + } else { + $cancel_uri = '/E'.$event->getID().'/'.$sequence; + } + + $is_cancelled = $event->getIsCancelled(); + $is_parent_cancelled = $event->getIsParentCancelled(); + $is_recurring = $event->getIsRecurring(); + $instance_of = $event->getInstanceOfEventPHID(); + $validation_exception = null; - $is_cancelled = $status->getIsCancelled(); if ($request->isFormPost()) { + if ($is_cancelled && $sequence) { + return id(new AphrontRedirectResponse())->setURI($cancel_uri); + } else if ($sequence) { + $event = $this->createEventFromGhost( + $user, + $event, + $sequence); + $event->applyViewerTimezone($user); + } + $xactions = array(); $xaction = id(new PhabricatorCalendarEventTransaction()) @@ -46,7 +72,7 @@ ->setContinueOnMissingFields(true); try { - $editor->applyTransactions($status, array($xaction)); + $editor->applyTransactions($event, array($xaction)); return id(new AphrontRedirectResponse())->setURI($cancel_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; @@ -54,15 +80,44 @@ } if ($is_cancelled) { - $title = pht('Reinstate Event'); - $paragraph = pht('Reinstate this event?'); - $cancel = pht('Don\'t Reinstate Event'); - $submit = pht('Reinstate Event'); + if ($sequence || $is_parent_cancelled) { + $title = pht('Cannot Reinstate Instance'); + $paragraph = pht('Cannot reinstate an instance of a + cancelled recurring event.'); + $cancel = pht('Cancel'); + $submit = null; + } else if ($is_recurring && !$instance_of) { + $title = pht('Reinstate Recurrence'); + $paragraph = pht('Reinstate the entire series + of recurring events?'); + $cancel = pht('Don\'t Reinstate Recurrence'); + $submit = pht('Reinstate Recurrence'); + } else { + $title = pht('Reinstate Event'); + $paragraph = pht('Reinstate this event?'); + $cancel = pht('Don\'t Reinstate Event'); + $submit = pht('Reinstate Event'); + } } else { - $title = pht('Cancel Event'); - $paragraph = pht('You can always reinstate the event later.'); - $cancel = pht('Don\'t Cancel Event'); - $submit = pht('Cancel Event'); + if ($sequence) { + $title = pht('Cancel Instance'); + $paragraph = pht('Cancel just this instance + of a recurring event.'); + $cancel = pht('Don\'t Cancel Instance'); + $submit = pht('Cancel Instance'); + } else if ($is_recurring && !$instance_of) { + $title = pht('Cancel Recurrence'); + $paragraph = pht('Cancel the entire series + of recurring events?'); + $cancel = pht('Don\'t Cancel Recurrence'); + $submit = pht('Cancel Recurrence'); + } else { + $title = pht('Cancel Event'); + $paragraph = pht('You can always reinstate + the event later.'); + $cancel = pht('Don\'t Cancel Event'); + $submit = pht('Cancel Event'); + } } return $this->newDialog() diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php --- a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php @@ -78,14 +78,15 @@ $cancel_uri = $this->getApplicationURI(); } else { $event = id(new PhabricatorCalendarEventQuery()) - ->setViewer($viewer) - ->withIDs(array($this->id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$event) { return new Aphront404Response(); } @@ -93,47 +94,23 @@ if ($request->getURIData('sequence')) { $index = $request->getURIData('sequence'); - $result = id(new PhabricatorCalendarEventQuery()) - ->setViewer($viewer) - ->withInstanceSequencePairs( - array( - array( - $event->getPHID(), - $index, - ), - )) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); + $result = $this->getEventAtIndexForGhostPHID( + $viewer, + $event->getPHID(), + $index); if ($result) { return id(new AphrontRedirectResponse()) ->setURI('/calendar/event/edit/'.$result->getID().'/'); } - $invitees = $event->getInvitees(); - - $new_ghost = $event->generateNthGhost($index, $viewer); - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - $new_ghost - ->setID(null) - ->setPHID(null) - ->removeViewerTimezone($viewer) - ->save(); - $ghost_invitees = array(); - foreach ($invitees as $invitee) { - $ghost_invitee = clone $invitee; - $ghost_invitee - ->setID(null) - ->setEventPHID($new_ghost->getPHID()) - ->save(); - } - unset($unguarded); + $event = $this->createEventFromGhost( + $viewer, + $event, + $index); + return id(new AphrontRedirectResponse()) - ->setURI('/calendar/event/edit/'.$new_ghost->getID().'/'); + ->setURI('/calendar/event/edit/'.$event->getID().'/'); } $end_value = AphrontFormDateControlValue::newFromEpoch( diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php --- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php @@ -27,12 +27,25 @@ return new Aphront404Response(); } - if ($sequence && $event->getIsRecurring()) { - $parent_event = $event; - $event = $event->generateNthGhost($sequence, $viewer); - $event->attachParentEvent($parent_event); - } else if ($sequence) { - return new Aphront404Response(); + if ($sequence) { + $result = $this->getEventAtIndexForGhostPHID( + $viewer, + $event->getPHID(), + $sequence); + + if ($result) { + $parent_event = $event; + $event = $result; + $event->attachParentEvent($parent_event); + return id(new AphrontRedirectResponse()) + ->setURI('/E'.$result->getID()); + } else if ($sequence && $event->getIsRecurring()) { + $parent_event = $event; + $event = $event->generateNthGhost($sequence, $viewer); + $event->attachParentEvent($parent_event); + } else if ($sequence) { + return new Aphront404Response(); + } } $title = 'E'.$event->getID(); @@ -178,21 +191,46 @@ ->setWorkflow(true)); } + $cancel_uri = $this->getApplicationURI("event/cancel/{$id}/"); + + if ($event->getIsGhostEvent()) { + $index = $event->getSequenceIndex(); + $can_reinstate = $event->getIsParentCancelled(); + + $cancel_label = pht('Cancel This Instance'); + $reinstate_label = pht('Reinstate This Instance'); + $cancel_disabled = (!$can_edit || $can_reinstate); + $cancel_uri = $this->getApplicationURI("event/cancel/{$id}/{$index}/"); + } else if ($event->getInstanceOfEventPHID()) { + $can_reinstate = $event->getIsParentCancelled(); + $cancel_label = pht('Cancel This Instance'); + $reinstate_label = pht('Reinstate This Instance'); + $cancel_disabled = (!$can_edit || $can_reinstate); + } else if ($event->getIsRecurring()) { + $cancel_label = pht('Cancel Recurrence'); + $reinstate_label = pht('Reinstate Recurrence'); + $cancel_disabled = !$can_edit; + } else { + $cancel_label = pht('Cancel Event'); + $reinstate_label = pht('Reinstate Event'); + $cancel_disabled = !$can_edit; + } + if ($is_cancelled) { $actions->addAction( id(new PhabricatorActionView()) - ->setName(pht('Reinstate Event')) + ->setName($reinstate_label) ->setIcon('fa-plus') - ->setHref($this->getApplicationURI("event/cancel/{$id}/")) - ->setDisabled(!$can_edit) + ->setHref($cancel_uri) + ->setDisabled($cancel_disabled) ->setWorkflow(true)); } else { $actions->addAction( id(new PhabricatorActionView()) - ->setName(pht('Cancel Event')) + ->setName($cancel_label) ->setIcon('fa-times') - ->setHref($this->getApplicationURI("event/cancel/{$id}/")) - ->setDisabled(!$can_edit) + ->setHref($cancel_uri) + ->setDisabled($cancel_disabled) ->setWorkflow(true)); } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -348,6 +348,10 @@ } public function getIsParentCancelled() { + if ($this->instanceOfEventPHID == null) { + return false; + } + $recurring_event = $this->getParentEvent(); if ($recurring_event->getIsCancelled()) { return true;