diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php index 08c7d91869..9d1c5b6349 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php @@ -1,401 +1,511 @@ getViewer(); $event = $this->loadEvent(); if (!$event) { return new Aphront404Response(); } // If we looked up or generated a stub event, redirect to that event's // canonical URI. $id = $request->getURIData('id'); if ($event->getID() != $id) { $uri = $event->getURI(); return id(new AphrontRedirectResponse())->setURI($uri); } $monogram = $event->getMonogram(); $page_title = $monogram.' '.$event->getName(); $crumbs = $this->buildApplicationCrumbs(); $start = new DateTime('@'.$event->getViewerDateFrom()); $start->setTimeZone($viewer->getTimeZone()); $crumbs->addTextCrumb( $start->format('F Y'), '/calendar/query/month/'.$start->format('Y/m/')); $crumbs->addTextCrumb( $start->format('D jS'), '/calendar/query/month/'.$start->format('Y/m/d/')); $crumbs->addTextCrumb($monogram); $crumbs->setBorder(true); $timeline = $this->buildTransactionTimeline( $event, new PhabricatorCalendarEventTransactionQuery()); $header = $this->buildHeaderView($event); $subheader = $this->buildSubheaderView($event); $curtain = $this->buildCurtain($event); $details = $this->buildPropertySection($event); + $recurring = $this->buildRecurringSection($event); $description = $this->buildDescriptionView($event); $comment_view = id(new PhabricatorCalendarEventEditEngine()) ->setViewer($viewer) ->buildEditEngineCommentView($event); $timeline->setQuoteRef($monogram); $comment_view->setTransactionTimeline($timeline); + $details_header = id(new PHUIHeaderView()) + ->setHeader(pht('Details')); + $recurring_header = $this->buildRecurringHeader($event); + $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) ->setMainColumn( array( $timeline, $comment_view, )) ->setCurtain($curtain) - ->addPropertySection(pht('Details'), $details) + ->addPropertySection($details_header, $details) + ->addPropertySection($recurring_header, $recurring) ->addPropertySection(pht('Description'), $description); return $this->newPage() ->setTitle($page_title) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($event->getPHID())) ->appendChild($view); } private function buildHeaderView( PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); $id = $event->getID(); if ($event->isCancelledEvent()) { $icon = 'fa-ban'; $color = 'red'; $status = pht('Cancelled'); } else { $icon = 'fa-check'; $color = 'bluegrey'; $status = pht('Active'); } - $invite_status = $event->getUserInviteStatus($viewer->getPHID()); - $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; - $is_invite_pending = ($invite_status == $status_invited); - $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($event->getName()) ->setStatus($icon, $color, $status) ->setPolicyObject($event) ->setHeaderIcon($event->getIcon()); - if ($is_invite_pending) { - $decline_button = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-times grey') - ->setHref($this->getApplicationURI("/event/decline/{$id}/")) - ->setWorkflow(true) - ->setText(pht('Decline')); - - $accept_button = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-check green') - ->setHref($this->getApplicationURI("/event/accept/{$id}/")) - ->setWorkflow(true) - ->setText(pht('Accept')); - - $header->addActionLink($decline_button) - ->addActionLink($accept_button); + foreach ($this->buildRSVPActions($event) as $action) { + $header->addActionLink($action); } + return $header; } private function buildCurtain(PhabricatorCalendarEvent $event) { $viewer = $this->getRequest()->getUser(); $id = $event->getID(); $is_attending = $event->getIsUserAttending($viewer->getPHID()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $event, PhabricatorPolicyCapability::CAN_EDIT); $edit_uri = "event/edit/{$id}/"; if ($event->isChildEvent()) { $edit_label = pht('Edit This Instance'); } else { $edit_label = pht('Edit Event'); } $curtain = $this->newCurtainView($event); if ($edit_label && $edit_uri) { $curtain->addAction( id(new PhabricatorActionView()) ->setName($edit_label) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI($edit_uri)) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); } if ($is_attending) { $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Decline Event')) ->setIcon('fa-user-times') ->setHref($this->getApplicationURI("event/join/{$id}/")) ->setWorkflow(true)); } else { $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Join Event')) ->setIcon('fa-user-plus') ->setHref($this->getApplicationURI("event/join/{$id}/")) ->setWorkflow(true)); } $cancel_uri = $this->getApplicationURI("event/cancel/{$id}/"); $cancel_disabled = !$can_edit; if ($event->isChildEvent()) { $cancel_label = pht('Cancel This Instance'); $reinstate_label = pht('Reinstate This Instance'); if ($event->getParentEvent()->getIsCancelled()) { $cancel_disabled = true; } } else if ($event->isParentEvent()) { $cancel_label = pht('Cancel All'); $reinstate_label = pht('Reinstate All'); } else { $cancel_label = pht('Cancel Event'); $reinstate_label = pht('Reinstate Event'); } if ($event->isCancelledEvent()) { $curtain->addAction( id(new PhabricatorActionView()) ->setName($reinstate_label) ->setIcon('fa-plus') ->setHref($cancel_uri) ->setDisabled($cancel_disabled) ->setWorkflow(true)); } else { $curtain->addAction( id(new PhabricatorActionView()) ->setName($cancel_label) ->setIcon('fa-times') ->setHref($cancel_uri) ->setDisabled($cancel_disabled) ->setWorkflow(true)); } return $curtain; } private function buildPropertySection( PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer); - if ($event->getIsRecurring()) { - $properties->addProperty( - pht('Recurs'), - ucwords(idx($event->getRecurrenceFrequency(), 'rule'))); - - if ($event->getRecurrenceEndDate()) { - $properties->addProperty( - pht('Recurrence Ends'), - phabricator_datetime($event->getRecurrenceEndDate(), $viewer)); - } - - if ($event->getInstanceOfEventPHID()) { - $properties->addProperty( - pht('Recurrence of Event'), - pht('%s of %s', - $event->getSequenceIndex(), - $viewer->renderHandle($event->getInstanceOfEventPHID())->render())); - } - } - $invitees = $event->getInvitees(); foreach ($invitees as $key => $invitee) { if ($invitee->isUninvited()) { unset($invitees[$key]); } } if ($invitees) { $invitee_list = new PHUIStatusListView(); $icon_invited = PHUIStatusItemView::ICON_OPEN; $icon_attending = PHUIStatusItemView::ICON_ACCEPT; $icon_declined = PHUIStatusItemView::ICON_REJECT; $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; $status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; $status_declined = PhabricatorCalendarEventInvitee::STATUS_DECLINED; $icon_map = array( $status_invited => $icon_invited, $status_attending => $icon_attending, $status_declined => $icon_declined, ); $icon_color_map = array( $status_invited => null, $status_attending => 'green', $status_declined => 'red', ); foreach ($invitees as $invitee) { $item = new PHUIStatusItemView(); $invitee_phid = $invitee->getInviteePHID(); $status = $invitee->getStatus(); $target = $viewer->renderHandle($invitee_phid); $icon = $icon_map[$status]; $icon_color = $icon_color_map[$status]; $item->setIcon($icon, $icon_color) ->setTarget($target); $invitee_list->addItem($item); } } else { $invitee_list = phutil_tag( 'em', array(), pht('None')); } $properties->addProperty( pht('Invitees'), $invitee_list); $properties->invokeWillRenderEvent(); return $properties; } + private function buildRecurringHeader(PhabricatorCalendarEvent $event) { + $viewer = $this->getViewer(); + + if (!$event->getIsRecurring()) { + return null; + } + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Recurring Event')); + + $sequence = $event->getSequenceIndex(); + if ($event->isParentEvent()) { + $parent = $event; + } else { + $parent = $event->getParentEvent(); + } + + $next_uri = $parent->getURI().'/'.($sequence + 1); + if ($sequence) { + if ($sequence > 1) { + $previous_uri = $parent->getURI().'/'.($sequence - 1); + } else { + $previous_uri = $parent->getURI(); + } + $has_previous = true; + } else { + $has_previous = false; + $previous_uri = null; + } + + $prev_button = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-chevron-left') + ->setHref($previous_uri) + ->setDisabled(!$has_previous) + ->setText(pht('Previous')); + + $next_button = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-chevron-right') + ->setHref($next_uri) + ->setText(pht('Next')); + + $header + ->addActionLink($next_button) + ->addActionLink($prev_button); + + return $header; + } + + private function buildRecurringSection(PhabricatorCalendarEvent $event) { + $viewer = $this->getViewer(); + + if (!$event->getIsRecurring()) { + return null; + } + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $is_parent = $event->isParentEvent(); + if ($is_parent) { + $parent_link = null; + } else { + $parent = $event->getParentEvent(); + $parent_link = $viewer + ->renderHandle($parent->getPHID()) + ->render(); + } + + $rule = $event->getFrequencyRule(); + switch ($rule) { + case PhabricatorCalendarEvent::FREQUENCY_DAILY: + if ($is_parent) { + $message = pht('This event repeats every day.'); + } else { + $message = pht( + 'This event is an instance of %s, and repeats every day.', + $parent_link); + } + break; + case PhabricatorCalendarEvent::FREQUENCY_WEEKLY: + if ($is_parent) { + $message = pht('This event repeats every week.'); + } else { + $message = pht( + 'This event is an instance of %s, and repeats every week.', + $parent_link); + } + break; + case PhabricatorCalendarEvent::FREQUENCY_MONTHLY: + if ($is_parent) { + $message = pht('This event repeats every month.'); + } else { + $message = pht( + 'This event is an instance of %s, and repeats every month.', + $parent_link); + } + break; + case PhabricatorCalendarEvent::FREQUENCY_YEARLY: + if ($is_parent) { + $message = pht('This event repeats every year.'); + } else { + $message = pht( + 'This event is an instance of %s, and repeats every year.', + $parent_link); + } + break; + } + + $properties->addProperty(pht('Event Series'), $message); + + return $properties; + } + private function buildDescriptionView( PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer); if (strlen($event->getDescription())) { $description = new PHUIRemarkupView($viewer, $event->getDescription()); $properties->addTextContent($description); return $properties; } return null; } - private function loadEvent() { $request = $this->getRequest(); $viewer = $this->getViewer(); $id = $request->getURIData('id'); $sequence = $request->getURIData('sequence'); // We're going to figure out which event you're trying to look at. Most of // the time this is simple, but you may be looking at an instance of a // recurring event which we haven't generated an object for. // If you are, we're going to generate a "stub" event so we have a real // ID and PHID to work with, since the rest of the infrastructure relies // on these identifiers existing. // Load the event identified by ID first. $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$event) { return null; } // If we aren't looking at an instance of this event, this is a completely // normal request and we can just return this event. if (!$sequence) { return $event; } // When you view "E123/999", E123 is normally the parent event. However, // you might visit a different instance first instead and then fiddle // with the URI. If the event we're looking at is a child, we are going // to act on the parent instead. if ($event->isChildEvent()) { $event = $event->getParentEvent(); } // Try to load the instance. If it already exists, we're all done and // can just return it. $instance = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withInstanceSequencePairs( array( array($event->getPHID(), $sequence), )) ->executeOne(); if ($instance) { return $instance; } if (!$viewer->isLoggedIn()) { throw new Exception( pht( 'This event instance has not been created yet. Log in to create '. 'it.')); } $instance = $event->newStub($viewer, $sequence); return $instance; } private function buildSubheaderView(PhabricatorCalendarEvent $event) { $viewer = $this->getViewer(); $host_phid = $event->getHostPHID(); $handles = $viewer->loadHandles(array($host_phid)); $handle = $handles[$host_phid]; $host = $viewer->renderHandle($host_phid); $host = phutil_tag('strong', array(), $host); $image_uri = $handles[$host_phid]->getImageURI(); $image_href = $handles[$host_phid]->getURI(); $date = $event->renderEventDate($viewer, true); $content = pht('Hosted by %s on %s.', $host, $date); return id(new PHUIHeadThingView()) ->setImage($image_uri) ->setImageHref($image_href) ->setContent($content); } + private function buildRSVPActions(PhabricatorCalendarEvent $event) { + $viewer = $this->getViewer(); + $id = $event->getID(); + + $invite_status = $event->getUserInviteStatus($viewer->getPHID()); + $status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED; + $is_invite_pending = ($invite_status == $status_invited); + if (!$is_invite_pending) { + return array(); + } + + $decline_button = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-times grey') + ->setHref($this->getApplicationURI("/event/decline/{$id}/")) + ->setWorkflow(true) + ->setText(pht('Decline')); + + $accept_button = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-check green') + ->setHref($this->getApplicationURI("/event/accept/{$id}/")) + ->setWorkflow(true) + ->setText(pht('Accept')); + + return array($decline_button, $accept_button); + } + } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index ca42f872f7..6e53751117 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1,780 +1,780 @@ setViewer($actor) ->withClasses(array('PhabricatorCalendarApplication')) ->executeOne(); $view_default = PhabricatorCalendarEventDefaultViewCapability::CAPABILITY; $edit_default = PhabricatorCalendarEventDefaultEditCapability::CAPABILITY; $view_policy = $app->getPolicy($view_default); $edit_policy = $app->getPolicy($edit_default); $now = PhabricatorTime::getNow(); $start = new DateTime('@'.$now); $start->setTimeZone($actor->getTimeZone()); $start->setTime($start->format('H'), 0, 0); $start->modify('+1 hour'); $end = id(clone $start)->modify('+1 hour'); $epoch_min = $start->format('U'); $epoch_max = $end->format('U'); $now_date = new DateTime('@'.$now); $now_min = id(clone $now_date)->setTime(0, 0)->format('U'); $now_max = id(clone $now_date)->setTime(23, 59)->format('U'); $default_icon = 'fa-calendar'; return id(new PhabricatorCalendarEvent()) ->setHostPHID($actor->getPHID()) ->setIsCancelled(0) ->setIsAllDay(0) ->setIsStub(0) ->setIsRecurring(0) ->setRecurrenceFrequency( array( 'rule' => self::FREQUENCY_WEEKLY, )) ->setIcon($default_icon) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setSpacePHID($actor->getDefaultSpacePHID()) ->attachInvitees(array()) ->setDateFrom($epoch_min) ->setDateTo($epoch_max) ->setAllDayDateFrom($now_min) ->setAllDayDateTo($now_max) ->applyViewerTimezone($actor); } private function newChild(PhabricatorUser $actor, $sequence) { if (!$this->isParentEvent()) { throw new Exception( pht( 'Unable to generate a new child event for an event which is not '. 'a recurring parent event!')); } $child = id(new self()) ->setIsCancelled(0) ->setIsStub(0) ->setInstanceOfEventPHID($this->getPHID()) ->setSequenceIndex($sequence) ->setIsRecurring(true) ->setRecurrenceFrequency($this->getRecurrenceFrequency()) ->attachParentEvent($this); return $child->copyFromParent($actor); } protected function readField($field) { static $inherit = array( 'hostPHID' => true, 'isAllDay' => true, 'icon' => true, 'spacePHID' => true, 'viewPolicy' => true, 'editPolicy' => true, 'name' => true, 'description' => true, ); // Read these fields from the parent event instead of this event. For // example, we want any changes to the parent event's name to if (isset($inherit[$field])) { if ($this->getIsStub()) { // TODO: This should be unconditional, but the execution order of // CalendarEventQuery and applyViewerTimezone() are currently odd. if ($this->parentEvent !== self::ATTACHABLE) { return $this->getParentEvent()->readField($field); } } } return parent::readField($field); } public function copyFromParent(PhabricatorUser $actor) { if (!$this->isChildEvent()) { throw new Exception( pht( 'Unable to copy from parent event: this is not a child event.')); } $parent = $this->getParentEvent(); $this ->setHostPHID($parent->getHostPHID()) ->setIsAllDay($parent->getIsAllDay()) ->setIcon($parent->getIcon()) ->setSpacePHID($parent->getSpacePHID()) ->setViewPolicy($parent->getViewPolicy()) ->setEditPolicy($parent->getEditPolicy()) ->setName($parent->getName()) ->setDescription($parent->getDescription()); $frequency = $parent->getFrequencyUnit(); $modify_key = '+'.$this->getSequenceIndex().' '.$frequency; $date = $parent->getDateFrom(); $date_time = PhabricatorTime::getDateTimeFromEpoch($date, $actor); $date_time->modify($modify_key); $date = $date_time->format('U'); $duration = $this->getDuration(); $utc = new DateTimeZone('UTC'); $allday_from = $parent->getAllDayDateFrom(); $allday_date = new DateTime('@'.$allday_from, $utc); $allday_date->setTimeZone($utc); $allday_date->modify($modify_key); $allday_min = $allday_date->format('U'); $allday_duration = ($parent->getAllDayDateTo() - $allday_from); $this ->setDateFrom($date) ->setDateTo($date + $duration) ->setAllDayDateFrom($allday_min) ->setAllDayDateTo($allday_min + $allday_duration); return $this; } public function newStub(PhabricatorUser $actor, $sequence) { $stub = $this->newChild($actor, $sequence); $stub->setIsStub(1); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $stub->save(); unset($unguarded); $stub->applyViewerTimezone($actor); return $stub; } public function newGhost(PhabricatorUser $actor, $sequence) { $ghost = $this->newChild($actor, $sequence); $ghost ->setIsGhostEvent(true) ->makeEphemeral(); $ghost->applyViewerTimezone($actor); return $ghost; } public function getViewerDateFrom() { if ($this->viewerDateFrom === null) { throw new PhutilInvalidStateException('applyViewerTimezone'); } return $this->viewerDateFrom; } public function getViewerDateTo() { if ($this->viewerDateTo === null) { throw new PhutilInvalidStateException('applyViewerTimezone'); } return $this->viewerDateTo; } public function applyViewerTimezone(PhabricatorUser $viewer) { if (!$this->getIsAllDay()) { $this->viewerDateFrom = $this->getDateFrom(); $this->viewerDateTo = $this->getDateTo(); } else { $zone = $viewer->getTimeZone(); $this->viewerDateFrom = $this->getDateEpochForTimezone( $this->getAllDayDateFrom(), new DateTimeZone('UTC'), 'Y-m-d', null, $zone); $this->viewerDateTo = $this->getDateEpochForTimezone( $this->getAllDayDateTo(), new DateTimeZone('UTC'), 'Y-m-d 23:59:00', null, $zone); } return $this; } public function getDuration() { return $this->getDateTo() - $this->getDateFrom(); } public function getDateEpochForTimezone( $epoch, $src_zone, $format, $adjust, $dst_zone) { $src = new DateTime('@'.$epoch); $src->setTimeZone($src_zone); if (strlen($adjust)) { $adjust = ' '.$adjust; } $dst = new DateTime($src->format($format).$adjust, $dst_zone); return $dst->format('U'); } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } return parent::save(); } /** * Get the event start epoch for evaluating invitee availability. * * When assessing availability, we pretend events start earlier than they * really do. This allows us to mark users away for the entire duration of a * series of back-to-back meetings, even if they don't strictly overlap. * * @return int Event start date for availability caches. */ public function getDateFromForCache() { return ($this->getViewerDateFrom() - phutil_units('15 minutes in seconds')); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text', 'dateFrom' => 'epoch', 'dateTo' => 'epoch', 'allDayDateFrom' => 'epoch', 'allDayDateTo' => 'epoch', 'description' => 'text', 'isCancelled' => 'bool', 'isAllDay' => 'bool', 'icon' => 'text32', 'mailKey' => 'bytes20', 'isRecurring' => 'bool', 'recurrenceEndDate' => 'epoch?', 'instanceOfEventPHID' => 'phid?', 'sequenceIndex' => 'uint32?', 'isStub' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_date' => array( 'columns' => array('dateFrom', 'dateTo'), ), 'key_instance' => array( 'columns' => array('instanceOfEventPHID', 'sequenceIndex'), 'unique' => true, ), ), self::CONFIG_SERIALIZATION => array( 'recurrenceFrequency' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorCalendarEventPHIDType::TYPECONST); } public function getMonogram() { return 'E'.$this->getID(); } public function getInvitees() { return $this->assertAttached($this->invitees); } public function attachInvitees(array $invitees) { $this->invitees = $invitees; return $this; } public function getInviteePHIDsForEdit() { $invitees = array(); foreach ($this->getInvitees() as $invitee) { if ($invitee->isUninvited()) { continue; } $invitees[] = $invitee->getInviteePHID(); } return $invitees; } public function getUserInviteStatus($phid) { $invitees = $this->getInvitees(); $invitees = mpull($invitees, null, 'getInviteePHID'); $invited = idx($invitees, $phid); if (!$invited) { return PhabricatorCalendarEventInvitee::STATUS_UNINVITED; } $invited = $invited->getStatus(); return $invited; } public function getIsUserAttending($phid) { $attending_status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; $old_status = $this->getUserInviteStatus($phid); $is_attending = ($old_status == $attending_status); return $is_attending; } public function getIsUserInvited($phid) { $uninvited_status = PhabricatorCalendarEventInvitee::STATUS_UNINVITED; $declined_status = PhabricatorCalendarEventInvitee::STATUS_DECLINED; $status = $this->getUserInviteStatus($phid); if ($status == $uninvited_status || $status == $declined_status) { return false; } return true; } public function getIsGhostEvent() { return $this->isGhostEvent; } public function setIsGhostEvent($is_ghost_event) { $this->isGhostEvent = $is_ghost_event; return $this; } public function getFrequencyRule() { return idx($this->recurrenceFrequency, 'rule'); } public function getFrequencyUnit() { $frequency = $this->getFrequencyRule(); switch ($frequency) { case 'daily': return 'day'; case 'weekly': return 'week'; case 'monthly': return 'month'; case 'yearly': return 'year'; default: return 'day'; } } public function getURI() { if ($this->getIsGhostEvent()) { $base = $this->getParentEvent()->getURI(); $sequence = $this->getSequenceIndex(); return "{$base}/{$sequence}/"; } return '/'.$this->getMonogram(); } public function getParentEvent() { return $this->assertAttached($this->parentEvent); } public function attachParentEvent($event) { $this->parentEvent = $event; return $this; } public function isParentEvent() { - return ($this->isRecurring && !$this->instanceOfEventPHID); + return ($this->getIsRecurring() && !$this->getInstanceOfEventPHID()); } public function isChildEvent() { return ($this->instanceOfEventPHID !== null); } public function isCancelledEvent() { if ($this->getIsCancelled()) { return true; } if ($this->isChildEvent()) { if ($this->getParentEvent()->getIsCancelled()) { return true; } } return false; } public function renderEventDate( PhabricatorUser $viewer, $show_end) { if ($show_end) { $min_date = PhabricatorTime::getDateTimeFromEpoch( $this->getViewerDateFrom(), $viewer); $max_date = PhabricatorTime::getDateTimeFromEpoch( $this->getViewerDateTo(), $viewer); $min_day = $min_date->format('Y m d'); $max_day = $max_date->format('Y m d'); $show_end_date = ($min_day != $max_day); } else { $show_end_date = false; } $min_epoch = $this->getViewerDateFrom(); $max_epoch = $this->getViewerDateTo(); if ($this->getIsAllDay()) { if ($show_end_date) { return pht( '%s - %s, All Day', phabricator_date($min_epoch, $viewer), phabricator_date($max_epoch, $viewer)); } else { return pht( '%s, All Day', phabricator_date($min_epoch, $viewer)); } } else if ($show_end_date) { return pht( '%s - %s', phabricator_datetime($min_epoch, $viewer), phabricator_datetime($max_epoch, $viewer)); } else if ($show_end) { return pht( '%s - %s', phabricator_datetime($min_epoch, $viewer), phabricator_time($max_epoch, $viewer)); } else { return pht( '%s', phabricator_datetime($min_epoch, $viewer)); } } public function getDisplayIcon(PhabricatorUser $viewer) { if ($this->isCancelledEvent()) { return 'fa-times'; } if ($viewer->isLoggedIn()) { $status = $this->getUserInviteStatus($viewer->getPHID()); switch ($status) { case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: return 'fa-check-circle'; case PhabricatorCalendarEventInvitee::STATUS_INVITED: return 'fa-user-plus'; case PhabricatorCalendarEventInvitee::STATUS_DECLINED: return 'fa-times'; } } return $this->getIcon(); } public function getDisplayIconColor(PhabricatorUser $viewer) { if ($this->isCancelledEvent()) { return 'red'; } if ($viewer->isLoggedIn()) { $status = $this->getUserInviteStatus($viewer->getPHID()); switch ($status) { case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: return 'green'; case PhabricatorCalendarEventInvitee::STATUS_INVITED: return 'green'; case PhabricatorCalendarEventInvitee::STATUS_DECLINED: return 'grey'; } } return 'bluegrey'; } public function getDisplayIconLabel(PhabricatorUser $viewer) { if ($this->isCancelledEvent()) { return pht('Cancelled'); } if ($viewer->isLoggedIn()) { $status = $this->getUserInviteStatus($viewer->getPHID()); switch ($status) { case PhabricatorCalendarEventInvitee::STATUS_ATTENDING: return pht('Attending'); case PhabricatorCalendarEventInvitee::STATUS_INVITED: return pht('Invited'); case PhabricatorCalendarEventInvitee::STATUS_DECLINED: return pht('Declined'); } } return null; } /* -( Markup Interface )--------------------------------------------------- */ /** * @task markup */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "calendar:T{$id}:{$field}:{$hash}"; } /** * @task markup */ public function getMarkupText($field) { return $this->getDescription(); } /** * @task markup */ public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newCalendarMarkupEngine(); } /** * @task markup */ public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } /** * @task markup */ public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { // The host of an event can always view and edit it. $user_phid = $this->getHostPHID(); if ($user_phid) { $viewer_phid = $viewer->getPHID(); if ($viewer_phid == $user_phid) { return true; } } if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { $status = $this->getUserInviteStatus($viewer->getPHID()); if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED || $status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING || $status == PhabricatorCalendarEventInvitee::STATUS_DECLINED) { return true; } } return false; } public function describeAutomaticCapability($capability) { return pht( 'The host of an event can always view and edit it. Users who are '. 'invited to an event can always view it.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorCalendarEventEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorCalendarEventTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getHostPHID()); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array($this->getHostPHID()); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSpacesInterface )----------------------------------------- */ public function getSpacePHID() { return $this->spacePHID; } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhabricatorCalendarEventFulltextEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the event.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('description') ->setType('string') ->setDescription(pht('The event description.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'description' => $this->getDescription(), ); } public function getConduitSearchAttachments() { return array(); } } diff --git a/src/view/phui/PHUITwoColumnView.php b/src/view/phui/PHUITwoColumnView.php index 4ada03240d..1c56886f7a 100644 --- a/src/view/phui/PHUITwoColumnView.php +++ b/src/view/phui/PHUITwoColumnView.php @@ -1,209 +1,224 @@ mainColumn = $main; return $this; } public function setSideColumn($side) { $this->sideColumn = $side; return $this; } public function setNavigation($nav) { $this->navigation = $nav; $this->display = self::DISPLAY_LEFT; return $this; } public function setHeader(PHUIHeaderView $header) { $this->header = $header; return $this; } public function setSubheader($subheader) { $this->subheader = $subheader; return $this; } public function setFooter($footer) { $this->footer = $footer; return $this; } public function addPropertySection($title, $section) { - $this->propertySection[] = array($title, $section); + $this->propertySection[] = array( + 'header' => $title, + 'content' => $section, + ); return $this; } public function setCurtain(PHUICurtainView $curtain) { $this->curtain = $curtain; return $this; } public function getCurtain() { return $this->curtain; } public function setFluid($fluid) { $this->fluid = $fluid; return $this; } public function setDisplay($display) { $this->display = $display; return $this; } private function getDisplay() { if ($this->display) { return $this->display; } else { return self::DISPLAY_RIGHT; } } protected function getTagAttributes() { $classes = array(); $classes[] = 'phui-two-column-view'; $classes[] = $this->getDisplay(); if ($this->fluid) { $classes[] = 'phui-two-column-fluid'; } if ($this->subheader) { $classes[] = 'with-subheader'; } return array( 'class' => implode(' ', $classes), ); } protected function getTagContent() { require_celerity_resource('phui-two-column-view-css'); $main = $this->buildMainColumn(); $side = $this->buildSideColumn(); $footer = $this->buildFooter(); $order = array($side, $main); $inner = phutil_tag_div('phui-two-column-row grouped', $order); $table = phutil_tag_div('phui-two-column-content', $inner); $header = null; if ($this->header) { $curtain = $this->getCurtain(); if ($curtain) { $action_list = $curtain->getActionList(); $this->header->setActionListID($action_list->getID()); } $header = phutil_tag_div( 'phui-two-column-header', $this->header); } $subheader = null; if ($this->subheader) { $subheader = phutil_tag_div( 'phui-two-column-subheader', $this->subheader); } return phutil_tag( 'div', array( 'class' => 'phui-two-column-container', ), array( $header, $subheader, $table, $footer, )); } private function buildMainColumn() { $view = array(); $sections = $this->propertySection; if ($sections) { - foreach ($sections as $content) { - if ($content[1]) { - $view[] = id(new PHUIObjectBoxView()) - ->setHeaderText($content[0]) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($content[1]); + foreach ($sections as $section) { + $section_header = $section['header']; + + $section_content = $section['content']; + if ($section_content === null) { + continue; } + + if ($section_header instanceof PHUIHeaderView) { + $header = $section_header; + } else { + $header = id(new PHUIHeaderView()) + ->setHeader($section_header); + } + + $view[] = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($section_content); } } return phutil_tag( 'div', array( 'class' => 'phui-main-column', ), array( $view, $this->mainColumn, )); } private function buildSideColumn() { $classes = array(); $classes[] = 'phui-side-column'; $navigation = null; if ($this->navigation) { $classes[] = 'side-has-nav'; $navigation = id(new PHUIObjectBoxView()) ->appendChild($this->navigation); } $curtain = $this->getCurtain(); return phutil_tag( 'div', array( 'class' => implode($classes, ' '), ), array( $navigation, $curtain, $this->sideColumn, )); } private function buildFooter() { $footer = $this->footer; return phutil_tag( 'div', array( 'class' => 'phui-two-column-content phui-two-column-footer', ), array( $footer, )); } }