diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '68d4f4fb', + 'core.pkg.css' => 'e2460e8f', 'core.pkg.js' => '3bbe23c6', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '30602b8c', @@ -135,7 +135,7 @@ 'rsrc/css/phui/phui-feed-story.css' => 'c9f3a0b5', 'rsrc/css/phui/phui-fontkit.css' => 'dd8ddf27', 'rsrc/css/phui/phui-form-view.css' => '808329f2', - 'rsrc/css/phui/phui-form.css' => 'f535f938', + 'rsrc/css/phui/phui-form.css' => '25876baf', 'rsrc/css/phui/phui-header-view.css' => '75aaf372', 'rsrc/css/phui/phui-icon.css' => 'bc766998', 'rsrc/css/phui/phui-image-mask.css' => '5a8b09c8', @@ -333,6 +333,7 @@ 'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18', 'rsrc/js/application/calendar/behavior-day-view.js' => '5c46cff2', 'rsrc/js/application/calendar/behavior-event-all-day.js' => '38dcf3c8', + 'rsrc/js/application/calendar/behavior-recurring-edit.js' => '85c73ceb', 'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408', 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '01774ab2', 'rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js' => 'cf86d16a', @@ -629,6 +630,7 @@ 'javelin-behavior-project-boards' => 'ba4fa35c', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', + 'javelin-behavior-recurring-edit' => '85c73ceb', 'javelin-behavior-refresh-csrf' => '7814b593', 'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf', 'javelin-behavior-releeph-request-state-change' => 'a0b57eb8', @@ -775,7 +777,7 @@ 'phui-feed-story-css' => 'c9f3a0b5', 'phui-font-icon-base-css' => '3dad2ae3', 'phui-fontkit-css' => 'dd8ddf27', - 'phui-form-css' => 'f535f938', + 'phui-form-css' => '25876baf', 'phui-form-view-css' => '808329f2', 'phui-header-view-css' => '75aaf372', 'phui-icon-view-css' => 'bc766998', diff --git a/resources/sql/autopatches/20150527.calendar.recurringevents.sql b/resources/sql/autopatches/20150527.calendar.recurringevents.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20150527.calendar.recurringevents.sql @@ -0,0 +1,17 @@ +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD isRecurring BOOL NOT NULL; + +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD recurrenceFrequency LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL; + +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD recurrenceEndDate INT UNSIGNED; + +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD instanceOfEventPHID varbinary(64); + +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD sequenceIndex INT UNSIGNED; + +UPDATE {$NAMESPACE}_calendar.calendar_event + SET recurrenceFrequency = '[]' WHERE recurrenceFrequency = ''; 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 @@ -40,7 +40,8 @@ public function getRoutes() { return array( - '/E(?P[1-9]\d*)' => 'PhabricatorCalendarEventViewController', + '/E(?P[1-9]\d*)(?:/(?P\d+))?' + => 'PhabricatorCalendarEventViewController', '/calendar/' => array( '(?:query/(?P[^/]+)/(?:(?P\d+)/'. '(?P\d+)/)?(?:(?P\d+)/)?)?' 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 @@ -21,9 +21,11 @@ $error_end_date = true; $validation_exception = null; + $is_recurring_id = celerity_generate_unique_node_id(); + $frequency_id = celerity_generate_unique_node_id(); $all_day_id = celerity_generate_unique_node_id(); $start_date_id = celerity_generate_unique_node_id(); - $end_date_id = null; + $end_date_id = celerity_generate_unique_node_id(); $next_workflow = $request->getStr('next'); $uri_query = $request->getStr('query'); @@ -70,7 +72,6 @@ $subscribers = array(); $invitees = array($user_phid); $cancel_uri = $this->getApplicationURI(); - $end_date_id = celerity_generate_unique_node_id(); } else { $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) @@ -113,6 +114,8 @@ $name = $event->getName(); $description = $event->getDescription(); $is_all_day = $event->getIsAllDay(); + $is_recurring = $event->getIsRecurring(); + $frequency = idx($event->getRecurrenceFrequency(), 'rule'); $icon = $event->getIcon(); $current_policies = id(new PhabricatorPolicyQuery()) @@ -134,6 +137,8 @@ $subscribers = $request->getArr('subscribers'); $edit_policy = $request->getStr('editPolicy'); $view_policy = $request->getStr('viewPolicy'); + $is_recurring = $request->getStr('isRecurring') ? 1 : 0; + $frequency = $request->getStr('frequency'); $is_all_day = $request->getStr('isAllDay'); $icon = $request->getStr('icon'); @@ -154,6 +159,16 @@ $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( + PhabricatorCalendarEventTransaction::TYPE_RECURRING) + ->setNewValue($is_recurring); + + $xactions[] = id(new PhabricatorCalendarEventTransaction()) + ->setTransactionType( + PhabricatorCalendarEventTransaction::TYPE_FREQUENCY) + ->setNewValue(array('rule' => $frequency)); + + $xactions[] = id(new PhabricatorCalendarEventTransaction()) + ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_ALL_DAY) ->setNewValue($is_all_day); @@ -233,18 +248,44 @@ } } - Javelin::initBehavior('event-all-day', array( - 'allDayID' => $all_day_id, - 'startDateID' => $start_date_id, - 'endDateID' => $end_date_id, - )); - $name = id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($name) ->setError($error_name); + Javelin::initBehavior('recurring-edit', array( + 'isRecurring' => $is_recurring_id, + 'frequency' => $frequency_id, + )); + + $is_recurring_checkbox = id(new AphrontFormCheckboxControl()) + ->addCheckbox( + 'isRecurring', + 1, + pht('Recurring Event'), + $is_recurring, + $is_recurring_id); + + $recurrence_frequency_select = id(new AphrontFormSelectControl()) + ->setName('frequency') + ->setOptions(array( + 'daily' => pht('Daily'), + 'weekly' => pht('Weekly'), + 'monthly' => pht('Monthly'), + 'yearly' => pht('Yearly'), + )) + ->setValue($frequency) + ->setLabel(pht('Recurring Event Frequency')) + ->setID($frequency_id) + ->setDisabled(!$is_recurring); + + Javelin::initBehavior('event-all-day', array( + 'allDayID' => $all_day_id, + 'startDateID' => $start_date_id, + 'endDateID' => $end_date_id, + )); + $all_day_checkbox = id(new AphrontFormCheckboxControl()) ->addCheckbox( 'isAllDay', @@ -323,6 +364,8 @@ ->addHiddenInput('query', $uri_query) ->setUser($viewer) ->appendChild($name) + ->appendChild($is_recurring_checkbox) + ->appendChild($recurrence_frequency_select) ->appendChild($all_day_checkbox) ->appendChild($start_control) ->appendChild($end_control) 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 @@ -17,6 +17,8 @@ $request = $this->getRequest(); $viewer = $request->getUser(); + $sequence = $request->getURIData('sequence'); + $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) @@ -25,6 +27,12 @@ return new Aphront404Response(); } + if ($sequence && $event->getIsRecurring()) { + $event = $event->generateNthGhost($sequence, $viewer); + } else if ($sequence) { + return new Aphront404Response(); + } + $title = 'E'.$event->getID(); $page_title = $title.' '.$event->getName(); $crumbs = $this->buildApplicationCrumbs(); @@ -127,13 +135,15 @@ $event, PhabricatorPolicyCapability::CAN_EDIT); - $actions->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Event')) - ->setIcon('fa-pencil') - ->setHref($this->getApplicationURI("event/edit/{$id}/")) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)); + if (!$event->getIsGhostEvent()) { + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Event')) + ->setIcon('fa-pencil') + ->setHref($this->getApplicationURI("event/edit/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + } if ($is_attending) { $actions->addAction( @@ -205,6 +215,12 @@ phabricator_datetime($event->getDateTo(), $viewer)); } + if ($event->getIsRecurring()) { + $properties->addProperty( + pht('Recurs'), + idx($event->getRecurrenceFrequency(), 'rule')); + } + $properties->addProperty( pht('Host'), $viewer->renderHandle($event->getUserPHID())); diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php --- a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php @@ -23,6 +23,12 @@ $types[] = PhabricatorCalendarEventTransaction::TYPE_ALL_DAY; $types[] = PhabricatorCalendarEventTransaction::TYPE_ICON; + $types[] = PhabricatorCalendarEventTransaction::TYPE_RECURRING; + $types[] = PhabricatorCalendarEventTransaction::TYPE_FREQUENCY; + $types[] = PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE; + $types[] = PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT; + $types[] = PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX; + $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; @@ -34,6 +40,16 @@ PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { + case PhabricatorCalendarEventTransaction::TYPE_RECURRING: + return $object->getIsRecurring(); + case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: + return $object->getRecurrenceFrequency(); + case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: + return $object->getRecurrenceEndDate(); + case PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT: + return $object->getInstanceOfEventPHID(); + case PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX: + return $object->getSequenceIndex(); case PhabricatorCalendarEventTransaction::TYPE_NAME: return $object->getName(); case PhabricatorCalendarEventTransaction::TYPE_START_DATE: @@ -72,6 +88,11 @@ PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { + case PhabricatorCalendarEventTransaction::TYPE_RECURRING: + case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: + case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: + case PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT: + case PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX: case PhabricatorCalendarEventTransaction::TYPE_NAME: case PhabricatorCalendarEventTransaction::TYPE_DESCRIPTION: case PhabricatorCalendarEventTransaction::TYPE_CANCEL: @@ -93,6 +114,16 @@ PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { + case PhabricatorCalendarEventTransaction::TYPE_RECURRING: + return $object->setIsRecurring($xaction->getNewValue()); + case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: + return $object->setRecurrenceFrequency($xaction->getNewValue()); + case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: + return $object->setRecurrenceEndDate($xaction->getNewValue()); + case PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT: + return $object->setInstanceOfEventPHID($xaction->getNewValue()); + case PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX: + return $object->setSequenceIndex($xaction->getNewValue()); case PhabricatorCalendarEventTransaction::TYPE_NAME: $object->setName($xaction->getNewValue()); return; @@ -126,6 +157,11 @@ PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { + case PhabricatorCalendarEventTransaction::TYPE_RECURRING: + case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: + case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: + case PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT: + case PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX: case PhabricatorCalendarEventTransaction::TYPE_NAME: case PhabricatorCalendarEventTransaction::TYPE_START_DATE: case PhabricatorCalendarEventTransaction::TYPE_END_DATE: @@ -181,6 +217,11 @@ switch ($xaction->getTransactionType()) { case PhabricatorCalendarEventTransaction::TYPE_ICON: break; + case PhabricatorCalendarEventTransaction::TYPE_RECURRING: + case PhabricatorCalendarEventTransaction::TYPE_FREQUENCY: + case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: + case PhabricatorCalendarEventTransaction::TYPE_INSTANCE_OF_EVENT: + case PhabricatorCalendarEventTransaction::TYPE_SEQUENCE_INDEX: case PhabricatorCalendarEventTransaction::TYPE_START_DATE: case PhabricatorCalendarEventTransaction::TYPE_END_DATE: case PhabricatorCalendarEventTransaction::TYPE_CANCEL: diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php --- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php @@ -11,6 +11,13 @@ private $creatorPHIDs; private $isCancelled; + private $generateGhosts = false; + + public function setGenerateGhosts($generate_ghosts) { + $this->generateGhosts = $generate_ghosts; + return $this; + } + public function withIDs(array $ids) { $this->ids = $ids; return $this; @@ -69,6 +76,7 @@ protected function loadPage() { $table = new PhabricatorCalendarEvent(); $conn_r = $table->establishConnection('r'); + $viewer = $this->getViewer(); $data = queryfx_all( $conn_r, @@ -86,6 +94,63 @@ $event->applyViewerTimezone($this->getViewer()); } + if (!$this->generateGhosts) { + return $events; + } + + foreach ($events as $event) { + $sequence_start = 0; + $instance_count = null; + + if ($event->getIsRecurring()) { + $frequency = $event->getFrequencyUnit(); + $modify_key = '+1 '.$frequency; + + if ($this->rangeBegin && $this->rangeBegin > $event->getDateFrom()) { + $max_date = $this->rangeBegin; + $date = $event->getDateFrom(); + $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); + + while ($date < $max_date) { + // TODO: optimize this to not loop through all off-screen events + $sequence_start++; + $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); + $date = $datetime->modify($modify_key)->format('U'); + } + + $start = $this->rangeBegin; + } else { + $start = $event->getDateFrom(); + } + + $date = $start; + $start_datetime = PhabricatorTime::getDateTimeFromEpoch( + $start, + $viewer); + + if ($this->rangeEnd) { + $end = $this->rangeEnd; + $instance_count = $sequence_start; + + while ($date < $end) { + $instance_count++; + $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); + $datetime->modify($modify_key); + $date = $datetime->format('U'); + } + } else { + $instance_count = $this->getRawResultLimit(); + } + + $sequence_start = max(1, $sequence_start); + + $max_sequence = $sequence_start + $instance_count; + for ($index = $sequence_start; $index < $max_sequence; $index++) { + $events[] = $event->generateNthGhost($index, $viewer); + } + } + } + return $events; } @@ -122,7 +187,7 @@ if ($this->rangeBegin) { $where[] = qsprintf( $conn_r, - 'event.dateTo >= %d', + 'event.dateTo >= %d OR event.isRecurring = 1', $this->rangeBegin); } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -50,7 +50,8 @@ } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new PhabricatorCalendarEventQuery()); + $query = id(new PhabricatorCalendarEventQuery()) + ->setGenerateGhosts(true); $viewer = $this->requireViewer(); $timezone = new DateTimeZone($viewer->getTimezoneIdentifier()); @@ -132,7 +133,8 @@ $query->withCreatorPHIDs($creator_phids); } - $is_cancelled = $saved->getParameter('isCancelled'); + $is_cancelled = $saved->getParameter('isCancelled', 'active'); + switch ($is_cancelled) { case 'active': $query->withIsCancelled(false); @@ -305,14 +307,14 @@ $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); foreach ($events as $event) { - $href = '/E'.$event->getID(); + // $href = '/E'.$event->getID(); $from = phabricator_datetime($event->getDateFrom(), $viewer); $to = phabricator_datetime($event->getDateTo(), $viewer); $creator_handle = $handles[$event->getUserPHID()]; $item = id(new PHUIObjectItemView()) ->setHeader($event->getName()) - ->setHref($href) + ->setHref($event->getURI()) ->addByline(pht('Creator: %s', $creator_handle->renderLink())) ->addAttribute(pht('From %s to %s', $from, $to)) ->addAttribute(id(new PhutilUTF8StringTruncator()) @@ -371,7 +373,8 @@ $event->setUserPHID($status->getUserPHID()); $event->setDescription(pht('%s (%s)', $name_text, $status_text)); $event->setName($status_text); - $event->setEventID($status->getID()); + $event->setURI($status->getURI()); + // $event->setEventID($status->getID()); $event->setViewerIsInvited($viewer_is_invited); $month_view->addEvent($event); } @@ -423,7 +426,7 @@ $event->setViewerIsInvited($viewer_is_invited); $event->setName($status->getName()); - $event->setURI('/'.$status->getMonogram()); + $event->setURI($status->getURI()); $day_view->addEvent($event); } 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 @@ -20,6 +20,14 @@ protected $icon; protected $mailKey; + protected $isRecurring = 0; + protected $recurrenceFrequency = array(); + protected $recurrenceEndDate; + + private $isGhostEvent = false; + protected $instanceOfEventPHID; + protected $sequenceIndex; + protected $viewPolicy; protected $editPolicy; @@ -36,8 +44,13 @@ ->withClasses(array('PhabricatorCalendarApplication')) ->executeOne(); + $view_policy = null; + $is_recurring = 0; + if ($mode == 'public') { $view_policy = PhabricatorPolicies::getMostOpenPolicy(); + } else if ($mode == 'recurring') { + $is_recurring = true; } else { $view_policy = $actor->getPHID(); } @@ -46,6 +59,7 @@ ->setUserPHID($actor->getPHID()) ->setIsCancelled(0) ->setIsAllDay(0) + ->setIsRecurring($is_recurring) ->setIcon(self::DEFAULT_ICON) ->setViewPolicy($view_policy) ->setEditPolicy($actor->getPHID()) @@ -180,12 +194,19 @@ 'isAllDay' => 'bool', 'icon' => 'text32', 'mailKey' => 'bytes20', + 'isRecurring' => 'bool', + 'recurrenceEndDate' => 'epoch?', + 'instanceOfEventPHID' => 'phid?', + 'sequenceIndex' => 'uint32?', ), self::CONFIG_KEY_SCHEMA => array( 'userPHID_dateFrom' => array( 'columns' => array('userPHID', 'dateTo'), ), ), + self::CONFIG_SERIALIZATION => array( + 'recurrenceFrequency' => self::SERIALIZATION_JSON, + ), ) + parent::getConfiguration(); } @@ -238,6 +259,69 @@ return true; } + public function getIsGhostEvent() { + return $this->isGhostEvent; + } + + public function setIsGhostEvent($is_ghost_event) { + $this->isGhostEvent = $is_ghost_event; + return $this; + } + + public function generateNthGhost( + $sequence_index, + PhabricatorUser $actor) { + + $frequency = $this->getFrequencyUnit(); + $modify_key = '+'.$sequence_index.' '.$frequency; + + $date = $this->dateFrom; + $date_time = PhabricatorTime::getDateTimeFromEpoch($date, $actor); + $date_time->modify($modify_key); + $date = $date_time->format('U'); + + $duration = $this->dateTo - $this->dateFrom; + + $edit_policy = PhabricatorPolicies::POLICY_NOONE; + + $ghost_event = id(clone $this) + ->setIsGhostEvent(true) + ->setDateFrom($date) + ->setDateTo($date + $duration) + ->setIsRecurring(false) + ->setRecurrenceFrequency(null) + ->setInstanceOfEventPHID($this->getPHID()) + ->setSequenceIndex($sequence_index) + ->setEditPolicy($edit_policy); + + return $ghost_event; + } + + public function getFrequencyUnit() { + $frequency = idx($this->recurrenceFrequency, 'rule'); + + switch ($frequency) { + case 'daily': + return 'day'; + case 'weekly': + return 'week'; + case 'monthly': + return 'month'; + case 'yearly': + return 'yearly'; + default: + return 'day'; + } + } + + public function getURI() { + $uri = '/'.$this->getMonogram(); + if ($this->isGhostEvent) { + $uri = $uri.'/'.$this->sequenceIndex; + } + return $uri; + } + /* -( Markup Interface )--------------------------------------------------- */ @@ -307,6 +391,9 @@ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { // The owner of a task can always view and edit it. $user_phid = $this->getUserPHID(); + if ($this->isGhostEvent) { + return false; + } if ($user_phid) { $viewer_phid = $viewer->getPHID(); if ($viewer_phid == $user_phid) { @@ -328,7 +415,8 @@ public function describeAutomaticCapability($capability) { return pht('The owner of an event can always view and edit it, - and invitees can always view it.'); + and invitees can always view it, except if the event is an + instance of a recurring event.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php b/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php --- a/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEventTransaction.php @@ -12,6 +12,13 @@ const TYPE_ICON = 'calendar.icon'; const TYPE_INVITE = 'calendar.invite'; + const TYPE_RECURRING = 'calendar.recurring'; + const TYPE_FREQUENCY = 'calendar.frequency'; + const TYPE_RECURRENCE_END_DATE = 'calendar.recurrenceenddate'; + + const TYPE_INSTANCE_OF_EVENT = 'calendar.instanceofevent'; + const TYPE_SEQUENCE_INDEX = 'calendar.sequenceindex'; + const MAILTAG_RESCHEDULE = 'calendar-reschedule'; const MAILTAG_CONTENT = 'calendar-content'; const MAILTAG_OTHER = 'calendar-other'; @@ -38,6 +45,11 @@ case self::TYPE_DESCRIPTION: case self::TYPE_CANCEL: case self::TYPE_ALL_DAY: + case self::TYPE_RECURRING: + case self::TYPE_FREQUENCY: + case self::TYPE_RECURRENCE_END_DATE: + case self::TYPE_INSTANCE_OF_EVENT: + case self::TYPE_SEQUENCE_INDEX: $phids[] = $this->getObjectPHID(); break; case self::TYPE_INVITE: @@ -60,6 +72,11 @@ case self::TYPE_CANCEL: case self::TYPE_ALL_DAY: case self::TYPE_INVITE: + case self::TYPE_RECURRING: + case self::TYPE_FREQUENCY: + case self::TYPE_RECURRENCE_END_DATE: + case self::TYPE_INSTANCE_OF_EVENT: + case self::TYPE_SEQUENCE_INDEX: return ($old === null); } return parent::shouldHide(); @@ -75,6 +92,11 @@ case self::TYPE_DESCRIPTION: case self::TYPE_ALL_DAY: case self::TYPE_CANCEL: + case self::TYPE_RECURRING: + case self::TYPE_FREQUENCY: + case self::TYPE_RECURRENCE_END_DATE: + case self::TYPE_INSTANCE_OF_EVENT: + case self::TYPE_SEQUENCE_INDEX: return 'fa-pencil'; break; case self::TYPE_INVITE: @@ -231,6 +253,12 @@ } } return $text; + case self::TYPE_RECURRING: + case self::TYPE_FREQUENCY: + case self::TYPE_RECURRENCE_END_DATE: + case self::TYPE_INSTANCE_OF_EVENT: + case self::TYPE_SEQUENCE_INDEX: + return pht('Recurring event has been updated'); } return parent::getTitle(); } @@ -411,6 +439,12 @@ } } return $text; + case self::TYPE_RECURRING: + case self::TYPE_FREQUENCY: + case self::TYPE_RECURRENCE_END_DATE: + case self::TYPE_INSTANCE_OF_EVENT: + case self::TYPE_SEQUENCE_INDEX: + return pht('Recurring event has been updated'); } return parent::getTitleForFeed(); diff --git a/src/view/phui/calendar/PHUICalendarListView.php b/src/view/phui/calendar/PHUICalendarListView.php --- a/src/view/phui/calendar/PHUICalendarListView.php +++ b/src/view/phui/calendar/PHUICalendarListView.php @@ -89,7 +89,7 @@ $content = javelin_tag( 'a', array( - 'href' => '/E'.$event->getEventID(), + 'href' => $event->getURI(), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tip, diff --git a/webroot/rsrc/css/phui/phui-form.css b/webroot/rsrc/css/phui/phui-form.css --- a/webroot/rsrc/css/phui/phui-form.css +++ b/webroot/rsrc/css/phui/phui-form.css @@ -140,7 +140,7 @@ color: {$lightgreytext}; } -select[disabled="disabled"], -input[disabled="disabled"] { +select[disabled], +input[disabled] { opacity: 0.5; } diff --git a/webroot/rsrc/js/application/calendar/behavior-recurring-edit.js b/webroot/rsrc/js/application/calendar/behavior-recurring-edit.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/js/application/calendar/behavior-recurring-edit.js @@ -0,0 +1,14 @@ +/** + * @provides javelin-behavior-recurring-edit + */ + + +JX.behavior('recurring-edit', function(config) { + var checkbox = JX.$(config.isRecurring); + JX.DOM.listen(checkbox, 'change', null, function() { + var frequency = JX.$(config.frequency); + + frequency.disabled = checkbox.checked ? false : true; + }); + +});