@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

When users edit recurring events, prompt to "Edit This Event" or "Edit All Future Events"

Summary:
Fixes T11804. This probably isn't perfect but seems to work fairly reasonably and not be as much of a weird nonsense mess like the old behavior was.

When a user edits a recurring event, we ask them what they're trying to do. Then we more or less do that.

Test Plan:
- Edited an event in the middle of a series.
- Edited the first event in a series.
- Edited "just this" and "all future" events in various places in a series.
- Edited normal events.
- Cancelled various events.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T11804

Differential Revision: https://secure.phabricator.com/D16782

+233 -64
+2 -53
src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php
··· 48 48 // are cancelling a child and all future events. 49 49 $must_fork = ($is_child && $is_future) || 50 50 ($is_parent && !$is_future); 51 - 52 51 if ($must_fork) { 53 - if ($is_child) { 54 - $fork_target = $event; 55 - } else { 56 - if ($event->isValidSequenceIndex($viewer, 1)) { 57 - $next_event = id(new PhabricatorCalendarEventQuery()) 58 - ->setViewer($viewer) 59 - ->withInstanceSequencePairs( 60 - array( 61 - array($event->getPHID(), 1), 62 - )) 63 - ->requireCapabilities( 64 - array( 65 - PhabricatorPolicyCapability::CAN_VIEW, 66 - PhabricatorPolicyCapability::CAN_EDIT, 67 - )) 68 - ->executeOne(); 69 - 70 - if (!$next_event) { 71 - $next_event = $event->newStub($viewer, 1); 72 - } 73 - 74 - $fork_target = $next_event; 75 - } else { 76 - // This appears to be a "recurring" event with no valid 77 - // instances: for example, its "until" date is before the second 78 - // instance would occur. This can happen if we already forked the 79 - // event or if users entered silly stuff. Just edit the event 80 - // directly without forking anything. 81 - $fork_target = null; 82 - } 83 - } 84 - 52 + $fork_target = $event->loadForkTarget($viewer); 85 53 if ($fork_target) { 86 54 $xactions = array(); 87 55 ··· 101 69 } 102 70 103 71 if ($is_future) { 104 - // NOTE: If you can't edit some of the future events, we just 105 - // don't try to update them. This seems like it's probably what 106 - // users are likely to expect. 107 - 108 - // NOTE: This only affects events that are currently in the same 109 - // series, not all events that were ever in the original series. 110 - // We could use series PHIDs instead of parent PHIDs to affect more 111 - // events if this turns out to be counterintuitive. Other 112 - // applications differ in their behavior. 113 - 114 - $future = id(new PhabricatorCalendarEventQuery()) 115 - ->setViewer($viewer) 116 - ->withParentEventPHIDs(array($event->getPHID())) 117 - ->withUTCInitialEpochBetween($event->getUTCInitialEpoch(), null) 118 - ->requireCapabilities( 119 - array( 120 - PhabricatorPolicyCapability::CAN_VIEW, 121 - PhabricatorPolicyCapability::CAN_EDIT, 122 - )) 123 - ->execute(); 72 + $future = $event->loadFutureEvents($viewer); 124 73 foreach ($future as $future_event) { 125 74 $targets[] = $future_event; 126 75 }
+52 -3
src/applications/calendar/controller/PhabricatorCalendarEventEditController.php
··· 6 6 public function handleRequest(AphrontRequest $request) { 7 7 $viewer = $this->getViewer(); 8 8 9 + $engine = id(new PhabricatorCalendarEventEditEngine()) 10 + ->setController($this); 11 + 9 12 $id = $request->getURIData('id'); 10 13 if ($id) { 11 14 $event = id(new PhabricatorCalendarEventQuery()) ··· 16 19 if ($response) { 17 20 return $response; 18 21 } 22 + 23 + $cancel_uri = $event->getURI(); 24 + 25 + $page = $request->getURIData('pageKey'); 26 + if ($page == 'recurring') { 27 + if ($event->isChildEvent()) { 28 + return $this->newDialog() 29 + ->setTitle(pht('Series Event')) 30 + ->appendParagraph( 31 + pht( 32 + 'This event is an instance in an event series. To change '. 33 + 'the behavior for the series, edit the parent event.')) 34 + ->addCancelButton($cancel_uri); 35 + } 36 + } else if ($event->getIsRecurring()) { 37 + $mode = $request->getStr('mode'); 38 + if (!$mode) { 39 + $form = id(new AphrontFormView()) 40 + ->setViewer($viewer) 41 + ->appendControl( 42 + id(new AphrontFormSelectControl()) 43 + ->setLabel(pht('Edit Events')) 44 + ->setName('mode') 45 + ->setOptions( 46 + array( 47 + PhabricatorCalendarEventEditEngine::MODE_THIS 48 + => pht('Edit Only This Event'), 49 + PhabricatorCalendarEventEditEngine::MODE_FUTURE 50 + => pht('Edit All Future Events'), 51 + ))); 52 + 53 + return $this->newDialog() 54 + ->setTitle(pht('Edit Event')) 55 + ->appendParagraph( 56 + pht( 57 + 'This event is part of a series. Which events do you '. 58 + 'want to edit?')) 59 + ->appendForm($form) 60 + ->addSubmitButton(pht('Continue')) 61 + ->addCancelButton($cancel_uri) 62 + ->setDisableWorkflowOnSubmit(true); 63 + 64 + } 65 + 66 + $engine 67 + ->addContextParameter('mode', $mode) 68 + ->setSeriesEditMode($mode); 69 + } 19 70 } 20 71 21 - return id(new PhabricatorCalendarEventEditEngine()) 22 - ->setController($this) 23 - ->buildResponse(); 72 + return $engine->buildResponse(); 24 73 } 25 74 26 75 }
+3 -7
src/applications/calendar/controller/PhabricatorCalendarEventViewController.php
··· 147 147 148 148 $edit_uri = "event/edit/{$id}/"; 149 149 $edit_uri = $this->getApplicationURI($edit_uri); 150 - 151 - if ($event->isChildEvent()) { 152 - $edit_label = pht('Edit This Instance'); 153 - } else { 154 - $edit_label = pht('Edit Event'); 155 - } 150 + $is_recurring = $event->getIsRecurring(); 151 + $edit_label = pht('Edit Event'); 156 152 157 153 $curtain = $this->newCurtainView($event); 158 154 ··· 163 159 ->setIcon('fa-pencil') 164 160 ->setHref($edit_uri) 165 161 ->setDisabled(!$can_edit) 166 - ->setWorkflow(!$can_edit)); 162 + ->setWorkflow(!$can_edit || $is_recurring)); 167 163 } 168 164 169 165 $recurring_uri = "{$edit_uri}page/recurring/";
+93
src/applications/calendar/editor/PhabricatorCalendarEventEditEngine.php
··· 5 5 6 6 const ENGINECONST = 'calendar.event'; 7 7 8 + private $rawTransactions; 9 + private $seriesEditMode = self::MODE_THIS; 10 + 11 + const MODE_THIS = 'this'; 12 + const MODE_FUTURE = 'future'; 13 + 14 + public function setSeriesEditMode($series_edit_mode) { 15 + $this->seriesEditMode = $series_edit_mode; 16 + return $this; 17 + } 18 + 19 + public function getSeriesEditMode() { 20 + return $this->seriesEditMode; 21 + } 22 + 8 23 public function getEngineName() { 9 24 return pht('Calendar Events'); 10 25 } ··· 77 92 $frequency = null; 78 93 } 79 94 95 + // At least for now, just hide "Invitees" when editing all future events. 96 + // This may eventually deserve a more nuanced approach. 97 + $hide_invitees = ($this->getSeriesEditMode() == self::MODE_FUTURE); 98 + 80 99 $fields = array( 81 100 id(new PhabricatorTextEditField()) 82 101 ->setKey('name') ··· 143 162 ->setConduitTypeDescription(pht('New event host.')) 144 163 ->setSingleValue($object->getHostPHID()), 145 164 id(new PhabricatorDatasourceEditField()) 165 + ->setIsHidden($hide_invitees) 146 166 ->setKey('inviteePHIDs') 147 167 ->setAliases(array('invite', 'invitee', 'invitees', 'inviteePHID')) 148 168 ->setLabel(pht('Invitees')) ··· 271 291 'until', 272 292 )), 273 293 ); 294 + } 295 + 296 + protected function willApplyTransactions($object, array $xactions) { 297 + $viewer = $this->getViewer(); 298 + $this->rawTransactions = $xactions; 299 + 300 + $is_parent = $object->isParentEvent(); 301 + $is_child = $object->isChildEvent(); 302 + $is_future = ($this->getSeriesEditMode() === self::MODE_FUTURE); 303 + 304 + $must_fork = ($is_child && $is_future) || 305 + ($is_parent && !$is_future); 306 + 307 + if ($must_fork) { 308 + $fork_target = $object->loadForkTarget($viewer); 309 + if ($fork_target) { 310 + $fork_xaction = id(new PhabricatorCalendarEventTransaction()) 311 + ->setTransactionType( 312 + PhabricatorCalendarEventForkTransaction::TRANSACTIONTYPE) 313 + ->setNewValue(true); 314 + 315 + if ($fork_target->getPHID() == $object->getPHID()) { 316 + // We're forking the object itself, so just slip it into the 317 + // transactions we're going to apply. 318 + array_unshift($xactions, $fork_xaction); 319 + } else { 320 + // Otherwise, we're forking a different object, so we have to 321 + // apply that separately. 322 + $this->applyTransactions($fork_target, array($fork_xaction)); 323 + } 324 + } 325 + } 326 + 327 + return $xactions; 328 + } 329 + 330 + protected function didApplyTransactions($object, array $xactions) { 331 + $viewer = $this->getViewer(); 332 + 333 + if ($this->getSeriesEditMode() !== self::MODE_FUTURE) { 334 + return; 335 + } 336 + 337 + $targets = $object->loadFutureEvents($viewer); 338 + if (!$targets) { 339 + return; 340 + } 341 + 342 + foreach ($targets as $target) { 343 + $apply = clone $this->rawTransactions; 344 + $this->applyTransactions($target, $apply); 345 + } 346 + } 347 + 348 + private function applyTransactions($target, array $xactions) { 349 + $viewer = $this->getViewer(); 350 + 351 + // TODO: This isn't the most accurate source we could use, but this mode 352 + // is web-only for now. 353 + $content_source = PhabricatorContentSource::newForSource( 354 + PhabricatorWebContentSource::SOURCECONST); 355 + 356 + $editor = id(new PhabricatorCalendarEventEditor()) 357 + ->setActor($viewer) 358 + ->setContentSource($content_source) 359 + ->setContinueOnNoEffect(true) 360 + ->setContinueOnMissingFields(true); 361 + 362 + try { 363 + $editor->applyTransactions($target, $xactions); 364 + } catch (PhabricatorApplicationTransactionValidationException $ex) { 365 + // Just ignore any issues we run into. 366 + } 274 367 } 275 368 276 369 }
+64
src/applications/calendar/storage/PhabricatorCalendarEvent.php
··· 154 154 ->setSequenceIndex($sequence) 155 155 ->setIsRecurring(true) 156 156 ->attachParentEvent($this) 157 + ->attachImportSource(null) 157 158 ->setAllDayDateFrom(0) 158 159 ->setAllDayDateTo(0) 159 160 ->setDateFrom(0) ··· 1058 1059 PhabricatorCalendarImport $import = null) { 1059 1060 $this->importSource = $import; 1060 1061 return $this; 1062 + } 1063 + 1064 + public function loadForkTarget(PhabricatorUser $viewer) { 1065 + if (!$this->getIsRecurring()) { 1066 + // Can't fork an event which isn't recurring. 1067 + return null; 1068 + } 1069 + 1070 + if ($this->isChildEvent()) { 1071 + // If this is a child event, this is the fork target. 1072 + return $this; 1073 + } 1074 + 1075 + if (!$this->isValidSequenceIndex($viewer, 1)) { 1076 + // This appears to be a "recurring" event with no valid instances: for 1077 + // example, its "until" date is before the second instance would occur. 1078 + // This can happen if we already forked the event or if users entered 1079 + // silly stuff. Just edit the event directly without forking anything. 1080 + return null; 1081 + } 1082 + 1083 + 1084 + $next_event = id(new PhabricatorCalendarEventQuery()) 1085 + ->setViewer($viewer) 1086 + ->withInstanceSequencePairs( 1087 + array( 1088 + array($this->getPHID(), 1), 1089 + )) 1090 + ->requireCapabilities( 1091 + array( 1092 + PhabricatorPolicyCapability::CAN_VIEW, 1093 + PhabricatorPolicyCapability::CAN_EDIT, 1094 + )) 1095 + ->executeOne(); 1096 + 1097 + if (!$next_event) { 1098 + $next_event = $this->newStub($viewer, 1); 1099 + } 1100 + 1101 + return $next_event; 1102 + } 1103 + 1104 + public function loadFutureEvents(PhabricatorUser $viewer) { 1105 + // NOTE: If you can't edit some of the future events, we just 1106 + // don't try to update them. This seems like it's probably what 1107 + // users are likely to expect. 1108 + 1109 + // NOTE: This only affects events that are currently in the same 1110 + // series, not all events that were ever in the original series. 1111 + // We could use series PHIDs instead of parent PHIDs to affect more 1112 + // events if this turns out to be counterintuitive. Other 1113 + // applications differ in their behavior. 1114 + 1115 + return id(new PhabricatorCalendarEventQuery()) 1116 + ->setViewer($viewer) 1117 + ->withParentEventPHIDs(array($this->getPHID())) 1118 + ->withUTCInitialEpochBetween($this->getUTCInitialEpoch(), null) 1119 + ->requireCapabilities( 1120 + array( 1121 + PhabricatorPolicyCapability::CAN_VIEW, 1122 + PhabricatorPolicyCapability::CAN_EDIT, 1123 + )) 1124 + ->execute(); 1061 1125 } 1062 1126 1063 1127
+8 -1
src/applications/calendar/xaction/PhabricatorCalendarEventForkTransaction.php
··· 33 33 $object->setSequenceIndex(0); 34 34 35 35 // Stop the parent event from recurring after the start date of this event. 36 - $parent->setUntilDateTime($object->newStartDateTime()); 36 + // Since the "until" time is inclusive, rewind it by one second. We could 37 + // figure out the previous instance's time instead or use a COUNT, but this 38 + // seems simpler as long as it doesn't cause any issues. 39 + $until_cutoff = $object->newStartDateTime() 40 + ->newRelativeDateTime('-PT1S') 41 + ->newAbsoluteDateTime(); 42 + 43 + $parent->setUntilDateTime($until_cutoff); 37 44 $parent->save(); 38 45 39 46 // NOTE: If we implement "COUNT" on editable events, we need to adjust
+11
src/applications/transactions/editengine/PhabricatorEditEngine.php
··· 996 996 ->setContinueOnNoEffect(true); 997 997 998 998 try { 999 + $xactions = $this->willApplyTransactions($object, $xactions); 999 1000 1000 1001 $editor->applyTransactions($object, $xactions); 1002 + 1003 + $this->didApplyTransactions($object, $xactions); 1001 1004 1002 1005 return $this->newEditResponse($request, $object, $xactions); 1003 1006 } catch (PhabricatorApplicationTransactionValidationException $ex) { ··· 2174 2177 2175 2178 $selected_key = $page->getKey(); 2176 2179 return $page_map[$selected_key]; 2180 + } 2181 + 2182 + protected function willApplyTransactions($object, array $xactions) { 2183 + return $xactions; 2184 + } 2185 + 2186 + protected function didApplyTransactions($object, array $xactions) { 2187 + return; 2177 2188 } 2178 2189 2179 2190